diff --git a/cmd/cli/create.go b/cmd/cli/create.go index c3c846b7..c515c6de 100644 --- a/cmd/cli/create.go +++ b/cmd/cli/create.go @@ -49,9 +49,13 @@ func init() { createClusterCmd.Flags().String("provider", "kind", "Cluster provider (only 'kind' is supported)") // Shadow global flags + createCmd.PersistentFlags().StringSlice("file", nil, "") createCmd.PersistentFlags().Bool("debug", false, "") createCmd.PersistentFlags().String("kubeconfig", "", "") createCmd.PersistentFlags().Bool("verbose", false, "") + + // Hide them from help output + createCmd.PersistentFlags().MarkHidden("file") createCmd.PersistentFlags().MarkHidden("debug") createCmd.PersistentFlags().MarkHidden("kubeconfig") createCmd.PersistentFlags().MarkHidden("verbose") diff --git a/cmd/cli/create_pattern.go b/cmd/cli/create_pattern.go new file mode 100644 index 00000000..2e921b5e --- /dev/null +++ b/cmd/cli/create_pattern.go @@ -0,0 +1,115 @@ +//go:build !runtime && !gateway + +package cli + +import ( + "fmt" + "path/filepath" + + "github.com/orkspace/orkestra/pkg/generate" + "github.com/spf13/cobra" +) + +var createPatternCmd = &cobra.Command{ + Use: "pattern", + Short: "Scaffold a new Orkestra pattern: katalog.yaml, simulate.yaml, e2e.yaml, README.md", + Long: `Creates the files needed to build, test, and publish an Orkestra pattern. + +Always written: + katalog.yaml — operator declaration + simulate.yaml — in-memory test scaffold (ork simulate) + e2e.yaml — real-cluster integration test scaffold (ork e2e) + README.md — actionable steps from edit to release + +Also written when --typed, --add-hook, or --add-constructor: + values.yaml — runtime image (set before ork e2e) + Makefile — registry, build, build-runtime, docker, push, release + Dockerfile — production container image (distroless, runtime binary only) + +Typed mode flags are forwarded to katalog generation: + --add-hook Include a hooks section + --add-constructor Include a constructor section + --typed Include both hooks and constructor (commented) + +Examples: + ork create pattern + ork create pattern --add-hook -o ./my-operator/ + ork create pattern --typed`, + RunE: func(cmd *cobra.Command, args []string) error { + addHook, _ := cmd.Flags().GetBool("add-hook") + addConstructor, _ := cmd.Flags().GetBool("add-constructor") + typed, _ := cmd.Flags().GetBool("typed") + outputDir, _ := cmd.Flags().GetString("output") + + if outputDir == "" { + outputDir = "." + } + + isTyped := typed || addHook || addConstructor + + katalogOpts := generate.KatalogScaffoldOptions{ + AddHook: addHook, + AddConstructor: addConstructor, + Typed: typed, + OutputFile: filepath.Join(outputDir, fileKatalog), + } + if err := katalogOpts.Validate(); err != nil { + return err + } + + fmt.Printf("generating pattern scaffold → %s/\n", outputDir) + + if _, err := generate.KatalogScaffold(katalogOpts); err != nil { + return fmt.Errorf("generating %s: %w", fileKatalog, err) + } + + if err := generate.WriteSimulateScaffold(filepath.Join(outputDir, fileSimulate)); err != nil { + return fmt.Errorf("generating %s: %w", fileSimulate, err) + } + + if err := generate.WriteE2EScaffold(filepath.Join(outputDir, fileE2e), isTyped); err != nil { + return fmt.Errorf("generating %s: %w", fileE2e, err) + } + + if err := generate.WriteREADME(filepath.Join(outputDir, fileReadMe), isTyped); err != nil { + return fmt.Errorf("generating %s: %w", fileReadMe, err) + } + + if isTyped { + if err := generate.WriteValuesYAML(filepath.Join(outputDir, fileValues)); err != nil { + return fmt.Errorf("generating %s: %w", fileValues, err) + } + if err := generate.WriteMakefile(filepath.Join(outputDir, fileMakeFile)); err != nil { + return fmt.Errorf("generating %s: %w", fileMakeFile, err) + } + if err := generate.WriteDockerfile(filepath.Join(outputDir, fileDockerfile)); err != nil { + return fmt.Errorf("generating %s: %w", fileDockerfile, err) + } + } + + fmt.Printf("\n→ pattern scaffold written to %s\n", bold(outputDir+"/")) + fmt.Printf(" %s %-16s %s\n", successMark(), fileKatalog, dim("declare your CRD(s) and resources")) + fmt.Printf(" %s %-16s %s\n", successMark(), fileSimulate, dim("ork simulate")) + fmt.Printf(" %s %-16s %s\n", successMark(), fileE2e, dim("ork e2e")) + fmt.Printf(" %s %-16s %s\n", successMark(), fileReadMe, dim("start here")) + if isTyped { + fmt.Printf(" %s %-16s %s\n", successMark(), fileValues, dim("set runtime.image before ork e2e")) + fmt.Printf(" %s %-16s %s\n", successMark(), fileMakeFile, dim("make registry, make build, make release")) + fmt.Printf(" %s %-16s %s\n", successMark(), fileDockerfile, dim("production container image")) + } + fmt.Println() + return nil + }, +} + +func init() { + createCmd.AddCommand(createPatternCmd) + createPatternCmd.Flags().Bool("add-hook", false, + "Typed mode: include a hooks section in katalog.yaml (also writes Makefile + Dockerfile)") + createPatternCmd.Flags().Bool("add-constructor", false, + "Typed mode: include a constructor section in katalog.yaml (also writes Makefile + Dockerfile)") + createPatternCmd.Flags().Bool("typed", false, + "Typed mode: include both hooks and constructor commented (also writes Makefile + Dockerfile)") + createPatternCmd.Flags().StringP("output", "o", "", + "Output directory (default: current directory)") +} diff --git a/cmd/cli/files.go b/cmd/cli/files.go index 8a01d339..a8e96e46 100644 --- a/cmd/cli/files.go +++ b/cmd/cli/files.go @@ -7,12 +7,16 @@ import ( ) const ( - fileKatalog = "katalog.yaml" - fileKomposer = "komposer.yaml" - fileE2e = "e2e.yaml" - fileSimulate = "simulate.yaml" - fileCrd = "crd.yaml" - fileCr = "cr.yaml" + fileKatalog = "katalog.yaml" + fileKomposer = "komposer.yaml" + fileE2e = "e2e.yaml" + fileSimulate = "simulate.yaml" + fileCrd = "crd.yaml" + fileCr = "cr.yaml" + fileReadMe = "README.md" + fileMakeFile = "Makefile" + fileDockerfile = "Dockerfile" + fileValues = "values.yaml" ) // resolveKatalogPaths resolves the katalog file paths in the following order: diff --git a/documentation/concepts/patterns/index.md b/documentation/concepts/patterns/index.md index a4b01d08..403fd7c1 100644 --- a/documentation/concepts/patterns/index.md +++ b/documentation/concepts/patterns/index.md @@ -48,4 +48,14 @@ This is the same shift containers made for applications. Patterns make operator YAML files are documents. Patterns are something more specific: they encode a solution to a named problem that recurs in the operator world. The name reflects intent — not format. +--- + +## Pages + +| Page | What it covers | +|------|---------------| +| [Scaffolding a pattern](scaffold.md) | `ork create pattern` — generate the full file set to build, test, and publish | + +--- + → See the [Pattern kinds in the registry](../../orkestra-registry/index.md) — examples of Katalogs, Motifs, and Komposers in practice. diff --git a/documentation/concepts/patterns/scaffold.md b/documentation/concepts/patterns/scaffold.md new file mode 100644 index 00000000..513fb6db --- /dev/null +++ b/documentation/concepts/patterns/scaffold.md @@ -0,0 +1,141 @@ +# Scaffolding a Pattern + +`ork create pattern` generates the full file set needed to build, test, and publish an Orkestra pattern. It surfaces the same katalog scaffolding as `ork generate katalog` and adds the testing layer — `simulate.yaml` and `e2e.yaml` — so the operator suite is complete from the start. + +```bash +ork create pattern +ork create pattern -o ./my-operator/ +ork create pattern --add-hook -o ./my-operator/ +``` + +--- + +## What gets generated + +```text +my-operator/ + katalog.yaml — operator declaration + simulate.yaml — in-memory test (ork simulate) + e2e.yaml — real-cluster test (ork e2e) + README.md — actionable steps from edit to release +``` + +In typed mode (`--add-hook`, `--add-constructor`, `--typed`): + +```text + values.yaml — runtime image (set before ork e2e) + Makefile — registry, build, build-runtime, docker, push, release + Dockerfile — production container image (distroless, runtime binary only) +``` + +--- + +## Dynamic vs typed + +The typed flags are forwarded directly to katalog generation — they behave identically to `ork generate katalog`. + +| Flag | Katalog mode | Extra files | +|------|-------------|-------------| +| *(none)* | Dynamic — declarative templates only | — | +| `--add-hook` | Typed — commented `hooks` declaration | `values.yaml`, `Makefile`, `Dockerfile` | +| `--add-constructor` | Typed — commented `constructor` declaration | `values.yaml`, `Makefile`, `Dockerfile` | +| `--typed` | Typed — both sections commented, pick one | `values.yaml`, `Makefile`, `Dockerfile` | + +--- + +## From scaffold to running operator + +### Dynamic mode + +```bash +# 1. Generate +ork create pattern -o ./my-operator/ +cd my-operator/ + +# 2. Edit katalog.yaml — fill in spec.crds, declare resources +# 3. Validate +ork validate + +# 4. Simulate — no cluster needed +ork simulate + +# 5. Run locally +ork run --dev + +# 6. E2E test +ork e2e + +# 7. Observe +ork control +``` + +### Typed mode + +```bash +# 1. Generate +ork create pattern --add-hook -o ./my-operator/ +cd my-operator/ + +# 2. Edit katalog.yaml — fill in apiTypes +# 3. Generate type registry +make registry + +# 4. Write your hook function + +# 5. Release the image +make release IMAGE=ghcr.io/myorg/my-operator:v0.1.0 + +# 6. Set the image in values.yaml +# runtime.image.repository: ghcr.io/myorg/my-operator +# runtime.image.tag: v0.1.0 + +# 7. Validate +ork validate + +# 8. Simulate +ork simulate + +# 9. Run locally +ork run --dev + +# 10. E2E test — passes values.yaml to the Orkestra Helm chart +ork e2e + +# 11. Push +ork push +``` + +--- + +## The testing layer + +`simulate.yaml` and `e2e.yaml` are starters — they reference `./katalog.yaml` and `./cr.yaml` but have no assertions yet. + +Add assertions before running, or generate them from a live run: + +```bash +ork simulate init +``` + +See [Simulate](../simulate/index.md) and [E2E](../e2e/index.md) for the full test authoring guide. + +--- + +## Publishing + +Once your tests pass, push to the registry: + +```bash +ork push +``` + +In typed mode, release the image first, then update `values.yaml` so `ork e2e` can pass it to the Orkestra Helm chart, then push the katalog: + +```bash +make release IMAGE=ghcr.io/myorg/my-operator:v0.1.0 +# edit values.yaml: set runtime.image.repository and runtime.image.tag +ork e2e +ork push +``` + +→ See [`ork create pattern` CLI reference](../../reference/cli/create.md#ork-create-pattern) diff --git a/documentation/reference/cli/create.md b/documentation/reference/cli/create.md index 1ace4e09..c0dc890d 100644 --- a/documentation/reference/cli/create.md +++ b/documentation/reference/cli/create.md @@ -47,6 +47,73 @@ ork create cluster --name ork-e2e --- +### `ork create pattern` + +Scaffold a complete Orkestra pattern directory: `katalog.yaml`, `simulate.yaml`, `e2e.yaml`, and `README.md`. In typed mode, also writes `values.yaml`, `Makefile`, and `Dockerfile`. + +```bash +ork create pattern [flags] +``` + +#### Flags + +| Flag | Description | +|------|-------------| +| `--add-hook` | Typed mode: include a `hooks` section in `katalog.yaml` (also writes `values.yaml`, `Makefile`, `Dockerfile`) | +| `--add-constructor` | Typed mode: include a `constructor` section in `katalog.yaml` (also writes `values.yaml`, `Makefile`, `Dockerfile`) | +| `--typed` | Typed mode: include both sections commented (also writes `values.yaml`, `Makefile`, `Dockerfile`) | +| `-o, --output ` | Output directory (default: current directory) | + +#### Files written + +| File | Always | Typed mode only | +|------|--------|-----------------| +| `katalog.yaml` | Yes | — | +| `simulate.yaml` | Yes | — | +| `e2e.yaml` | Yes | — | +| `README.md` | Yes | — | +| `values.yaml` | — | Yes | +| `Makefile` | — | Yes | +| `Dockerfile` | — | Yes | + +#### Examples + +Scaffold a dynamic-mode pattern in the current directory: + +```bash +ork create pattern +``` + +Scaffold into a new directory: + +```bash +ork create pattern -o ./my-operator/ +``` + +Typed mode with hooks: + +```bash +ork create pattern --add-hook -o ./my-operator/ +``` + +Typed mode with both sections (choose one after generation): + +```bash +ork create pattern --typed +``` + +#### Behavior + +- `katalog.yaml` is generated by the same engine as `ork generate katalog` — the typed flags behave identically. +- `simulate.yaml` and `e2e.yaml` are minimal starters that reference `./katalog.yaml` and `./cr.yaml`. Edit assertions before running. +- `e2e.yaml` in typed mode includes `valuesFiles: [./values.yaml]` — the values file is how `ork e2e` passes the runtime image to the Orkestra Helm chart. +- `values.yaml` sets `runtime.image.repository` and `runtime.image.tag`. Update these after `make release` before running `ork e2e`. +- `README.md` contains numbered, actionable steps from edit to release. Typed mode adds the registry generation and build steps. +- `Makefile` targets: `registry`, `build`, `build-runtime`, `validate`, `simulate`, `e2e`, `docker`, `push`, `release`, `clean`. +- `Dockerfile` builds a production image using the runtime binary only (distroless base, no shell). + +--- + ## Notes - `ork create cluster` is intended for local development and CI environments, not production. diff --git a/pkg/generate/katalog_generator.go b/pkg/generate/katalog_generator.go index 6cad2088..ec330441 100644 --- a/pkg/generate/katalog_generator.go +++ b/pkg/generate/katalog_generator.go @@ -26,6 +26,7 @@ import ( "errors" "fmt" "os" + "path/filepath" "strings" "text/template" "time" @@ -186,6 +187,9 @@ func KatalogScaffold(opts KatalogScaffoldOptions) (string, error) { if dest == "" { dest = "katalog.yaml" } + if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { + return "", fmt.Errorf("creating directory for %s: %w", dest, err) + } if err := os.WriteFile(dest, []byte(out), 0o644); err != nil { return "", fmt.Errorf("writing %s: %w", dest, err) } @@ -197,6 +201,7 @@ func KatalogScaffold(opts KatalogScaffoldOptions) (string, error) { // Template syntax appearing inside YAML comments is escaped via {{ "{{" }}. var katalogTmpl = template.Must(template.New("katalog-scaffold").Parse( `# Generated by "ork generate katalog{{ .FlagSuffix }}" on {{ .Timestamp }} +# https://orkestra.sh/docs/reference/schema/katalog/ # Edit every TODO placeholder before running "ork run". apiVersion: orkestra.orkspace.io/v1 kind: Katalog @@ -278,24 +283,25 @@ spec: queue: maxDepth: 100 operatorBox: - default: {{ if .DefaultFalse }}false{{ else }}true{{ end }} + reconciler: + default: {{ if .DefaultFalse }}false{{ else }}true{{ end }} {{ if .ShowHooks }} - # hooks: - # # Package exporting the hook factory function. - # location: github.com/myorg/my-operator/hooks - # # Function signature: func() domain.AnyReconcileHooks - # # Return: domain.ReconcileHooks[*MyKind]{OnReconcile: ...} - # function: MyResourceHooks - # alias: myhook + # hooks: + # # Package exporting the hook factory function. + # location: github.com/myorg/my-operator/hooks + # # Function signature: func() domain.AnyReconcileHooks + # # Return: domain.ReconcileHooks[*MyKind]{OnReconcile: ...} + # function: MyResourceHooks + # alias: myhook {{ end -}} {{ if .ShowConstructor }} - # constructor: - # # Package exporting the reconciler constructor. - # location: github.com/myorg/my-operator/reconciler - # # Function signature: - # # func(*kubeclient.Kubeclient, cache.SharedIndexInformer, *event.Event) domain.Reconciler - # function: NewMyResourceReconciler - # alias: myrec + # constructor: + # # Package exporting the reconciler constructor. + # location: github.com/myorg/my-operator/reconciler + # # Function signature: + # # func(*kubeclient.Kubeclient, cache.SharedIndexInformer, *event.Event) domain.Reconciler + # function: NewMyResourceReconciler + # alias: myrec {{ end -}} {{ if not .IsTyped }} # Declarative template blocks — uncomment and fill in what you need. diff --git a/pkg/generate/pattern_generator.go b/pkg/generate/pattern_generator.go new file mode 100644 index 00000000..1bf7746a --- /dev/null +++ b/pkg/generate/pattern_generator.go @@ -0,0 +1,83 @@ +// pkg/generate/pattern_generator.go +package generate + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "text/template" +) + +// WriteSimulateScaffold writes a simulate.yaml starter to dest. +func WriteSimulateScaffold(dest string) error { + return writeStaticTemplate("templates/pattern_simulate.tmpl", dest) +} + +// WriteE2EScaffold writes an e2e.yaml starter to dest. +// When typed is true, a valuesFiles entry referencing values.yaml is included. +func WriteE2EScaffold(dest string, typed bool) error { + tmpl, err := template.ParseFS(templateFS, "templates/pattern_e2e.tmpl") + if err != nil { + return fmt.Errorf("parsing e2e template: %w", err) + } + var buf bytes.Buffer + if err := tmpl.Execute(&buf, patternReadmeData{Typed: typed}); err != nil { + return fmt.Errorf("rendering e2e: %w", err) + } + if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { + return fmt.Errorf("creating directory for %q: %w", dest, err) + } + return os.WriteFile(dest, buf.Bytes(), 0o644) +} + +// WriteValuesYAML writes a values.yaml with the runtime image placeholder to dest. +func WriteValuesYAML(dest string) error { + return writeStaticTemplate("templates/pattern_values.tmpl", dest) +} + +// WriteMakefile writes a clean Makefile (no example-pack workarounds) to dest. +func WriteMakefile(dest string) error { + return writeStaticTemplate("templates/pattern_makefile.tmpl", dest) +} + +// WriteDockerfile writes the production Dockerfile to dest. +func WriteDockerfile(dest string) error { + return writeStaticTemplate("templates/pattern_dockerfile.tmpl", dest) +} + +type patternReadmeData struct { + Typed bool +} + +// WriteREADME writes a README.md with actionable steps to dest. +// When typed is true, the steps include make registry, build, and release. +func WriteREADME(dest string, typed bool) error { + tmpl, err := template.ParseFS(templateFS, "templates/pattern_readme.tmpl") + if err != nil { + return fmt.Errorf("parsing readme template: %w", err) + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, patternReadmeData{Typed: typed}); err != nil { + return fmt.Errorf("rendering readme: %w", err) + } + + if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { + return fmt.Errorf("creating directory for %q: %w", dest, err) + } + return os.WriteFile(dest, buf.Bytes(), 0o644) +} + +// writeStaticTemplate reads a template file from the embedded FS and writes it +// to dest as-is — no template execution, raw bytes only. +func writeStaticTemplate(name, dest string) error { + content, err := templateFS.ReadFile(name) + if err != nil { + return fmt.Errorf("reading embedded %q: %w", name, err) + } + if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { + return fmt.Errorf("creating directory for %q: %w", dest, err) + } + return os.WriteFile(dest, content, 0o644) +} diff --git a/pkg/generate/templates/pattern_dockerfile.tmpl b/pkg/generate/templates/pattern_dockerfile.tmpl new file mode 100644 index 00000000..53083ae7 --- /dev/null +++ b/pkg/generate/templates/pattern_dockerfile.tmpl @@ -0,0 +1,4 @@ +FROM gcr.io/distroless/static-debian12:nonroot +COPY ork /usr/local/bin/ork +USER 65532:65532 +ENTRYPOINT ["/usr/local/bin/ork"] diff --git a/pkg/generate/templates/pattern_e2e.tmpl b/pkg/generate/templates/pattern_e2e.tmpl new file mode 100644 index 00000000..26ddb202 --- /dev/null +++ b/pkg/generate/templates/pattern_e2e.tmpl @@ -0,0 +1,25 @@ +# https://orkestra.sh/docs/reference/schema/e2e/ +apiVersion: orkestra.orkspace.io/v1 +kind: E2E +metadata: + name: # TODO: operator name + description: "End-to-end test for " +spec: + katalog: ./katalog.yaml + crd: ./crd.yaml + cr: ./cr.yaml +{{- if .Typed}} + valuesFiles: + - ./values.yaml +{{- end}} + # timeout: 120s + expect: + # TODO: add assertions + # - name: Deployment ready + # after: cr-applied + # timeout: 60s + # resources: + # - kind: Deployment + # name: "{{ "{{" }} .metadata.name {{ "}}" }}" + # namespace: "{{ "{{" }} .metadata.namespace {{ "}}" }}" + # ready: true diff --git a/pkg/generate/templates/pattern_makefile.tmpl b/pkg/generate/templates/pattern_makefile.tmpl new file mode 100644 index 00000000..20951d43 --- /dev/null +++ b/pkg/generate/templates/pattern_makefile.tmpl @@ -0,0 +1,105 @@ +# ── Typed Orkestra Operator ──────────────────────────────────────────────────── +BINARY_NAME ?= ork +DEV_OUTPUT_DIR ?= $(HOME)/.orkestra/bin +PROD_OUTPUT_DIR ?= $(HOME)/.orkestra/bin/runtime +KATALOG ?= katalog.yaml + +IMAGE_REPO ?= myorg/my-operator +IMAGE_TAG ?= latest +IMAGE ?= $(IMAGE_REPO):$(IMAGE_TAG) +BUILD_TAGS ?= + +GOOS ?= linux +GOARCH ?= amd64 +CGO_ENABLED = 0 + +ORK_LDFLAGS := -X github.com/orkspace/orkestra/pkg/version.Version=$(GIT_VERSION) \ + -X github.com/orkspace/orkestra/pkg/version.Commit=$(GIT_COMMIT) \ + -X github.com/orkspace/orkestra/pkg/version.Date=$(GIT_DATE) + +# ── registry ────────────────────────────────────────────────────────────────── +# Generates pkg/typeregistry/zz_generated_typeregistry.go. +# Re-run whenever you change apiTypes fields in katalog.yaml. +.PHONY: registry +registry: + ork generate registry --file $(KATALOG) + +# ── build (development) ─────────────────────────────────────────────────────── +# Builds the full CLI with all commands (validate, e2e, simulate, run, etc.) +.PHONY: build +build: + @mkdir -p $(DEV_OUTPUT_DIR) + go mod tidy + gofmt -w . + go build \ + -ldflags "$(ORK_LDFLAGS)" \ + -o $(DEV_OUTPUT_DIR)/$(BINARY_NAME) ./cmd/orkestra + @echo "✅ Development build: $(DEV_OUTPUT_DIR)/$(BINARY_NAME)" + +# ── build-runtime (production) ──────────────────────────────────────────────── +# Builds a production binary with ONLY the `run` command (no validate, e2e, etc.) +# This is what runs in your cluster — smaller attack surface, focused responsibility. +.PHONY: build-runtime +build-runtime: + @mkdir -p $(PROD_OUTPUT_DIR) + go mod tidy + gofmt -w . + GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build \ + -tags "runtime" \ + -ldflags "$(ORK_LDFLAGS)" \ + -o $(PROD_OUTPUT_DIR)/$(BINARY_NAME) ./cmd/orkestra + @echo "✅ Production runtime build: $(PROD_OUTPUT_DIR)/$(BINARY_NAME)" + @echo " This binary only supports 'ork run' — perfect for production." + +# ── validate ────────────────────────────────────────────────────────────────── +.PHONY: validate +validate: + $(DEV_OUTPUT_DIR)/$(BINARY_NAME) validate -f $(KATALOG) + +# ── simulate ────────────────────────────────────────────────────────────────── +.PHONY: simulate +simulate: + $(DEV_OUTPUT_DIR)/$(BINARY_NAME) simulate + +# ── e2e ─────────────────────────────────────────────────────────────────────── +.PHONY: e2e +e2e: + $(DEV_OUTPUT_DIR)/$(BINARY_NAME) e2e + +# ── docker / push / release ─────────────────────────────────────────────────── +# Copies the production runtime binary into the current directory for docker build, +# then removes it. The Docker image contains ONLY the production binary (runtime tag). +.PHONY: docker +docker: build-runtime + @cp $(PROD_OUTPUT_DIR)/$(BINARY_NAME) ./$(BINARY_NAME) + docker build -t $(IMAGE) . + @rm -f ./$(BINARY_NAME) + @echo "✅ Docker image built: $(IMAGE)" + +.PHONY: push +push: + docker push $(IMAGE) + +.PHONY: release +release: docker push + +# ── clean ───────────────────────────────────────────────────────────────────── +.PHONY: clean +clean: + @rm -f $(DEV_OUTPUT_DIR)/$(BINARY_NAME) + @rm -rf $(PROD_OUTPUT_DIR) + @echo "✅ Removed all local builds" + +# ── help ────────────────────────────────────────────────────────────────────── +.PHONY: help +help: + @echo " registry generate type registry from Katalog" + @echo " build compile full development CLI (all commands)" + @echo " build-runtime compile production binary (only 'ork run')" + @echo " validate run katalog validation (using development binary)" + @echo " e2e run end‑to‑end tests (using development binary)" + @echo " simulate run simulation tests (using development binary)" + @echo " docker build-runtime + copy into Docker image" + @echo " push push Docker image to registry" + @echo " release docker + push" + @echo " clean remove all local builds" diff --git a/pkg/generate/templates/pattern_readme.tmpl b/pkg/generate/templates/pattern_readme.tmpl new file mode 100644 index 00000000..2b71e3e9 --- /dev/null +++ b/pkg/generate/templates/pattern_readme.tmpl @@ -0,0 +1,100 @@ +# My Operator + +Generated by `ork create pattern`. + +## Step 1 — Edit katalog.yaml + +Fill in your CRD location and fields: + +- `spec.crds..crdFile` — path to your CRD file +- `spec.crds..crFiles` — path to your example CR +- `operatorBox.onCreate` — resources to create on each CR + +> **Before publishing or running in production:** replace `crdFile` and `crFiles` +> with `apiTypes` (group, version, kind, plural). `crdFile`/`crFiles` are +> development-only — the runtime reads them from disk; `apiTypes` references a +> CRD already installed in the cluster and is what belongs in a published Katalog. +{{if .Typed}} +## Step 2 — Generate type registry + +```bash +make registry +``` + +Re-run whenever you change `apiTypes` fields in `katalog.yaml`. + +## Step 3 — Write your hook or constructor + +Open the generated stub and implement the function body. The signature is generated — fill in the logic. + +## Step 4 — Release the image + +```bash +make release IMAGE=ghcr.io/myorg/my-operator:v0.1.0 +``` + +## Step 5 — Set the image in values.yaml + +Update `values.yaml` to match the image you just released: + +```yaml +runtime: + image: + repository: ghcr.io/myorg/my-operator + tag: v0.1.0 +``` + +`ork e2e` passes this file to the Orkestra Helm chart so it deploys your operator image. + +## Step 6 — Validate + +```bash +make validate +``` +{{else}} +## Step 2 — Validate + +```bash +ork validate +``` +{{end}} +## Step {{if .Typed}}7{{else}}3{{end}} — Simulate + +```bash +ork simulate +``` + +Runs without a cluster. Edit `simulate.yaml` assertions first, or generate them from a live run: + +```bash +ork simulate init +``` + +## Step {{if .Typed}}8{{else}}4{{end}} — Run locally + +```bash +ork run --dev +``` + +`--dev` spins up a local kind cluster. Skip it if you already have one. + +## Step {{if .Typed}}9{{else}}5{{end}} — E2E test + +```bash +ork e2e +``` + +## Step {{if .Typed}}10{{else}}6{{end}} — Observe + +```bash +ork control +``` + +Open http://localhost:8081. Login with username `orkestra`, password `orkestra`. +{{if .Typed}} +## Step 11 — Push + +```bash +ork push +``` +{{end}} diff --git a/pkg/generate/templates/pattern_simulate.tmpl b/pkg/generate/templates/pattern_simulate.tmpl new file mode 100644 index 00000000..71d7072c --- /dev/null +++ b/pkg/generate/templates/pattern_simulate.tmpl @@ -0,0 +1,16 @@ +# https://orkestra.sh/docs/reference/schema/simulate/ +apiVersion: orkestra.orkspace.io/v1 +kind: Simulate +metadata: + name: # TODO: operator name +spec: + katalog: ./katalog.yaml + cr: ./cr.yaml + expect: + # TODO: add assertions — or generate them from a live run: + # ork simulate init + # + # - cycle: 1 + # verb: create + # resource: deployments + # name: "{{ .metadata.name }}" diff --git a/pkg/generate/templates/pattern_values.tmpl b/pkg/generate/templates/pattern_values.tmpl new file mode 100644 index 00000000..190ae7a0 --- /dev/null +++ b/pkg/generate/templates/pattern_values.tmpl @@ -0,0 +1,4 @@ +runtime: + image: + repository: ghcr.io/myorg/my-operator + tag: latest diff --git a/pkg/runners/configmaps.go b/pkg/runners/configmaps.go index b01cc828..bf299f3e 100644 --- a/pkg/runners/configmaps.go +++ b/pkg/runners/configmaps.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_configmaps.go +// pkg/runners/configmaps.go package runners import ( diff --git a/pkg/runners/cronjobs.go b/pkg/runners/cronjobs.go index 459232ec..e65f2409 100644 --- a/pkg/runners/cronjobs.go +++ b/pkg/runners/cronjobs.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_cronjobs.go +// pkg/runners/cronjobs.go package runners import ( diff --git a/pkg/runners/deployments.go b/pkg/runners/deployments.go index 0a025e16..23f12118 100644 --- a/pkg/runners/deployments.go +++ b/pkg/runners/deployments.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_deployments.go +// pkg/runners/deployments.go package runners import ( diff --git a/pkg/runners/hpas.go b/pkg/runners/hpas.go index 0d1adc53..8f3b327d 100644 --- a/pkg/runners/hpas.go +++ b/pkg/runners/hpas.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_hpas.go +// pkg/runners/hpas.go package runners import ( diff --git a/pkg/runners/ingresses.go b/pkg/runners/ingresses.go index e10f777b..44ce28e9 100644 --- a/pkg/runners/ingresses.go +++ b/pkg/runners/ingresses.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_ingresses.go +// pkg/runners/ingresses.go package runners import ( diff --git a/pkg/runners/jobs.go b/pkg/runners/jobs.go index a23a2b51..3108946f 100644 --- a/pkg/runners/jobs.go +++ b/pkg/runners/jobs.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_jobs.go +// pkg/runners/jobs.go package runners import ( diff --git a/pkg/runners/namespaces.go b/pkg/runners/namespaces.go index dd5e509c..2e11947c 100644 --- a/pkg/runners/namespaces.go +++ b/pkg/runners/namespaces.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_namespace.go +// pkg/runners/namespaces.go package runners import ( diff --git a/pkg/runners/pdbs.go b/pkg/runners/pdbs.go index 68bdb64d..8f2350f2 100644 --- a/pkg/runners/pdbs.go +++ b/pkg/runners/pdbs.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_pdbs.go +// pkg/runners/pdbs.go package runners import ( diff --git a/pkg/runners/pods.go b/pkg/runners/pods.go index 9ba65ff6..3ab7163f 100644 --- a/pkg/runners/pods.go +++ b/pkg/runners/pods.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_pods.go +// pkg/runners/pods.go package runners import ( diff --git a/pkg/runners/pvcs.go b/pkg/runners/pvcs.go index 3920d95d..a53e8817 100644 --- a/pkg/runners/pvcs.go +++ b/pkg/runners/pvcs.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_pvcs.go +// pkg/runners/pvcs.go package runners import ( diff --git a/pkg/runners/pvs.go b/pkg/runners/pvs.go index 7ec759f6..a7b025c6 100644 --- a/pkg/runners/pvs.go +++ b/pkg/runners/pvs.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_pvs.go +// pkg/runners/pvs.go package runners import ( diff --git a/pkg/runners/replicasets.go b/pkg/runners/replicasets.go index cd2c1eb0..6f233308 100644 --- a/pkg/runners/replicasets.go +++ b/pkg/runners/replicasets.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_replicasets.go +// pkg/runners/replicasets.go package runners import ( diff --git a/pkg/runners/rolebindings.go b/pkg/runners/rolebindings.go index 124efd52..29c9e5b9 100644 --- a/pkg/runners/rolebindings.go +++ b/pkg/runners/rolebindings.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_rolebindings.go +// pkg/runners/rolebindings.go package runners import ( diff --git a/pkg/runners/roles.go b/pkg/runners/roles.go index e3a911cf..a8806703 100644 --- a/pkg/runners/roles.go +++ b/pkg/runners/roles.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_roles.go +// pkg/runners/roles.go package runners import ( diff --git a/pkg/runners/secret_tls.go b/pkg/runners/secret_tls.go index ed3329e4..4a6b1d68 100644 --- a/pkg/runners/secret_tls.go +++ b/pkg/runners/secret_tls.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_secrets_tls.go +// pkg/runners/secret_tls.go // // TLS certificate generation and secret rotation for Orkestra secrets. // diff --git a/pkg/runners/secrets.go b/pkg/runners/secrets.go index 1d294012..1c19db86 100644 --- a/pkg/runners/secrets.go +++ b/pkg/runners/secrets.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_secrets.go +// pkg/runners/secrets.go // // Adds to the previous version: // - orktypes.EvaluateWhen instead of evaluateConditions (fixes anyOf: being ignored) diff --git a/pkg/runners/secrets_once.go b/pkg/runners/secrets_once.go index a6ed8a3e..4dd41126 100644 --- a/pkg/runners/secrets_once.go +++ b/pkg/runners/secrets_once.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_secrets_once.go +// pkg/runners/secrets_once.go // // once: true on secrets — idempotent random secret generation. // diff --git a/pkg/runners/serviceaccounts.go b/pkg/runners/serviceaccounts.go index e334fe2b..b8b35e7e 100644 --- a/pkg/runners/serviceaccounts.go +++ b/pkg/runners/serviceaccounts.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_serviceaccounts.go +// pkg/runners/serviceaccounts.go package runners import ( diff --git a/pkg/runners/services.go b/pkg/runners/services.go index 882c9649..628bcdf1 100644 --- a/pkg/runners/services.go +++ b/pkg/runners/services.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_services.go +// pkg/runners/services.go package runners import ( diff --git a/pkg/runners/statefulsets.go b/pkg/runners/statefulsets.go index 7494f219..047db991 100644 --- a/pkg/runners/statefulsets.go +++ b/pkg/runners/statefulsets.go @@ -1,4 +1,4 @@ -// pkg/reconciler/run_statefulsets.go +// pkg/runners/statefulsets.go package runners import (