From 6146aa5a13ce7ccd0d0fb47c9745bf6226555204 Mon Sep 17 00:00:00 2001 From: ialexeze Date: Sun, 16 Aug 2026 21:17:34 +0000 Subject: [PATCH 1/4] feat(target-operatorbox): generator, validation, and load-time wiring for per-target dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TargetHookRegistry / TargetReconcilerRegistry: new GVK→target maps in pkg/types - TargetHookFactories / TargetReconcilerFactories: runtime fields on CRDEntry - HasServeTargetEntries(): accessor replacing direct nil checks - EffectiveOperatorBox: deep merge via mergeReconcilerConfig (args key-by-key) - addTargetHooks / addTargetConstructors: new validate pipeline steps (6b, 8b) - generator: per-target loop emits TargetHookRegistry / TargetReconcilerRegistry entries - validate_hooks_reconcilers.go: extracted from validation_methods, fixed save-back bug, removed IsTargetOnly gate - 20 tests covering addHooks, addReconcilers, addTargetHooks, addTargetConstructors - fixture 03-hooks-targets: per-target hooks fixture with intent/ pattern --- CHANGELOG.md | 10 +- .../reference/schema/02-katalog/20-serve.md | 2 +- .../02-katalog/26-serve-target-operatorbox.md | 43 +- pkg/katalog/validate.go | 15 + pkg/katalog/validate_hooks_reconcilers.go | 216 +++++ .../validate_hooks_reconcilers_test.go | 366 +++++++++ pkg/katalog/validation_methods.go | 54 -- pkg/kubeclient/fixture/01-hooks/go.mod | 2 +- pkg/kubeclient/fixture/02-constructor/go.mod | 2 +- .../fixture/03-hooks-targets/Dockerfile | 4 + .../fixture/03-hooks-targets/Makefile | 97 +++ .../fixture/03-hooks-targets/README.md | 116 +++ .../blockchainappwithtargets_types.go | 76 ++ .../03-hooks-targets/api/v1alpha1/register.go | 22 + .../fixture/03-hooks-targets/cleanup.sh | 6 + .../03-hooks-targets/cmd/orkestra/main.go | 25 + .../fixture/03-hooks-targets/cr-e2e.yaml | 11 + .../fixture/03-hooks-targets/cr.yaml | 11 + .../fixture/03-hooks-targets/crd.yaml | 63 ++ .../fixture/03-hooks-targets/e2e.yaml | 60 ++ .../fixture/03-hooks-targets/go.mod | 234 ++++++ .../fixture/03-hooks-targets/go.sum | 746 ++++++++++++++++++ .../hooks/blockchainappwithtargets_hooks.go | 58 ++ .../intent/intent-v2-disabled.yaml | 6 + .../intent/intent-v2-enabled.yaml | 6 + .../fixture/03-hooks-targets/katalog.yaml | 115 +++ .../typeregistry/zz_generated_typeregistry.go | 85 ++ .../simulate-v2-disabled.yaml | 26 + .../03-hooks-targets/simulate-v2-enabled.yaml | 22 + .../fixture/03-hooks-targets/values.yaml | 4 + pkg/kubeclient/fixture/README.md | 5 +- pkg/kubeclient/fixture/e2e.yaml | 1 + pkg/kubeclient/fixture/go.mod | 2 +- pkg/kubeclient/fixture/komposer.yaml | 1 + pkg/kubeclient/fixture/simulate.yaml | 2 + pkg/labels/labels.go | 6 +- .../clusterrolebindings/clusterrolebinding.go | 2 +- pkg/resources/clusterroles/clusterrole.go | 2 +- pkg/resources/configmaps/configmap.go | 2 +- pkg/resources/cronjobs/cronjob.go | 2 +- pkg/resources/customresources/custom.go | 2 +- pkg/resources/deployments/deployment.go | 2 +- pkg/resources/fixture/go.mod | 2 +- pkg/resources/hpas/hpa.go | 2 +- pkg/resources/ingresses/ingress.go | 2 +- pkg/resources/jobs/job.go | 2 +- pkg/resources/limitranges/limitrange.go | 2 +- pkg/resources/namespaces/namespace.go | 2 +- .../networkpolicies/networkpolicy.go | 2 +- pkg/resources/pdbs/pdb.go | 2 +- pkg/resources/pods/pod.go | 2 +- pkg/resources/pvcs/pvc.go | 2 +- pkg/resources/pvs/pv.go | 2 +- pkg/resources/replicasets/replicaset.go | 2 +- pkg/resources/resourcequotas/resourcequota.go | 2 +- pkg/resources/rolebindings/rolebinding.go | 2 +- pkg/resources/roles/role.go | 2 +- pkg/resources/secrets/secret.go | 2 +- .../serviceaccounts/serviceaccount.go | 2 +- pkg/resources/services/services.go | 2 +- pkg/resources/statefulsets/statefulset.go | 2 +- pkg/tools/generate/registry_generator.go | 95 ++- pkg/tools/generate/registry_template.go | 38 + pkg/tools/generate/type.go | 27 +- pkg/types/types.go | 12 + pkg/types/types_crd_entry.go | 79 +- pkg/types/types_operatorbox.go | 1 + 67 files changed, 2698 insertions(+), 124 deletions(-) create mode 100644 pkg/katalog/validate_hooks_reconcilers.go create mode 100644 pkg/katalog/validate_hooks_reconcilers_test.go create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/Dockerfile create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/Makefile create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/README.md create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/api/v1alpha1/blockchainappwithtargets_types.go create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/api/v1alpha1/register.go create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/cleanup.sh create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/cmd/orkestra/main.go create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/cr-e2e.yaml create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/cr.yaml create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/crd.yaml create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/e2e.yaml create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/go.mod create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/go.sum create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/hooks/blockchainappwithtargets_hooks.go create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-disabled.yaml create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-enabled.yaml create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/katalog.yaml create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/pkg/typeregistry/zz_generated_typeregistry.go create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/simulate-v2-disabled.yaml create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/simulate-v2-enabled.yaml create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/values.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 51600b110..a16ebe397 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## v0.7.15 — Gateway Webhook Intake + Artifact Signing [UNRELEASED] +## v0.7.15 — Gateway Webhook Intake + Artifact Signing ### Artifact signing — Cosign keyless, `publish:` block, local testing @@ -229,7 +229,9 @@ Both gates use the full resolver chain (`.spec`, `.metadata`, serve intent, prof ### Per-target `operatorBox` — surface-specific reconciliation -`serve.target..operatorBox` overrides the CRD-level `operatorBox` for CRs routed through that surface. The gateway stamps `orkestra.orkspace.io/serve-target` on every applied CR; the runtime reads that annotation at reconcile time and uses the matching target's templates instead of the shared CRD-level ones. CRs applied via `kubectl apply` (no annotation) fall back to the CRD-level `operatorBox`. +`serve.target..operatorBox` overrides the CRD-level `operatorBox` for CRs routed through that surface. The gateway stamps `orkestra.orkspace.io/serve-target` on every applied CR; the runtime reads that annotation at reconcile time and uses the matching target's templates, hooks, and gates instead of the shared CRD-level ones. CRs applied via `kubectl apply` (no annotation) fall back to the CRD-level `operatorBox`. + +This applies equally to declarative and typed (hooks/constructor) operators. A per-target `reconciler.hooks.args` block means the same binary receives different resolved values depending on which surface delivered the intent — no code change, no separate operator: ```yaml operatorBox: @@ -255,9 +257,9 @@ serve: - name: "{{ .metadata.name }}-apifixture" ``` -`preReconcile` and `status` follow the same pattern — a target may declare its own gate conditions or status fields, with the CRD-level config as the fallback when absent. Reconciler-level settings (`reconciler.workers`, `reconciler.resync`, `reconciler.queue`, `reconciler.profile`, `autoscale`, `rollback`) are rejected by `ork validate` on target entries — the worker pool is fixed at CRD level. +`preReconcile`, `status`, and `reconciler.hooks.args` follow the same pattern — a target may declare its own values, with the CRD-level config as the fallback when absent. Reconciler settings (`workers`, `resync`, `autoscale`) are fixed at CRD level. -Cleanup on target change is handled automatically: `DeleteIfOwned` removes resources declared by the previous target's operatorBox that are no longer present in the new one. +Cleanup on target switch is handled automatically via a label-selector sweep on `orkestra-owner=.` — immune to spec fields being cleared before cleanup runs. `keepPreviousSurface: true` skips the sweep when both surfaces should run simultaneously. **`ork simulate --target `** — simulates a specific target's operatorBox. Also declarable in `simulate.yaml` via `spec.target:`. CLI flag takes precedence over the spec field. diff --git a/documentation/reference/schema/02-katalog/20-serve.md b/documentation/reference/schema/02-katalog/20-serve.md index 08d511564..1014fa4b6 100644 --- a/documentation/reference/schema/02-katalog/20-serve.md +++ b/documentation/reference/schema/02-katalog/20-serve.md @@ -775,7 +775,7 @@ spec: **Cleanup on target change** — when a CR moves between targets (e.g. re-submitted via a different surface), the previous target's resources are cleaned up automatically via a label-selector sweep on `orkestra-owner=.`. No manual cleanup is needed. To retain old-target resources deliberately, set `keepPreviousSurface: true` in `target..apply.overrides`. -**What stays fixed at the CRD level** — worker counts, resync intervals, and autoscale config are always taken from the CRD-level `operatorBox`. Only templates (`onReconcile`, `onCreate`, `onDelete`), status, finalizers, and external/cross blocks are resolved per-target. +**What stays fixed at the CRD level** — reconciler settings (`workers`, `resync`, `autoscale`) are always taken from the CRD-level `operatorBox`. Everything else — templates, gates, status, hooks, and `hooks.args` — falls back to the CRD-level value when absent on the target, and can be overridden when present. → [Full per-target operatorBox reference](26-serve-target-operatorbox.md) — preReconcile gates, surface switch cleanup, `keepPreviousSurface`, simulate patterns diff --git a/documentation/reference/schema/02-katalog/26-serve-target-operatorbox.md b/documentation/reference/schema/02-katalog/26-serve-target-operatorbox.md index e7e69bd18..afc7f25ff 100644 --- a/documentation/reference/schema/02-katalog/26-serve-target-operatorbox.md +++ b/documentation/reference/schema/02-katalog/26-serve-target-operatorbox.md @@ -11,31 +11,37 @@ This makes the target the unit of runtime execution: the same CRD can behave dif ```yaml spec: crds: - website: + app: operatorBox: # CRD-level fallback — used by kubectl apply / unknown targets - onCreate: - deployments: - - name: "{{ .metadata.name }}" + reconciler: + hooks: + location: github.com/myorg/myoperator/hooks + function: AppHooks + args: + featureEnabled: '{{ .external.flags.body }}' + inBusinessHours: '{{ inBusinessHours }}' serve: enabled: true target: - web: + v2-enabled: primary: true - operatorBox: # used when CR arrives via the "web" target + operatorBox: # hooks — args forced, gate active preReconcile: enqueueGate: when: - - field: "{{ .spec.image }}" - notEquals: "" - onCreate: - deployments: - - name: "{{ .metadata.name }}-web" - image: "{{ .spec.image }}" - replicas: "{{ .spec.replicas }}" + - field: '{{ inBusinessHours }}' + equals: "true" + reconciler: + hooks: + location: github.com/myorg/myoperator/hooks + function: AppHooks + args: + featureEnabled: "true" + inBusinessHours: '{{ inBusinessHours }}' regional: - operatorBox: # used when CR arrives via the "regional" target + operatorBox: # declarative — forEach over regions preReconcile: reconcileGate: when: @@ -150,14 +156,7 @@ CRD-level wins if set; per-target applies otherwise. ## What stays fixed at the CRD level -Per-target `operatorBox` overrides lifecycle templates and gates. These fields are always taken from the CRD-level `operatorBox`: - -- Worker counts, resync intervals, and autoscale config -- `finalizers` -- `rollBackOnError` -- `reconciler.constructor` and `reconciler.hooks` (custom reconciler wiring) - -Only `onCreate`, `onReconcile`, `onDelete`, `preReconcile`, `status`, and `external`/`cross` blocks are resolved per-target. +Reconciler settings — `workers`, `resync`, and `autoscale` — are always taken from the CRD-level `operatorBox`. Everything else (`onCreate`, `onReconcile`, `onDelete`, `preReconcile`, `status`, `reconciler.hooks`, `reconciler.hooks.args`) can be overridden per-target. When a target's `operatorBox` omits a block, it falls back to the CRD-level value. --- diff --git a/pkg/katalog/validate.go b/pkg/katalog/validate.go index 623a3140b..16e46c031 100644 --- a/pkg/katalog/validate.go +++ b/pkg/katalog/validate.go @@ -58,6 +58,14 @@ func (k *Katalog) ValidateConfig(kfg *konfig.Konfig) (*Katalog, error) { if err := k.addReconcilers(); err != nil { return nil, err } + + // ------------------------------------------------------------------------- + // 6b. Add Target Constructors // TargetReconcilerRegistry → TargetReconcilerFactories + // ------------------------------------------------------------------------- + if err := k.addTargetConstructors(); err != nil { + return nil, err + } + // ------------------------------------------------------------------------- // 7. Add RuntimeObjects // ObjectRegistry + ListRegistry // ------------------------------------------------------------------------- @@ -72,6 +80,13 @@ func (k *Katalog) ValidateConfig(kfg *konfig.Konfig) (*Katalog, error) { return nil, err } + // ------------------------------------------------------------------------- + // 8b. Add Target Hooks // TargetHookRegistry → TargetHookFactories + // ------------------------------------------------------------------------- + if err := k.addTargetHooks(); err != nil { + return nil, err + } + // ------------------------------------------------------------------------- // 9. Validate Status // ------------------------------------------------------------------------- diff --git a/pkg/katalog/validate_hooks_reconcilers.go b/pkg/katalog/validate_hooks_reconcilers.go new file mode 100644 index 000000000..ea0723bd2 --- /dev/null +++ b/pkg/katalog/validate_hooks_reconcilers.go @@ -0,0 +1,216 @@ +package katalog + +import ( + "fmt" + + "github.com/orkspace/orkestra/domain" + orktypes "github.com/orkspace/orkestra/pkg/types" +) + +// --------------------------------------------------------------------------------- +// Add reconcilers +func (k *Katalog) addReconcilers() error { + for name, crd := range k.enabledCRDs { + rc := crd.OperatorBox + + // Add providers block + if len(rc.ProviderBlocks) > 0 { + blocks, err := orktypes.ParseProviderBlocks(rc.RawProviders) + if err != nil { + return err + } + rc.ProviderBlocks = blocks + } + + if !crd.IsDynamic() { + if crd.DefaultReconcile() { + // Per-target operatorBoxes can declare reconciler.default: false with a + // constructor — apply the same registration check as the CRD-level path. + if crd.Serve != nil && crd.Serve.Target.Entries != nil { + for targetName, targetCfg := range crd.Serve.Target.Entries { + if targetCfg.OperatorBox == nil || targetCfg.OperatorBox.Reconciler == nil { + continue + } + rec := targetCfg.OperatorBox.Reconciler + if rec.Default != nil && !*rec.Default { + constructorFn, ok := orktypes.ReconcilerRegistry[crd.GroupVersionKind] + if !ok { + return fmt.Errorf( + "CRD %q target %q: reconciler.default: false but no constructor registered — "+ + "check reconciler.constructor in Katalog and re-run ork generate registry", + name, targetName, + ) + } + targetCfg.OperatorBox.Constructor = constructorFn + crd.Serve.Target.Entries[targetName] = targetCfg + } + } + } + crd.OperatorBox = rc + k.enabledCRDs[name] = crd + continue + } + + constructorFn, ok := orktypes.ReconcilerRegistry[crd.GroupVersionKind] + if !ok { + return fmt.Errorf( + "CRD %q: no constructor registered — "+ + "check reconciler.constructor in Katalog and re-run ork generate registry", + name, + ) + } + + rc.Constructor = constructorFn + } + + crd.OperatorBox = rc + k.enabledCRDs[name] = crd + } + return nil +} + +// --------------------------------------------------------------------------------- +// Add hooks +func (k *Katalog) addHooks() error { + for name, crd := range k.enabledCRDs { + if !crd.DefaultReconcile() { + continue + } + hookFn, ok := orktypes.HookRegistry[crd.GroupVersionKind] + if ok { + crd.OperatorBox.HookFactory = hookFn + } + + if !crd.HasServeTargetEntries() { + k.enabledCRDs[name] = crd + continue + } + + // Per-target hook validation: a target that declares hooks at the same + // location as the CRD-level binary (or just overrides args) relies on the + // CRD-level factory — it must be registered. Targets with a distinct binary + // are validated separately by addTargetHooks. + crdLevelHookLoc := "" + if crd.OperatorBox.Reconciler != nil && crd.OperatorBox.Reconciler.Hooks != nil { + crdLevelHookLoc = crd.OperatorBox.Reconciler.Hooks.Location + } + for targetName, targetCfg := range crd.Serve.Target.Entries { + if targetCfg.OperatorBox == nil || targetCfg.OperatorBox.Reconciler == nil { + continue + } + h := targetCfg.OperatorBox.Reconciler.Hooks + if h == nil { + continue + } + // Distinct binary → addTargetHooks validates; skip here. + if h.Location != "" && h.Location != crdLevelHookLoc { + continue + } + // Same binary (or args-only override) — CRD-level factory must be registered. + if (h.Location != "" || h.Function != "") && !ok { + return fmt.Errorf( + "CRD %q target %q: reconciler.hooks declared but no hook factory registered for GVK %s — "+ + "re-run ork generate registry", + name, targetName, crd.GroupVersionKind, + ) + } + } + + k.enabledCRDs[name] = crd + } + return nil +} + +// --------------------------------------------------------------------------------- +// addTargetHooks wires per-target hook factories from TargetHookRegistry onto +// CRDEntry.TargetHookFactories. Only targets that declare a distinct hook binary +// (different location from the CRD-level hooks) need an entry here — targets that +// share the CRD-level binary and only override args are handled at reconcile time +// by mergeReconcilerConfig inside EffectiveOperatorBox. +func (k *Katalog) addTargetHooks() error { + for name, crd := range k.enabledCRDs { + if !crd.HasServeTargetEntries() { + continue + } + crdLevelLocation := "" + if crd.OperatorBox.Reconciler != nil && crd.OperatorBox.Reconciler.Hooks != nil { + crdLevelLocation = crd.OperatorBox.Reconciler.Hooks.Location + } + gvk := crd.GroupVersionKind + for targetName, targetCfg := range crd.Serve.Target.Entries { + if targetCfg.OperatorBox == nil || targetCfg.OperatorBox.Reconciler == nil { + continue + } + h := targetCfg.OperatorBox.Reconciler.Hooks + if h == nil || h.Location == "" || h.Location == crdLevelLocation { + continue + } + targetMap, ok := orktypes.TargetHookRegistry[gvk] + if !ok { + return fmt.Errorf( + "CRD %q target %q: per-target hooks (location %q) declared but "+ + "no TargetHookRegistry entry for this GVK — re-run ork generate registry", + name, targetName, h.Location, + ) + } + fn, ok := targetMap[targetName] + if !ok { + return fmt.Errorf( + "CRD %q target %q: no TargetHookRegistry entry for this target — "+ + "re-run ork generate registry", + name, targetName, + ) + } + if crd.TargetHookFactories == nil { + crd.TargetHookFactories = make(map[string]func() domain.AnyReconcileHooks) + } + crd.TargetHookFactories[targetName] = fn + } + k.enabledCRDs[name] = crd + } + return nil +} + +// --------------------------------------------------------------------------------- +// addTargetConstructors wires per-target constructor factories from +// TargetReconcilerRegistry onto CRDEntry.TargetReconcilerFactories. +func (k *Katalog) addTargetConstructors() error { + for name, crd := range k.enabledCRDs { + if !crd.HasServeTargetEntries() { + continue + } + gvk := crd.GroupVersionKind + for targetName, targetCfg := range crd.Serve.Target.Entries { + if targetCfg.OperatorBox == nil || targetCfg.OperatorBox.Reconciler == nil { + continue + } + rec := targetCfg.OperatorBox.Reconciler + if rec.Default == nil || *rec.Default || rec.ConstructorDecl == nil { + continue + } + targetMap, ok := orktypes.TargetReconcilerRegistry[gvk] + if !ok { + return fmt.Errorf( + "CRD %q target %q: reconciler.default: false declared but "+ + "no TargetReconcilerRegistry entry for this GVK — re-run ork generate registry", + name, targetName, + ) + } + fn, ok := targetMap[targetName] + if !ok { + return fmt.Errorf( + "CRD %q target %q: no TargetReconcilerRegistry entry for this target — "+ + "re-run ork generate registry", + name, targetName, + ) + } + if crd.TargetReconcilerFactories == nil { + crd.TargetReconcilerFactories = make(map[string]orktypes.NewReconcilerFunc) + } + crd.TargetReconcilerFactories[targetName] = fn + } + k.enabledCRDs[name] = crd + } + return nil +} + diff --git a/pkg/katalog/validate_hooks_reconcilers_test.go b/pkg/katalog/validate_hooks_reconcilers_test.go new file mode 100644 index 000000000..c59468fbb --- /dev/null +++ b/pkg/katalog/validate_hooks_reconcilers_test.go @@ -0,0 +1,366 @@ +package katalog + +import ( + "testing" + + "github.com/orkspace/orkestra/domain" + "github.com/orkspace/orkestra/pkg/event" + "github.com/orkspace/orkestra/pkg/kubeclient" + orktypes "github.com/orkspace/orkestra/pkg/types" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/tools/cache" +) + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +var testGVK = schema.GroupVersionKind{Group: "test.io", Version: "v1", Kind: "MyApp"} + +func stubHookFn() func() domain.AnyReconcileHooks { + return func() domain.AnyReconcileHooks { return nil } +} + +func stubRecFn() orktypes.NewReconcilerFunc { + return func(kubeclient.Interface, cache.SharedIndexInformer, event.Recorder) domain.Reconciler { + return nil + } +} + +// crdWithGVK builds a typed (non-dynamic) CRDEntry for tests. +// Setting APITypes.Location makes IsDynamic() return false so the constructor +// and per-target reconciler paths inside addReconcilers() are exercised. +func crdWithGVK(gvk schema.GroupVersionKind) orktypes.CRDEntry { + return orktypes.CRDEntry{ + GroupVersionKind: gvk, + APITypes: orktypes.APITypes{ + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + Location: "github.com/test/apis/v1", + }, + OperatorBox: orktypes.OperatorBoxConfig{ + Reconciler: &orktypes.ReconcilerConfig{}, + }, + } +} + +func withTargetHookLocation(crd orktypes.CRDEntry, targetName, location string) orktypes.CRDEntry { + if crd.Serve == nil { + crd.Serve = &orktypes.ServeConfig{Enabled: true} + } + if crd.Serve.Target.Entries == nil { + crd.Serve.Target.Entries = map[string]*orktypes.ServeTargetConfig{} + } + crd.Serve.Target.Entries[targetName] = &orktypes.ServeTargetConfig{ + OperatorBox: &orktypes.OperatorBoxConfig{ + Reconciler: &orktypes.ReconcilerConfig{ + Hooks: &orktypes.HookDeclaration{Location: location, Function: "New"}, + }, + }, + } + return crd +} + +func withTargetDefaultFalse(crd orktypes.CRDEntry, targetName string) orktypes.CRDEntry { + if crd.Serve == nil { + crd.Serve = &orktypes.ServeConfig{Enabled: true} + } + if crd.Serve.Target.Entries == nil { + crd.Serve.Target.Entries = map[string]*orktypes.ServeTargetConfig{} + } + crd.Serve.Target.Entries[targetName] = &orktypes.ServeTargetConfig{ + OperatorBox: &orktypes.OperatorBoxConfig{ + Reconciler: &orktypes.ReconcilerConfig{ + Default: boolPtr(false), + ConstructorDecl: &orktypes.ConstructorDeclaration{Location: "github.com/test/rec", Function: "New"}, + }, + }, + } + return crd +} + +// ── addHooks ───────────────────────────────────────────────────────────────── + +func TestAddHooks_WiresFactoryWhenRegistered(t *testing.T) { + fn := stubHookFn() + orktypes.HookRegistry[testGVK] = fn + t.Cleanup(func() { delete(orktypes.HookRegistry, testGVK) }) + + crd := crdWithGVK(testGVK) + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addHooks(); err != nil { + t.Fatalf("addHooks returned error: %v", err) + } + got := k.enabledCRDs["myapp"] + if got.OperatorBox.HookFactory == nil { + t.Error("expected HookFactory to be set, got nil") + } +} + +func TestAddHooks_NoEntryIsOK(t *testing.T) { + delete(orktypes.HookRegistry, testGVK) + + crd := crdWithGVK(testGVK) + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addHooks(); err != nil { + t.Fatalf("addHooks returned unexpected error: %v", err) + } + if k.enabledCRDs["myapp"].OperatorBox.HookFactory != nil { + t.Error("HookFactory should be nil when no registry entry exists") + } +} + +func TestAddHooks_ErrorWhenTargetSharesBinaryButNotRegistered(t *testing.T) { + // Target shares the CRD-level hook binary (same location) but no factory is + // registered. addHooks should error — the shared binary is missing. + delete(orktypes.HookRegistry, testGVK) + + crd := crdWithGVK(testGVK) + // Set the CRD-level hook location. + crd.OperatorBox.Reconciler.Hooks = &orktypes.HookDeclaration{Location: "github.com/test/hooks", Function: "New"} + // Target declares the same location — sharing the binary. + crd = withTargetHookLocation(crd, "v2", "github.com/test/hooks") + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addHooks(); err == nil { + t.Fatal("expected error when target shares binary but factory not registered, got nil") + } +} + +func TestAddHooks_NoErrorWhenTargetHasDistinctBinary(t *testing.T) { + // Target has a different location → addTargetHooks handles it; addHooks should not error. + delete(orktypes.HookRegistry, testGVK) + + crd := withTargetHookLocation(crdWithGVK(testGVK), "v2", "github.com/test/v2hooks") + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addHooks(); err != nil { + t.Fatalf("addHooks should not error for distinct-binary target (addTargetHooks validates): %v", err) + } +} + +func TestAddHooks_SkipsNonDefaultReconcilers(t *testing.T) { + crd := crdWithGVK(testGVK) + crd.OperatorBox.Reconciler.Default = boolPtr(false) + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addHooks(); err != nil { + t.Fatalf("addHooks returned error: %v", err) + } +} + +// ── addReconcilers ──────────────────────────────────────────────────────────── + +func TestAddReconcilers_DefaultReconcileSkipsConstructor(t *testing.T) { + crd := crdWithGVK(testGVK) + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addReconcilers(); err != nil { + t.Fatalf("addReconcilers returned error: %v", err) + } + if k.enabledCRDs["myapp"].OperatorBox.Constructor != nil { + t.Error("Constructor should not be set for default reconciler") + } +} + +func TestAddReconcilers_WiresConstructorWhenRegistered(t *testing.T) { + fn := stubRecFn() + orktypes.ReconcilerRegistry[testGVK] = fn + t.Cleanup(func() { delete(orktypes.ReconcilerRegistry, testGVK) }) + + crd := crdWithGVK(testGVK) + crd.OperatorBox.Reconciler.Default = boolPtr(false) + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addReconcilers(); err != nil { + t.Fatalf("addReconcilers returned error: %v", err) + } + if k.enabledCRDs["myapp"].OperatorBox.Constructor == nil { + t.Error("expected Constructor to be set, got nil") + } +} + +func TestAddReconcilers_ErrorWhenDefaultFalseAndNotRegistered(t *testing.T) { + delete(orktypes.ReconcilerRegistry, testGVK) + + crd := crdWithGVK(testGVK) + crd.OperatorBox.Reconciler.Default = boolPtr(false) + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addReconcilers(); err == nil { + t.Fatal("expected error for missing constructor registration, got nil") + } +} + +func TestAddReconcilers_PerTargetDefaultFalseWiresConstructor(t *testing.T) { + fn := stubRecFn() + orktypes.ReconcilerRegistry[testGVK] = fn + t.Cleanup(func() { delete(orktypes.ReconcilerRegistry, testGVK) }) + + crd := withTargetDefaultFalse(crdWithGVK(testGVK), "v2") + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addReconcilers(); err != nil { + t.Fatalf("addReconcilers returned error: %v", err) + } + entry := k.enabledCRDs["myapp"].Serve.Target.Entries["v2"] + if entry.OperatorBox.Constructor == nil { + t.Error("expected Constructor to be set on per-target config, got nil") + } +} + +func TestAddReconcilers_PerTargetDefaultFalseErrorWhenMissing(t *testing.T) { + delete(orktypes.ReconcilerRegistry, testGVK) + + crd := withTargetDefaultFalse(crdWithGVK(testGVK), "v2") + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addReconcilers(); err == nil { + t.Fatal("expected error for missing per-target constructor, got nil") + } +} + +// ── addTargetHooks ──────────────────────────────────────────────────────────── + +func TestAddTargetHooks_WiresFactoryForDistinctBinary(t *testing.T) { + fn := stubHookFn() + orktypes.TargetHookRegistry[testGVK] = map[string]func() domain.AnyReconcileHooks{ + "v2": fn, + } + t.Cleanup(func() { delete(orktypes.TargetHookRegistry, testGVK) }) + + crd := withTargetHookLocation(crdWithGVK(testGVK), "v2", "github.com/test/v2hooks") + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addTargetHooks(); err != nil { + t.Fatalf("addTargetHooks returned error: %v", err) + } + got := k.enabledCRDs["myapp"] + if got.TargetHookFactories == nil || got.TargetHookFactories["v2"] == nil { + t.Error("expected TargetHookFactories[v2] to be set, got nil") + } +} + +func TestAddTargetHooks_SkipsTargetWithSameBinaryAsBase(t *testing.T) { + // Target location matches CRD-level → no TargetHookRegistry needed. + crd := crdWithGVK(testGVK) + crd.OperatorBox.Reconciler.Hooks = &orktypes.HookDeclaration{Location: "github.com/test/hooks"} + crd = withTargetHookLocation(crd, "v2", "github.com/test/hooks") // same location + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addTargetHooks(); err != nil { + t.Fatalf("addTargetHooks returned error: %v", err) + } + if k.enabledCRDs["myapp"].TargetHookFactories != nil { + t.Error("TargetHookFactories should be nil when target shares base binary") + } +} + +func TestAddTargetHooks_ErrorWhenGVKMissingFromRegistry(t *testing.T) { + delete(orktypes.TargetHookRegistry, testGVK) + + crd := withTargetHookLocation(crdWithGVK(testGVK), "v2", "github.com/test/v2hooks") + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addTargetHooks(); err == nil { + t.Fatal("expected error when GVK missing from TargetHookRegistry, got nil") + } +} + +func TestAddTargetHooks_ErrorWhenTargetNameMissingFromRegistry(t *testing.T) { + orktypes.TargetHookRegistry[testGVK] = map[string]func() domain.AnyReconcileHooks{ + "other": stubHookFn(), // registered for a different target + } + t.Cleanup(func() { delete(orktypes.TargetHookRegistry, testGVK) }) + + crd := withTargetHookLocation(crdWithGVK(testGVK), "v2", "github.com/test/v2hooks") + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addTargetHooks(); err == nil { + t.Fatal("expected error when target name missing from TargetHookRegistry, got nil") + } +} + +func TestAddTargetHooks_SkipsWhenNoServeTargetEntries(t *testing.T) { + crd := crdWithGVK(testGVK) + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addTargetHooks(); err != nil { + t.Fatalf("addTargetHooks returned error for CRD with no serve.target.entries: %v", err) + } +} + +// ── addTargetConstructors ───────────────────────────────────────────────────── + +func TestAddTargetConstructors_WiresFactoryForDistinctConstructor(t *testing.T) { + fn := stubRecFn() + orktypes.TargetReconcilerRegistry[testGVK] = map[string]orktypes.NewReconcilerFunc{ + "v2": fn, + } + t.Cleanup(func() { delete(orktypes.TargetReconcilerRegistry, testGVK) }) + + crd := withTargetDefaultFalse(crdWithGVK(testGVK), "v2") + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addTargetConstructors(); err != nil { + t.Fatalf("addTargetConstructors returned error: %v", err) + } + got := k.enabledCRDs["myapp"] + if got.TargetReconcilerFactories == nil || got.TargetReconcilerFactories["v2"] == nil { + t.Error("expected TargetReconcilerFactories[v2] to be set, got nil") + } +} + +func TestAddTargetConstructors_SkipsTargetWithDefaultReconciler(t *testing.T) { + // target has default: true → no TargetReconcilerRegistry needed + crd := crdWithGVK(testGVK) + if crd.Serve == nil { + crd.Serve = &orktypes.ServeConfig{Enabled: true} + } + crd.Serve.Target.Entries = map[string]*orktypes.ServeTargetConfig{ + "v2": {OperatorBox: &orktypes.OperatorBoxConfig{Reconciler: &orktypes.ReconcilerConfig{Default: boolPtr(true)}}}, + } + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addTargetConstructors(); err != nil { + t.Fatalf("addTargetConstructors returned error: %v", err) + } + if k.enabledCRDs["myapp"].TargetReconcilerFactories != nil { + t.Error("TargetReconcilerFactories should be nil when target uses default reconciler") + } +} + +func TestAddTargetConstructors_ErrorWhenGVKMissingFromRegistry(t *testing.T) { + delete(orktypes.TargetReconcilerRegistry, testGVK) + + crd := withTargetDefaultFalse(crdWithGVK(testGVK), "v2") + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addTargetConstructors(); err == nil { + t.Fatal("expected error when GVK missing from TargetReconcilerRegistry, got nil") + } +} + +func TestAddTargetConstructors_ErrorWhenTargetNameMissingFromRegistry(t *testing.T) { + orktypes.TargetReconcilerRegistry[testGVK] = map[string]orktypes.NewReconcilerFunc{ + "other": stubRecFn(), + } + t.Cleanup(func() { delete(orktypes.TargetReconcilerRegistry, testGVK) }) + + crd := withTargetDefaultFalse(crdWithGVK(testGVK), "v2") + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addTargetConstructors(); err == nil { + t.Fatal("expected error when target name missing from TargetReconcilerRegistry, got nil") + } +} + +func TestAddTargetConstructors_SkipsWhenNoServeTargetEntries(t *testing.T) { + crd := crdWithGVK(testGVK) + k := katalogWith(map[string]orktypes.CRDEntry{"myapp": crd}) + + if err := k.addTargetConstructors(); err != nil { + t.Fatalf("addTargetConstructors returned error for CRD with no serve.target.entries: %v", err) + } +} diff --git a/pkg/katalog/validation_methods.go b/pkg/katalog/validation_methods.go index 8bec4fd04..6e1827e25 100644 --- a/pkg/katalog/validation_methods.go +++ b/pkg/katalog/validation_methods.go @@ -365,60 +365,6 @@ func (k *Katalog) addRuntimeObjects() error { return nil } -// --------------------------------------------------------------------------------- -// Add reconcilers -func (k *Katalog) addReconcilers() error { - for name, crd := range k.enabledCRDs { - rc := crd.OperatorBox - - // Add providers block - if len(rc.ProviderBlocks) > 0 { - blocks, err := orktypes.ParseProviderBlocks(rc.RawProviders) - if err != nil { - return err - } - rc.ProviderBlocks = blocks - } - - if !crd.IsDynamic() { - if crd.DefaultReconcile() { - continue - } - - constructorFn, ok := orktypes.ReconcilerRegistry[crd.GroupVersionKind] - if !ok { - return fmt.Errorf( - "CRD %q: no constructor registered — "+ - "check reconciler.constructor in Katalog and re-run ork generate registry", - name, - ) - } - - rc.Constructor = constructorFn - } - - crd.OperatorBox = rc - k.enabledCRDs[name] = crd - } - return nil -} - -// --------------------------------------------------------------------------------- -// Add hooks -func (k *Katalog) addHooks() error { - for name, crd := range k.enabledCRDs { - if !crd.DefaultReconcile() { - continue - } - if hookFn, ok := orktypes.HookRegistry[crd.GroupVersionKind]; ok { - crd.OperatorBox.HookFactory = hookFn - k.enabledCRDs[name] = crd - } - // not found — fine, GenericReconciler runs without hooks - } - return nil -} - // validateStatus sets IgnoreStatusPatch and IgnoreObservedGeneration on each // enabled CRD entry based on the built-in resource registry. // diff --git a/pkg/kubeclient/fixture/01-hooks/go.mod b/pkg/kubeclient/fixture/01-hooks/go.mod index e1fb67e45..d79d6e4ff 100644 --- a/pkg/kubeclient/fixture/01-hooks/go.mod +++ b/pkg/kubeclient/fixture/01-hooks/go.mod @@ -1,6 +1,6 @@ module github.com/orkspace/orkestra-args-hooks -go 1.26.4 +go 1.26.6 require ( github.com/orkspace/orkestra v0.0.0 diff --git a/pkg/kubeclient/fixture/02-constructor/go.mod b/pkg/kubeclient/fixture/02-constructor/go.mod index 15e678ef9..900601f95 100644 --- a/pkg/kubeclient/fixture/02-constructor/go.mod +++ b/pkg/kubeclient/fixture/02-constructor/go.mod @@ -1,6 +1,6 @@ module github.com/orkspace/orkestra-args-constructor -go 1.26.4 +go 1.26.6 require ( github.com/orkspace/orkestra v0.0.0 diff --git a/pkg/kubeclient/fixture/03-hooks-targets/Dockerfile b/pkg/kubeclient/fixture/03-hooks-targets/Dockerfile new file mode 100644 index 000000000..53083ae73 --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/Dockerfile @@ -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/kubeclient/fixture/03-hooks-targets/Makefile b/pkg/kubeclient/fixture/03-hooks-targets/Makefile new file mode 100644 index 000000000..d3a131738 --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/Makefile @@ -0,0 +1,97 @@ +# ── Typed Orkestra Operator — Hooks with Targets ────────────────────────────── +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) + +.PHONY: registry +registry: + @[ -f go.mod.txt ] && mv go.mod.txt go.mod || true + @[ -f go.sum.txt ] && mv go.sum.txt go.sum || true + @find . -name "*.go" | xargs grep -l "^//go:build ignore$$" 2>/dev/null | while read f; do tail -n +3 "$$f" > "$$f.tmp" && mv "$$f.tmp" "$$f"; done || true + ork generate registry --file $(KATALOG) + +.PHONY: build +build: + @[ -f go.mod.txt ] && mv go.mod.txt go.mod || true + @[ -f go.sum.txt ] && mv go.sum.txt go.sum || true + @find . -name "*.go" | xargs grep -l "^//go:build ignore$$" 2>/dev/null | while read f; do tail -n +3 "$$f" > "$$f.tmp" && mv "$$f.tmp" "$$f"; done || true + @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)" + +.PHONY: build-runtime +build-runtime: + @[ -f go.mod.txt ] && mv go.mod.txt go.mod || true + @[ -f go.sum.txt ] && mv go.sum.txt go.sum || true + @find . -name "*.go" | xargs grep -l "^//go:build ignore$$" 2>/dev/null | while read f; do tail -n +3 "$$f" > "$$f.tmp" && mv "$$f.tmp" "$$f"; done || true + @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)" + +.PHONY: validate +validate: + $(DEV_OUTPUT_DIR)/$(BINARY_NAME) validate -f $(KATALOG) + +.PHONY: simulate +simulate: + $(DEV_OUTPUT_DIR)/$(BINARY_NAME) simulate + +.PHONY: e2e +e2e: + $(DEV_OUTPUT_DIR)/$(BINARY_NAME) e2e + +.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 + +.PHONY: clean +clean: + @rm -f $(DEV_OUTPUT_DIR)/$(BINARY_NAME) + @rm -rf $(PROD_OUTPUT_DIR) + @echo "✅ Removed all local builds" + +.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/kubeclient/fixture/03-hooks-targets/README.md b/pkg/kubeclient/fixture/03-hooks-targets/README.md new file mode 100644 index 000000000..2c7da0e8c --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/README.md @@ -0,0 +1,116 @@ +# Per-Target Args — BlockchainAppWithTargets + +The same hook binary can behave differently on different surfaces. The platform +team declares two targets — `v2-enabled` and `v2-disabled` — each with its own +`operatorBox` and its own `args`. The hook reads `kube.Args()` and never knows +which surface it came from. + +```yaml +serve: + target: + v2-enabled: + primary: true + operatorBox: + preReconcile: + enqueueGate: # blocks outside business hours on this surface + when: + - field: '{{ inBusinessHours }}' + equals: "true" + reconciler: + hooks: + args: + featureEnabled: "true" # forced — no HTTP call needed + inBusinessHours: '{{ inBusinessHours }}' + + v2-disabled: + operatorBox: + reconciler: + hooks: + args: + featureEnabled: "false" # forced off — no gate + inBusinessHours: '{{ inBusinessHours }}' +``` + +The hook code is identical to `01-hooks`. The Katalog determines what each +surface means; the caller just picks a target: + +**Requirement:** `ork` CLI — install from [orkestra-install](https://github.com/orkspace/orkestra#getting-started) + +--- + +## Step 1 — Generate the registry + +```bash +make registry +``` + +## Step 2 — Build + +```bash +make clean && make build +ork validate katalog.yaml +ork simulate -f simulate-v2-enabled.yaml +ork simulate -f simulate-v2-disabled.yaml +``` + +## Step 3 — Run + +```bash +ork run --dev-server +``` + +In another terminal, start the gateway and get the token: + +```bash +export ORK_PORT=8888 +ork gate run +``` + +Apply via the `v2-enabled` surface (feature on, business-hours gate active): + +```bash +export TOKEN=$(kubectl get secret ork-dev-token -n default -o jsonpath='{.data.token}' | base64 -d) +``` + +```bash +ork serve apply -f intent/intent-v2-enabled.yaml --token $TOKEN --api http://localhost:8888 +``` + +Check the result: + +```bash +kubectl get deployment 03-hooks-targets-my-chain \ + -o jsonpath='{.metadata.annotations.feature\.demo/v2-enabled}' && echo + +kubectl get blockchainappwithtargets 03-hooks-targets-my-chain \ + -o jsonpath='{.status.featureEnabled}' && echo + +kubectl get blockchainappwithtargets 03-hooks-targets-my-chain \ + -o jsonpath='{.status.inBusinessHours}' && echo +``` + +Switch to `v2-disabled` (feature off, no gate): + +```bash +ork serve apply -f intent/intent-v2-disabled.yaml --token $TOKEN --api http://localhost:8888 +``` + +> Switching targets cleans up the previous surface's resources automatically. +> `keepPreviousSurface: true` on the target entry skips the cleanup when you +> want both surfaces running simultaneously. + +## E2E + +```bash +make docker push IMAGE_REPO=yourregistry/blockchainappwithtargets-operator IMAGE_TAG=latest + +ork e2e --dev-server \ + --set runtime.image.repository=yourregistry/blockchainappwithtargets-operator \ + --set runtime.image.tag=latest +``` + +## Cleanup + +```bash +chmod +x ./cleanup.sh && ./cleanup.sh +``` diff --git a/pkg/kubeclient/fixture/03-hooks-targets/api/v1alpha1/blockchainappwithtargets_types.go b/pkg/kubeclient/fixture/03-hooks-targets/api/v1alpha1/blockchainappwithtargets_types.go new file mode 100644 index 000000000..8df2fb197 --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/api/v1alpha1/blockchainappwithtargets_types.go @@ -0,0 +1,76 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +type BlockchainAppWithTargetsSpec struct { + Image string `json:"image"` + Network string `json:"network"` + NodeType string `json:"nodeType,omitempty"` + Replicas int `json:"replicas,omitempty"` + ServiceUrl string `json:"serviceUrl,omitempty"` +} + +type BlockchainAppWithTargetsStatus struct { + Phase string `json:"phase,omitempty"` + Network string `json:"network,omitempty"` + NodeType string `json:"nodeType,omitempty"` + FeatureEnabled string `json:"featureEnabled,omitempty"` + InBusinessHours bool `json:"inBusinessHours,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +type BlockchainAppWithTargets struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec BlockchainAppWithTargetsSpec `json:"spec,omitempty"` + Status BlockchainAppWithTargetsStatus `json:"status,omitempty"` +} + +type BlockchainAppWithTargetsList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []BlockchainAppWithTargets `json:"items"` +} + +func (a *BlockchainAppWithTargets) DeepCopyObject() runtime.Object { + if a == nil { + return nil + } + out := new(BlockchainAppWithTargets) + a.DeepCopyInto(out) + return out +} + +func (a *BlockchainAppWithTargets) DeepCopyInto(out *BlockchainAppWithTargets) { + *out = *a + out.TypeMeta = a.TypeMeta + a.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = a.Spec + a.Status.DeepCopyInto(&out.Status) +} + +func (s *BlockchainAppWithTargetsStatus) DeepCopyInto(out *BlockchainAppWithTargetsStatus) { + *out = *s + if s.Conditions != nil { + out.Conditions = make([]metav1.Condition, len(s.Conditions)) + copy(out.Conditions, s.Conditions) + } +} + +func (al *BlockchainAppWithTargetsList) DeepCopyObject() runtime.Object { + if al == nil { + return nil + } + out := new(BlockchainAppWithTargetsList) + *out = *al + if al.Items != nil { + out.Items = make([]BlockchainAppWithTargets, len(al.Items)) + for i := range al.Items { + al.Items[i].DeepCopyInto(&out.Items[i]) + } + } + return out +} diff --git a/pkg/kubeclient/fixture/03-hooks-targets/api/v1alpha1/register.go b/pkg/kubeclient/fixture/03-hooks-targets/api/v1alpha1/register.go new file mode 100644 index 000000000..827a4b23a --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/api/v1alpha1/register.go @@ -0,0 +1,22 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + GroupVersion = schema.GroupVersion{Group: "demo.orkestra.io", Version: "v1alpha1"} + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + AddToScheme = SchemeBuilder.AddToScheme +) + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(GroupVersion, + &BlockchainAppWithTargets{}, + &BlockchainAppWithTargetsList{}, + ) + metav1.AddToGroupVersion(scheme, GroupVersion) + return nil +} diff --git a/pkg/kubeclient/fixture/03-hooks-targets/cleanup.sh b/pkg/kubeclient/fixture/03-hooks-targets/cleanup.sh new file mode 100644 index 000000000..e288aaafc --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/cleanup.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail +echo "Cleaning up blockchainappwithtargets hooks operator..." +kubectl delete blockchainappwithtargets 03-hooks-targets-my-chain --ignore-not-found +kubectl delete -f crd.yaml --ignore-not-found +echo "✓ Done. Stop 'ork run' with Ctrl+C if still running locally." diff --git a/pkg/kubeclient/fixture/03-hooks-targets/cmd/orkestra/main.go b/pkg/kubeclient/fixture/03-hooks-targets/cmd/orkestra/main.go new file mode 100644 index 000000000..7567184db --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/cmd/orkestra/main.go @@ -0,0 +1,25 @@ +// Code generated by "ork generate registry" on 2026-08-16T20:29:01Z. DO NOT EDIT. +// Re-generate by running: ork generate registry --file +package main + +import ( + "context" + + "github.com/orkspace/orkestra/cmd/cli" + "github.com/orkspace/orkestra/pkg/konfig" + "github.com/orkspace/orkestra/pkg/logger" + "github.com/orkspace/orkestra/pkg/utils" + + _ "github.com/orkspace/orkestra-args-hooks-targets/pkg/typeregistry" +) + +func main() { + kfg, err := konfig.Init() + if err != nil { + logger.Fatal().AnErr("failed to load configurations", err) + utils.Exit(err) + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + cli.Execute(kfg, ctx) +} \ No newline at end of file diff --git a/pkg/kubeclient/fixture/03-hooks-targets/cr-e2e.yaml b/pkg/kubeclient/fixture/03-hooks-targets/cr-e2e.yaml new file mode 100644 index 000000000..cefd83bf0 --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/cr-e2e.yaml @@ -0,0 +1,11 @@ +apiVersion: demo.orkestra.io/v1alpha1 +kind: BlockchainAppWithTargets +metadata: + name: 03-hooks-targets-my-chain + namespace: default +spec: + image: ethereum/client-go:v1.14.0 + network: testnet + nodeType: full-node + replicas: 5 + serviceUrl: "http://orkestra-dev-server.orkestra-system.svc:9999" diff --git a/pkg/kubeclient/fixture/03-hooks-targets/cr.yaml b/pkg/kubeclient/fixture/03-hooks-targets/cr.yaml new file mode 100644 index 000000000..a294174ea --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/cr.yaml @@ -0,0 +1,11 @@ +apiVersion: demo.orkestra.io/v1alpha1 +kind: BlockchainAppWithTargets +metadata: + name: 03-hooks-targets-my-chain + namespace: default +spec: + image: ethereum/client-go:v1.14.0 + network: testnet + nodeType: full-node + replicas: 5 + serviceUrl: "http://localhost:9999" diff --git a/pkg/kubeclient/fixture/03-hooks-targets/crd.yaml b/pkg/kubeclient/fixture/03-hooks-targets/crd.yaml new file mode 100644 index 000000000..e5a749929 --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/crd.yaml @@ -0,0 +1,63 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: blockchainappwithtargets.demo.orkestra.io +spec: + group: demo.orkestra.io + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Network + type: string + jsonPath: .spec.network + - name: NodeType + type: string + jsonPath: .spec.nodeType + - name: Feature + type: string + jsonPath: .status.featureEnabled + - name: Phase + type: string + jsonPath: .status.phase + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + required: [image, network] + properties: + image: + type: string + network: + type: string + enum: [mainnet, testnet, devnet] + nodeType: + type: string + enum: [validator, full-node, light-node] + default: full-node + replicas: + type: integer + minimum: 1 + description: Desired replica count when the feature flag is enabled. + serviceUrl: + type: string + description: > + Base URL of the feature-flag service. + Local: http://localhost:9999 + In-cluster: http://orkestra-dev-server.orkestra-system.svc:9999 + status: + type: object + x-kubernetes-preserve-unknown-fields: true + names: + kind: BlockchainAppWithTargets + plural: blockchainappwithtargets + singular: blockchainappwithtargets + scope: Namespaced diff --git a/pkg/kubeclient/fixture/03-hooks-targets/e2e.yaml b/pkg/kubeclient/fixture/03-hooks-targets/e2e.yaml new file mode 100644 index 000000000..38ecea645 --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/e2e.yaml @@ -0,0 +1,60 @@ +apiVersion: orkestra.orkspace.io/v1 +kind: E2E +metadata: + name: blockchainappwithtargets-hooks-e2e + description: > + Run with: ork e2e --dev-server + + Verifies per-target args resolution end-to-end. v2-enabled target forces + featureEnabled=true via args and gates on business hours. v2-disabled forces + featureEnabled=false with no gate. Same hook binary, no HTTP call needed. + +spec: + katalog: ./katalog.yaml + crd: ./crd.yaml + cr: ./cr-e2e.yaml + + valuesFiles: + - ./values.yaml + + notes: + functions: + - name: inBusinessHours + expression: '{{ and weekday (timeInWindow "09:00" "18:00") }}' + + cluster: + provider: kind + name: ork-args-hooks-targets + reuse: false + + expect: + - name: Deployment created and ready (v2-enabled) + after: cr-applied + timeout: 90s + resources: + - kind: Deployment + namespace: default + name: 03-hooks-targets-my-chain + ready: true + + - name: Feature flag annotation true (v2-enabled, business hours) + after: cr-applied + timeout: 30s + when: + - field: '{{ inBusinessHours }}' + equals: "true" + kubectl: + get: + - kind: Deployment + name: 03-hooks-targets-my-chain + namespace: default + field: '.metadata.annotations.feature\.demo/v2-enabled' + equals: "true" + + - name: Deployment removed on delete + after: cr-deleted + timeout: 30s + resources: + - kind: Deployment + namespace: default + count: 0 diff --git a/pkg/kubeclient/fixture/03-hooks-targets/go.mod b/pkg/kubeclient/fixture/03-hooks-targets/go.mod new file mode 100644 index 000000000..d8a638f9e --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/go.mod @@ -0,0 +1,234 @@ +module github.com/orkspace/orkestra-args-hooks-targets + +go 1.26.6 + +require ( + github.com/orkspace/orkestra v0.0.0 + k8s.io/apimachinery v0.36.1 +) + +require ( + cel.dev/expr v0.25.1 // indirect + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.20.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/iam v1.7.0 // indirect + cloud.google.com/go/monitoring v1.24.3 // indirect + cloud.google.com/go/pubsub v1.50.2 // indirect + cloud.google.com/go/pubsub/v2 v2.4.0 // indirect + cloud.google.com/go/storage v1.62.1 // indirect + dario.cat/mergo v1.0.1 // indirect + filippo.io/edwards25519 v1.1.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/servicebus/armservicebus v1.2.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.2.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/Masterminds/squirrel v1.5.4 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.7 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.18 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.17 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect + github.com/aws/aws-sdk-go-v2/service/rds v1.118.2 // indirect + github.com/aws/aws-sdk-go-v2/service/route53 v1.62.7 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect + github.com/aws/smithy-go v1.25.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect + github.com/containerd/containerd v1.7.33 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v1.0.0-rc.1 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/go-errors/errors v1.4.2 // indirect + github.com/go-gorp/gorp/v3 v3.1.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonreference v0.21.3 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.2 // indirect + github.com/go-sql-driver/mysql v1.9.3 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect + github.com/googleapis/gax-go/v2 v2.22.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/gosuri/uitable v0.0.4 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.9.2 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.1 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/spdystream v0.5.1 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pierrec/lz4/v4 v4.1.15 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/otlptranslator v1.0.0 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/redis/go-redis/v9 v9.14.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/rs/zerolog v1.35.1 // indirect + github.com/rubenv/sql-migrate v1.8.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + github.com/segmentio/kafka-go v0.4.51 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + go.mongodb.org/mongo-driver v1.17.9 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.39.0 // indirect + golang.org/x/time v0.15.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/api v0.280.0 // indirect + google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/grpc v1.82.1 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + helm.sh/helm/v3 v3.20.2 // indirect + k8s.io/api v0.36.1 // indirect + k8s.io/apiextensions-apiserver v0.36.1 // indirect + k8s.io/apiserver v0.36.1 // indirect + k8s.io/cli-runtime v0.36.1 // indirect + k8s.io/client-go v0.36.1 // indirect + k8s.io/component-base v0.36.1 // indirect + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect + k8s.io/kubectl v0.36.1 // indirect + k8s.io/streaming v0.36.1 // indirect + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect + oras.land/oras-go/v2 v2.6.2 // indirect + sigs.k8s.io/controller-runtime v0.24.1 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/kustomize/api v0.21.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) + +replace github.com/orkspace/orkestra => ../../../.. diff --git a/pkg/kubeclient/fixture/03-hooks-targets/go.sum b/pkg/kubeclient/fixture/03-hooks-targets/go.sum new file mode 100644 index 000000000..7f4e30bd1 --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/go.sum @@ -0,0 +1,746 @@ +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= +cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U= +cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY= +cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU= +cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58= +cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA= +cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak= +cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY= +cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E= +cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= +cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= +cloud.google.com/go/pubsub v1.50.2 h1:54Up97HnThdP4H8jjWJSSQ/mnYG2EKon7ZSNETRq0tM= +cloud.google.com/go/pubsub v1.50.2/go.mod h1:jyCWeZdGFqd4mitSsBERnJcpqaHBsxQoPkNvjj4sp0w= +cloud.google.com/go/pubsub/v2 v2.4.0 h1:oMKNiBQpXImRWnHYla9uSU66ZzByZwBSCJOEs/pTKVg= +cloud.google.com/go/pubsub/v2 v2.4.0/go.mod h1:2lS/XQKq5qtOMs6kHBK+WX1ytUC36kLl2ig3zqsGUx8= +cloud.google.com/go/storage v1.62.1 h1:Os0G3XbUbjZumkpDUf2Y0rLoXJTCF1kU2kWUujKYXD8= +cloud.google.com/go/storage v1.62.1/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= +filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/servicebus/armservicebus v1.2.0 h1:jngSeKBnzC7qIk3rvbWHsLI7eeasEucORHWr2CHX0Yg= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/servicebus/armservicebus v1.2.0/go.mod h1:1YXAxWw6baox+KafeQU2scy21/4IHvqXoIJuCpcvpMQ= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.2.0 h1:S087deZ0kP1RUg4pU7w9U9xpUedTCbOtz+mnd0+hrkQ= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.2.0/go.mod h1:B4cEyXrWBmbfMDAPnpJ1di7MAt5DKP57jPEObAvZChg= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= +github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY= +github.com/aws/aws-sdk-go-v2/config v1.32.18 h1:Hcia46bxhGgF3BaSnG8nSNCWmqTK6bj9xN9/FJ3WK6Q= +github.com/aws/aws-sdk-go-v2/config v1.32.18/go.mod h1:zEjCAYmxqDadH1WX8CdBvmLKhUEUVFgKRQG38zjDmrY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.17 h1:gP2nkGsS+KMvF/jfFz2Vv2qiiOqWKyPACSzPsqHgoW8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.17/go.mod h1:Bsew3S/moG5iT77giPj1q8wb/s0RE5/QfH+ASjYtuQc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 h1:ieLCO1JxUWuxTZ1cRd0GAaeX7O6cIxnwk7tc1LsQhC4= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15/go.mod h1:e3IzZvQ3kAWNykvE0Tr0RDZCMFInMvhku3qNpcIQXhM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mwgx5ToK7wot2sBBce/ojzgnPzZXUV445gTSyE8= +github.com/aws/aws-sdk-go-v2/service/rds v1.118.2 h1:pkEeQneYFpTAnGhyqSbyp/DlCPPJTGt0GkWahlLYzMA= +github.com/aws/aws-sdk-go-v2/service/rds v1.118.2/go.mod h1:7gS+cGrKF0mH253QHFlStmx79ws+DlNk+04ZRfmw3U0= +github.com/aws/aws-sdk-go-v2/service/route53 v1.62.7 h1:twRRMmtSITnt/rrp+D7UDLzE5pKMZe759aalkUdN+OY= +github.com/aws/aws-sdk-go-v2/service/route53 v1.62.7/go.mod h1:ztM1lr+sRoCAI8336ZUvlRPbToue0d3gE/wd6jomSJ8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 h1:etqBTKY581iwLL/H/S2sVgk3C9lAsTJFeXWFDsDcWOU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.0 h1:nDARhv/oF55bcxF7rCI/4PDxOKnVXVWwDuDwCs2I2SQ= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.0/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio= +github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI= +github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= +github.com/containerd/containerd v1.7.33 h1:iAkYGC/ifR/V+0eR4iXWHNGYUF0DF2PmGV5iz4Irj5M= +github.com/containerd/containerd v1.7.33/go.mod h1:gSbSCVjPCdkfJCjyrzz7aRC+xFlqVbatNpfHfVCYGUM= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= +github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.19 h1:tUN6H7LWqNx4hQVxomd0CVsDwaDr9gaRQaI4GpSmrsA= +github.com/creack/pty v1.1.19/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= +github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= +github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= +github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= +github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= +github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas= +github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4= +github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= +github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y= +github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= +github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= +github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= +github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= +github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= +github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= +github.com/redis/go-redis/extra/redisotel/v9 v9.5.3 h1:kuvuJL/+MZIEdvtb/kTBRiRgYaOmx1l+lYJyVdrRUOs= +github.com/redis/go-redis/extra/redisotel/v9 v9.5.3/go.mod h1:7f/FMrf5RRRVHXgfk7CzSVzXHiWeuOQUu2bsVqWoa+g= +github.com/redis/go-redis/v9 v9.14.1 h1:nDCrEiJmfOWhD76xlaw+HXT0c9hfNWeXgl0vIRYSDvQ= +github.com/redis/go-redis/v9 v9.14.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= +github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= +github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0= +github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/segmentio/kafka-go v0.4.51 h1:JgDPPG75tC1rWIS2Me6MwcvXJ6f49UQ4HjAOef71Hno= +github.com/segmentio/kafka-go v0.4.51/go.mod h1:Y1gn60kzLEEaW28YshXyk2+VCUKbJ3Qr6DrnT3i4+9E= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.einride.tech/aip v0.83.0 h1:TI21IdeOnLTwZEJ3BxtImIZk6bsN2Q+sd0x99SLiQ+M= +go.einride.tech/aip v0.83.0/go.mod h1:E8+wdTApA70odnpFzJgsGogHozC2JCIhFJBKPr8bVig= +go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= +go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= +go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= +go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y= +go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= +go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.280.0 h1:F4OfEHZhZh6a7uTufJAXXVd/2TQ8EjM4vZH+jX/vFYk= +google.golang.org/api v0.280.0/go.mod h1:oGKmPZRDoD3vdkf6MA7F4VNkR1rxCiuaPSkhsf3EolU= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +helm.sh/helm/v3 v3.20.2 h1:binM4rvPx5DcNsa1sIt7UZi55lRbu3pZUFmQkSoRh48= +helm.sh/helm/v3 v3.20.2/go.mod h1:Fl1kBaWCpkUrM6IYXPjQ3bdZQfFrogKArqptvueZ6Ww= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY= +k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo= +k8s.io/apiextensions-apiserver v0.36.1 h1:6JfYmPUsuUIHuN+3QxutXYWj492RqF5fBSx67GYK5Ks= +k8s.io/apiextensions-apiserver v0.36.1/go.mod h1:pLzZin90riwisdzKwv/GoTwENooytoIx5zWJb4Hkby8= +k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA= +k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8= +k8s.io/apiserver v0.36.1 h1:iMS5V+rPUertv5P9RaqJgmHHTuh4quWpoxchvMUY+JY= +k8s.io/apiserver v0.36.1/go.mod h1:Cby1PbLWztu0GDOxoO6iFOyyqIsziHNEW+w9zVQ22Kw= +k8s.io/cli-runtime v0.36.1 h1:yuC/BGnnj1YYPh6D1P+pZnzinCs6DvMq86yAeNqoqzM= +k8s.io/cli-runtime v0.36.1/go.mod h1:ZQWHGt8xAF7KnviB79vX0lYNyUUqKIpU+LQg7exuFAw= +k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0= +k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU= +k8s.io/component-base v0.36.1 h1:iG6GsELftXqTNG9HG6kiVjatSgAw1sf5pJ6R5a6N0kA= +k8s.io/component-base v0.36.1/go.mod h1:nf9XPlntRdqO6WMeEWAA5F93Y4ICZQdeT9GeqLDB3JI= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/kubectl v0.36.1 h1:96HqS9twIdHM0MlJLTwbo14b9kUKPkOzZ4tlRDLv4qI= +k8s.io/kubectl v0.36.1/go.mod h1:/DGPAIewKsFWF9VFgGvkPhao2Ev4SNuE3BioZo8yPbk= +k8s.io/streaming v0.36.1 h1:L+K68n4Gg940BGNNYtUBvL1WTLL0YnKT3s+P1MNAmR4= +k8s.io/streaming v0.36.1/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +oras.land/oras-go/v2 v2.6.2 h1:N04RXngAp1LJKTG6ifz3xHPipasEkWr+hFmInja5YKo= +oras.land/oras-go/v2 v2.6.2/go.mod h1:PlTtg4JTDJkDe8yVHpM2wz7/YDc00GVas+i4jAW2TZ4= +sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4= +sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= +sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI= +sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI= +sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/pkg/kubeclient/fixture/03-hooks-targets/hooks/blockchainappwithtargets_hooks.go b/pkg/kubeclient/fixture/03-hooks-targets/hooks/blockchainappwithtargets_hooks.go new file mode 100644 index 000000000..31b1d1e26 --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/hooks/blockchainappwithtargets_hooks.go @@ -0,0 +1,58 @@ +package hooks + +import ( + "context" + "fmt" + + apiv1 "github.com/orkspace/orkestra-args-hooks-targets/api/v1alpha1" + "github.com/orkspace/orkestra/domain" + "github.com/orkspace/orkestra/pkg/kubeclient" + orkdeploy "github.com/orkspace/orkestra/pkg/resources/deployments" +) + +// BlockchainAppHooks returns the hook implementation registered in the Katalog. +func BlockchainAppHooks() domain.AnyReconcileHooks { + return domain.ReconcileHooks[*apiv1.BlockchainAppWithTargets]{OnReconcile: onBlockchainAppWithTargetsReconcile} +} + +func onBlockchainAppWithTargetsReconcile(ctx context.Context, obj *apiv1.BlockchainAppWithTargets) error { + kube, ok := kubeclient.FromContext(ctx) + if !ok { + return fmt.Errorf("kubeclient not in context") + } + + // Both values come from the Katalog args — resolved per target surface. + // v2-enabled target: featureEnabled="true", enqueueGate blocks outside hours. + // v2-disabled target: featureEnabled="false", no gate. + // The hook binary is identical — only the args change between targets. + inBusinessHours := kube.Args().String("inBusinessHours") == "true" + featureEnabled := kube.Args().String("featureEnabled") == "true" + + annotation := "false" + if inBusinessHours && featureEnabled { + annotation = "true" + } + + replicas := int32(obj.Spec.Replicas) + if replicas == 0 { + replicas = 1 + } + + spec := orkdeploy.ResolvedDeploymentSpec{ + Name: obj.Name, + Namespace: obj.Namespace, + Image: obj.Spec.Image, + Replicas: replicas, + Annotations: map[string]string{ + "feature.demo/v2-enabled": annotation, + }, + } + if err := orkdeploy.Apply(ctx, kube, obj, spec); err != nil { + return fmt.Errorf("blockchainappwithtargets deployment: %w", err) + } + + return kube.PatchStatus(ctx, obj, map[string]any{ + "featureEnabled": annotation, + "inBusinessHours": inBusinessHours, + }) +} diff --git a/pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-disabled.yaml b/pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-disabled.yaml new file mode 100644 index 000000000..f576e38ce --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-disabled.yaml @@ -0,0 +1,6 @@ +target: v2-disabled +name: 03-hooks-targets-my-chain +image: ethereum/client-go:v1.14.0 +network: testnet +nodeType: full-node +replicas: 5 diff --git a/pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-enabled.yaml b/pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-enabled.yaml new file mode 100644 index 000000000..db32c01a7 --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-enabled.yaml @@ -0,0 +1,6 @@ +target: v2-enabled +name: 03-hooks-targets-my-chain +image: ethereum/client-go:v1.14.0 +network: testnet +nodeType: full-node +replicas: 5 diff --git a/pkg/kubeclient/fixture/03-hooks-targets/katalog.yaml b/pkg/kubeclient/fixture/03-hooks-targets/katalog.yaml new file mode 100644 index 000000000..819d8305a --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/katalog.yaml @@ -0,0 +1,115 @@ +apiVersion: orkestra.orkspace.io/v1 +kind: Katalog +metadata: + name: blockchainappwithtargets-hooks + author: orkspace + version: 0.1.0 + description: > + BlockchainAppWithTargets — same hook binary as 01-hooks, but the feature + flag value comes from the target surface rather than a live HTTP call. + "v2-enabled" forces featureEnabled=true; "v2-disabled" forces it false. + +notes: + functions: + - name: inBusinessHours + description: True on weekdays between 09:00–18:00 UTC + expression: '{{ and weekday (timeInWindow "09:00" "18:00") }}' + +gateway: + enabled: true + api: + enabled: true + auth: + tokens: + - name: dev + secretRef: + name: ork-dev-token + namespace: default + key: token + +spec: + crds: + blockchainappwithtargets: + crdFile: ./crd.yaml + apiTypes: + group: demo.orkestra.io + version: v1alpha1 + kind: BlockchainAppWithTargets + plural: blockchainappwithtargets + object: BlockchainAppWithTargets + objectList: BlockchainAppWithTargetsList + location: github.com/orkspace/orkestra-args-hooks-targets/api/v1alpha1 + alias: bcappwithtargetsv1 + + operatorBox: + reconciler: + default: true + # hooks: + # location: github.com/orkspace/orkestra-args-hooks-targets/hooks + # function: BlockchainAppHooks + # alias: bchooks + # resources: + # - kind: Deployment + # args: + # featureEnabled: '{{ .external.flags.body }}' + # inBusinessHours: '{{ inBusinessHours }}' + workers: 2 + resync: 30s + + status: + fields: + - path: phase + value: "Running" + - path: featureEnabled + value: "{{ .external.flags.body }}" + when: + - field: external.flags.called + equals: "true" + + serve: + enabled: true + namespace: default + fields: + image: + label: "Container image" + required: true + network: + label: "Network" + required: true + nodeType: + label: "Node type" + replicas: + label: "Replicas" + target: + v2-enabled: + primary: true + operatorBox: + preReconcile: + enqueueGate: + when: + - field: '{{ inBusinessHours }}' + equals: "true" + reconciler: + hooks: + hooks: + location: github.com/orkspace/orkestra-args-hooks-targets/hooks + function: BlockchainAppHooks + alias: bchooks + resources: + - kind: Deployment + args: + featureEnabled: "true" + inBusinessHours: '{{ inBusinessHours }}' + + v2-disabled: + operatorBox: + reconciler: + hooks: + location: github.com/orkspace/orkestra-args-hooks-targets/hooks + function: BlockchainAppHooks + alias: bchooks + resources: + - kind: Deployment + args: + featureEnabled: "false" + inBusinessHours: '{{ inBusinessHours }}' diff --git a/pkg/kubeclient/fixture/03-hooks-targets/pkg/typeregistry/zz_generated_typeregistry.go b/pkg/kubeclient/fixture/03-hooks-targets/pkg/typeregistry/zz_generated_typeregistry.go new file mode 100644 index 000000000..999a4201c --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/pkg/typeregistry/zz_generated_typeregistry.go @@ -0,0 +1,85 @@ +// pkg/typeregistry/zz_generated_typeregistry.go +// Code generated by "ork generate registry" on 2026-08-16T20:29:01Z. DO NOT EDIT. +// Re-generate by running: ork generate registry --file +// +// This file registers compiled Go types and external functions. +// Dynamic template CRDs do not appear here — they are handled at runtime +// by GenericReconciler without any code generation. +package typeregistry + +import ( + "github.com/orkspace/orkestra/domain" + "github.com/orkspace/orkestra/pkg/logger" + orktypes "github.com/orkspace/orkestra/pkg/types" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + bcappwithtargetsv1 "github.com/orkspace/orkestra-args-hooks-targets/api/v1alpha1" + bchooks "github.com/orkspace/orkestra-args-hooks-targets/hooks" +) + +// init runs before main(). It calls RegisterRuntimeObjects to populate the +// GVK-keyed registries and appends AddToScheme functions to SchemeAdderFns so +// that NewSchemeRegistry can find them through the internal pkg/typeregistry stub. +// +// A blank import of this package from main.go is the only wiring needed: +// +// import _ "myapp/pkg/typeregistry" +// +// "ork generate registry -k " populates this +// +// No explicit call to RegisterRuntimeObjects or RegisterTypedScheme is required. +func init() { + logger.Debug().Msg("typeregistry.init() started") + RegisterRuntimeObjects() + orktypes.SchemeAdderFns = append(orktypes.SchemeAdderFns, func(s *runtime.Scheme) error { + s.AddKnownTypeWithName(schema.GroupVersionKind{Group: "demo.orkestra.io", Version: "v1alpha1", Kind: "BlockchainAppWithTargets"}, &bcappwithtargetsv1.BlockchainAppWithTargets{}) + s.AddKnownTypeWithName(schema.GroupVersionKind{Group: "demo.orkestra.io", Version: "v1alpha1", Kind: "BlockchainAppWithTargetsList"}, &bcappwithtargetsv1.BlockchainAppWithTargetsList{}) + metav1.AddToGroupVersion(s, schema.GroupVersion{Group: "demo.orkestra.io", Version: "v1alpha1"}) + return nil + }) + logger.Debug().Int("length", len(orktypes.SchemeAdderFns)).Msg("typeregistry.init() finished") +} + +// RegisterRuntimeObjects populates ObjectRegistry, ListRegistry, HookRegistry, +// and ReconcilerRegistry. Called by init() — do not call directly. +// +// Object/List entries — factory functions for typed CRDs. +// +// Used by the informer to create zero-value instances for cache storage +// and type assertion during reconciliation. +// +// Hook entries — factory functions for Go hook implementations. +// +// Called by addHooks() during Katalog validation to wire HookFactory +// onto the CRD entry. GenericReconciler calls HookFactory() once at +// startCRDWorkers time to obtain the typed hooks. +// +// Reconciler entries — constructor functions for custom reconcilers. +// +// Called by addReconcilers() during Katalog validation to wire Constructor +// onto the CRD entry. DependencyKordinator calls Constructor() once at +// startCRDWorkers time to build the reconciler. +func RegisterRuntimeObjects() { + logger.Debug().Msg("RegisterRuntimeObjects called") + + // BlockchainAppWithTargets — typed CRD object and list factories + orktypes.ObjectRegistry[schema.GroupVersionKind{Group: "demo.orkestra.io", Version: "v1alpha1", Kind: "BlockchainAppWithTargets"}] = + func() runtime.Object { return &bcappwithtargetsv1.BlockchainAppWithTargets{} } + orktypes.ListRegistry[schema.GroupVersionKind{Group: "demo.orkestra.io", Version: "v1alpha1", Kind: "BlockchainAppWithTargets"}] = + func() runtime.Object { return &bcappwithtargetsv1.BlockchainAppWithTargetsList{} } + + // BlockchainAppWithTargets — Go hook factory + // Calls bchooks.BlockchainAppHooks() to obtain typed ReconcileHooks. + orktypes.HookRegistry[schema.GroupVersionKind{Group: "demo.orkestra.io", Version: "v1alpha1", Kind: "BlockchainAppWithTargets"}] = + func() domain.AnyReconcileHooks { + return bchooks.BlockchainAppHooks() + } + + logger.Debug(). + Int("objectRegistrySize", len(orktypes.ObjectRegistry)). + Int("listRegistrySize", len(orktypes.ListRegistry)). + Int("hookRegistrySize", len(orktypes.HookRegistry)). + Msg("Runtime objects registered") +} diff --git a/pkg/kubeclient/fixture/03-hooks-targets/simulate-v2-disabled.yaml b/pkg/kubeclient/fixture/03-hooks-targets/simulate-v2-disabled.yaml new file mode 100644 index 000000000..0cd17a1de --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/simulate-v2-disabled.yaml @@ -0,0 +1,26 @@ +apiVersion: orkestra.orkspace.io/v1 +kind: Simulate +metadata: + name: blockchainappwithtargets-v2-disabled + description: > + Run with: ork simulate --dev-server + + Routes through the "v2-disabled" target. featureEnabled is forced to "false" + via args — the hook sees the flag as off regardless of the flag service. + No enqueueGate on this surface; the Deployment is still created but the + hook behaves as if the feature is disabled. + +spec: + katalog: ./katalog.yaml + cr: ./cr.yaml + target: v2-disabled + cycles: 3 + + expect: + steady: true + noErrors: true + ops: + - cycle: 1 + verb: apply + resource: deployments + name: 03-hooks-targets-my-chain diff --git a/pkg/kubeclient/fixture/03-hooks-targets/simulate-v2-enabled.yaml b/pkg/kubeclient/fixture/03-hooks-targets/simulate-v2-enabled.yaml new file mode 100644 index 000000000..0ba57a634 --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/simulate-v2-enabled.yaml @@ -0,0 +1,22 @@ +apiVersion: orkestra.orkspace.io/v1 +kind: Simulate +metadata: + name: blockchainappwithtargets-v2-enabled + description: > + Run with: ork simulate --dev-server + + Routes through the "v2-enabled" target. featureEnabled is forced to "true" + via args — no live HTTP call needed. The enqueueGate blocks outside business + hours; during business hours the Deployment is created at spec.replicas (5). + +spec: + katalog: ./katalog.yaml + cr: ./cr.yaml + target: v2-enabled + cycles: 3 + + expect: + steady: true + noErrors: true + # enqueueGate on inBusinessHours means the deployment op only appears + # Mon–Fri 09:00–18:00 UTC — no ops assertion here to keep simulate deterministic diff --git a/pkg/kubeclient/fixture/03-hooks-targets/values.yaml b/pkg/kubeclient/fixture/03-hooks-targets/values.yaml new file mode 100644 index 000000000..63b11867d --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/values.yaml @@ -0,0 +1,4 @@ +runtime: + image: + repository: ghcr.io/orkspace/orkestra/pkg/kubeclient/fixture/03-hooks-targets + tag: latest diff --git a/pkg/kubeclient/fixture/README.md b/pkg/kubeclient/fixture/README.md index d279f99a4..282160df2 100644 --- a/pkg/kubeclient/fixture/README.md +++ b/pkg/kubeclient/fixture/README.md @@ -24,8 +24,9 @@ story with the two typed operator patterns: | Sub-directory | CRD | Pattern | World state | External call | |---|---|---|---|---| -| [`01-hooks/`](01-hooks/) | `BlockchainApp` | hooks | note evaluated by runtime → passed via args | runtime calls flag service when in business hours | -| [`02-constructor/`](02-constructor/) | `BlockchainNode` | constructor | constructor checks clock using window from args | constructor calls flag service when in business hours | +| [`01-hooks/`](01-hooks/README.md) | `BlockchainApp` | hooks | note evaluated by runtime → passed via args | runtime calls flag service when in business hours | +| [`02-constructor/`](02-constructor/README.md) | `BlockchainNode` | constructor | constructor checks clock using window from args | constructor calls flag service when in business hours | +| [`03-hooks-targets/`](03-hooks-targets/README.md) | `BlockchainAppWithTargets` | hooks + per-target operatorBox | same hook binary; `featureEnabled` and gate vary by target surface | no HTTP call — flag value resolved from target args | **Requirement:** `ork` CLI — install from [orkestra-install](https://github.com/orkspace/orkestra#getting-started) diff --git a/pkg/kubeclient/fixture/e2e.yaml b/pkg/kubeclient/fixture/e2e.yaml index 757fc2bc2..4ff02c388 100644 --- a/pkg/kubeclient/fixture/e2e.yaml +++ b/pkg/kubeclient/fixture/e2e.yaml @@ -8,3 +8,4 @@ metadata: imports: - 01-hooks/e2e.yaml - 02-constructor/e2e.yaml + - 03-hooks-targets/e2e.yaml diff --git a/pkg/kubeclient/fixture/go.mod b/pkg/kubeclient/fixture/go.mod index 0dc4e9f18..546a28b4d 100644 --- a/pkg/kubeclient/fixture/go.mod +++ b/pkg/kubeclient/fixture/go.mod @@ -1,6 +1,6 @@ module github.com/orkspace/orkestra-args-demo -go 1.26.4 +go 1.26.6 replace github.com/orkspace/orkestra => ../../.. diff --git a/pkg/kubeclient/fixture/komposer.yaml b/pkg/kubeclient/fixture/komposer.yaml index 717301cb2..fcffeb4b5 100644 --- a/pkg/kubeclient/fixture/komposer.yaml +++ b/pkg/kubeclient/fixture/komposer.yaml @@ -11,3 +11,4 @@ imports: files: - 01-hooks/katalog.yaml - 02-constructor/katalog.yaml + - 03-hooks-targets/katalog.yaml diff --git a/pkg/kubeclient/fixture/simulate.yaml b/pkg/kubeclient/fixture/simulate.yaml index 74e549def..2daa41f6b 100644 --- a/pkg/kubeclient/fixture/simulate.yaml +++ b/pkg/kubeclient/fixture/simulate.yaml @@ -8,3 +8,5 @@ metadata: imports: - 01-hooks/simulate.yaml - 02-constructor/simulate.yaml + - 03-hooks-targets/simulate-v2-enabled.yaml + - 03-hooks-targets/simulate-v2-disabled.yaml diff --git a/pkg/labels/labels.go b/pkg/labels/labels.go index cf750e122..1da0f3e25 100644 --- a/pkg/labels/labels.go +++ b/pkg/labels/labels.go @@ -236,7 +236,10 @@ func EffectiveOwnerKey(ownerName string, ownerAnnotations map[string]string) str // OrkestraOwner encodes the surface identity via EffectiveOwnerKey so that // resources from different serve surfaces carry distinct labels. This enables // precise orphan detection when a CR switches targets. -func StampOrkestraLabels(lbls map[string]string, ownerName string, ownerAnnotations map[string]string) { +func StampOrkestraLabels(lbls map[string]string, ownerName string, ownerAnnotations map[string]string) map[string]string { + if lbls == nil { + lbls = make(map[string]string) + } lbls[ManagedKey] = ManagedValue lbls[OrkestraOwner] = EffectiveOwnerKey(ownerName, ownerAnnotations) if ownerAnnotations != nil { @@ -248,4 +251,5 @@ func StampOrkestraLabels(lbls map[string]string, ownerName string, ownerAnnotati lbls[OrkestraServeTarget] = target } } + return lbls } diff --git a/pkg/resources/clusterrolebindings/clusterrolebinding.go b/pkg/resources/clusterrolebindings/clusterrolebinding.go index af212a433..e63db725a 100644 --- a/pkg/resources/clusterrolebindings/clusterrolebinding.go +++ b/pkg/resources/clusterrolebindings/clusterrolebinding.go @@ -190,7 +190,7 @@ func Resolve(src orktypes.ClusterRoleBindingTemplateSource, ownerName string) Re // ── Internal helpers ────────────────────────────────────────────────────────── func buildClusterRoleBinding(owner domain.Object, spec ResolvedClusterRoleBindingSpec) *rbacv1.ClusterRoleBinding { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) crb := &rbacv1.ClusterRoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: spec.Name, diff --git a/pkg/resources/clusterroles/clusterrole.go b/pkg/resources/clusterroles/clusterrole.go index d70f1f8e2..e36d650dd 100644 --- a/pkg/resources/clusterroles/clusterrole.go +++ b/pkg/resources/clusterroles/clusterrole.go @@ -171,7 +171,7 @@ func Resolve(src orktypes.ClusterRoleTemplateSource, ownerName string) ResolvedC // ── Internal helpers ────────────────────────────────────────────────────────── func buildClusterRole(owner domain.Object, spec ResolvedClusterRoleSpec) *rbacv1.ClusterRole { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) cr := &rbacv1.ClusterRole{ ObjectMeta: metav1.ObjectMeta{ Name: spec.Name, diff --git a/pkg/resources/configmaps/configmap.go b/pkg/resources/configmaps/configmap.go index 657e40e68..5c43368ff 100644 --- a/pkg/resources/configmaps/configmap.go +++ b/pkg/resources/configmaps/configmap.go @@ -330,7 +330,7 @@ func resolveData( } func buildConfigMap(owner domain.Object, spec ResolvedConfigMapSpec, namespace string, data map[string]string) *corev1.ConfigMap { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) return &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: spec.Name, diff --git a/pkg/resources/cronjobs/cronjob.go b/pkg/resources/cronjobs/cronjob.go index d12bc1964..3992f6aa3 100644 --- a/pkg/resources/cronjobs/cronjob.go +++ b/pkg/resources/cronjobs/cronjob.go @@ -294,7 +294,7 @@ func Resolve(src orktypes.CronJobTemplateSource, ownerName string, reg orktypes. // ── Internal helpers ────────────────────────────────────────────────────────── func buildCronJob(owner domain.Object, spec ResolvedCronJobSpec, namespace string) *batchv1.CronJob { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) cj := &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: spec.Name, diff --git a/pkg/resources/customresources/custom.go b/pkg/resources/customresources/custom.go index 7716deb3f..23ab1e4b4 100644 --- a/pkg/resources/customresources/custom.go +++ b/pkg/resources/customresources/custom.go @@ -348,7 +348,7 @@ func buildUnstructured(spec ResolvedCustomResourceSpec, owner domain.Object, gvk for k, v := range spec.Metadata.Labels { lbls[k] = v } - orklabels.StampOrkestraLabels(lbls, owner.GetName(), owner.GetAnnotations()) + lbls = orklabels.StampOrkestraLabels(lbls, owner.GetName(), owner.GetAnnotations()) u.SetLabels(lbls) // Annotations: copy for the same reason as Labels above. diff --git a/pkg/resources/deployments/deployment.go b/pkg/resources/deployments/deployment.go index 7c0548ef1..7ebc04c7d 100644 --- a/pkg/resources/deployments/deployment.go +++ b/pkg/resources/deployments/deployment.go @@ -226,7 +226,7 @@ func Resolve(src orktypes.DeploymentTemplateSource, ownerName string, reg orktyp // ── Internal helpers ────────────────────────────────────────────────────────── func buildDeployment(owner domain.Object, spec ResolvedDeploymentSpec, namespace string) *appsv1.Deployment { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) // Debug line logger.Debug(). Interface("env", spec.Env). diff --git a/pkg/resources/fixture/go.mod b/pkg/resources/fixture/go.mod index c6f323e7e..4aca1d066 100644 --- a/pkg/resources/fixture/go.mod +++ b/pkg/resources/fixture/go.mod @@ -1,6 +1,6 @@ module github.com/orkspace/orkestra-resource-probe -go 1.26.4 +go 1.26.6 replace github.com/orkspace/orkestra => ../../.. diff --git a/pkg/resources/hpas/hpa.go b/pkg/resources/hpas/hpa.go index 43e5eec58..f6c77da60 100644 --- a/pkg/resources/hpas/hpa.go +++ b/pkg/resources/hpas/hpa.go @@ -215,7 +215,7 @@ func Resolve(src orktypes.HPATemplateSource, ownerName string, reg orktypes.Prof // ── Internal helpers ────────────────────────────────────────────────────────── func buildHPA(owner domain.Object, spec ResolvedHPASpec, namespace string) *autoscalingv2.HorizontalPodAutoscaler { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) apiVersion := "" kind := "" if u, ok := owner.(*unstructured.Unstructured); ok { diff --git a/pkg/resources/ingresses/ingress.go b/pkg/resources/ingresses/ingress.go index 8a79ac025..d659f212a 100644 --- a/pkg/resources/ingresses/ingress.go +++ b/pkg/resources/ingresses/ingress.go @@ -210,7 +210,7 @@ func Resolve(src orktypes.IngressTemplateSource, ownerName string) ResolvedIngre // ── Internal helpers ────────────────────────────────────────────────────────── func buildIngress(owner domain.Object, spec ResolvedIngressSpec, namespace string) *networkingv1.Ingress { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) apiVersion := "" kind := "" if u, ok := owner.(*unstructured.Unstructured); ok { diff --git a/pkg/resources/jobs/job.go b/pkg/resources/jobs/job.go index 793020435..95b56ed14 100644 --- a/pkg/resources/jobs/job.go +++ b/pkg/resources/jobs/job.go @@ -181,7 +181,7 @@ func Resolve(src orktypes.JobTemplateSource, backoffLimit int, ownerName string, // ── Internal helpers ────────────────────────────────────────────────────────── func buildJob(owner domain.Object, spec ResolvedJobSpec, namespace string) *batchv1.Job { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) backoffLimit := int32(spec.BackoffLimit) container := corev1.Container{ diff --git a/pkg/resources/limitranges/limitrange.go b/pkg/resources/limitranges/limitrange.go index 8a240e148..d3c765271 100644 --- a/pkg/resources/limitranges/limitrange.go +++ b/pkg/resources/limitranges/limitrange.go @@ -288,7 +288,7 @@ func buildLimitRange( namespace string, limits []orktypes.LimitRangeItem, ) *corev1.LimitRange { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) return &corev1.LimitRange{ ObjectMeta: metav1.ObjectMeta{ Name: spec.Name, diff --git a/pkg/resources/namespaces/namespace.go b/pkg/resources/namespaces/namespace.go index 53830cdbc..c685b68a5 100644 --- a/pkg/resources/namespaces/namespace.go +++ b/pkg/resources/namespaces/namespace.go @@ -190,7 +190,7 @@ func Resolve(src orktypes.NamespaceTemplateSource, ownerName string) ResolvedNam // ── Internal helpers ────────────────────────────────────────────────────────── func buildNamespace(owner domain.Object, spec ResolvedNamespaceSpec) *corev1.Namespace { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) ns := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: spec.Name, diff --git a/pkg/resources/networkpolicies/networkpolicy.go b/pkg/resources/networkpolicies/networkpolicy.go index 3094a704c..21a777ee7 100644 --- a/pkg/resources/networkpolicies/networkpolicy.go +++ b/pkg/resources/networkpolicies/networkpolicy.go @@ -313,7 +313,7 @@ func buildNetworkPolicyFromSpec( namespace string, npSpec networkingv1.NetworkPolicySpec, ) *networkingv1.NetworkPolicy { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) return &networkingv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: spec.Name, diff --git a/pkg/resources/pdbs/pdb.go b/pkg/resources/pdbs/pdb.go index 3e7bde5ca..81811a144 100644 --- a/pkg/resources/pdbs/pdb.go +++ b/pkg/resources/pdbs/pdb.go @@ -207,7 +207,7 @@ func Resolve(src orktypes.PDBTemplateSource, ownerName string, reg orktypes.Prof // ── Internal helpers ────────────────────────────────────────────────────────── func buildPDB(owner domain.Object, spec ResolvedPDBSpec, namespace string) *policyv1.PodDisruptionBudget { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) apiVersion := "" kind := "" if u, ok := owner.(*unstructured.Unstructured); ok { diff --git a/pkg/resources/pods/pod.go b/pkg/resources/pods/pod.go index 2792ac7f6..7db9a213a 100644 --- a/pkg/resources/pods/pod.go +++ b/pkg/resources/pods/pod.go @@ -210,7 +210,7 @@ func Resolve(src orktypes.PodTemplateSource, ownerName string, reg orktypes.Prof // ── Internal helpers ────────────────────────────────────────────────────────── func buildPod(owner domain.Object, spec ResolvedPodSpec, namespace string) *corev1.Pod { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: spec.Name, diff --git a/pkg/resources/pvcs/pvc.go b/pkg/resources/pvcs/pvc.go index 36ab125d7..37519df1a 100644 --- a/pkg/resources/pvcs/pvc.go +++ b/pkg/resources/pvcs/pvc.go @@ -149,7 +149,7 @@ func Resolve(src orktypes.PVCTemplateSource, ownerName string) ResolvedPVCSpec { // ── Internal helpers ────────────────────────────────────────────────────────── func buildPVC(owner domain.Object, spec ResolvedPVCSpec, ns string) *corev1.PersistentVolumeClaim { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) apiVersion := "" kind := "" if u, ok := owner.(*unstructured.Unstructured); ok { diff --git a/pkg/resources/pvs/pv.go b/pkg/resources/pvs/pv.go index 6d1fc40a2..bc3a32672 100644 --- a/pkg/resources/pvs/pv.go +++ b/pkg/resources/pvs/pv.go @@ -141,7 +141,7 @@ func Resolve(src orktypes.PVTemplateSource, ownerName string) ResolvedPVSpec { // ── Internal helpers ────────────────────────────────────────────────────────── func buildPV(owner domain.Object, spec ResolvedPVSpec) *corev1.PersistentVolume { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) capacityQty := resource.MustParse(spec.Capacity) var accessModes []corev1.PersistentVolumeAccessMode diff --git a/pkg/resources/replicasets/replicaset.go b/pkg/resources/replicasets/replicaset.go index 8e7099c6e..fe74e1c97 100644 --- a/pkg/resources/replicasets/replicaset.go +++ b/pkg/resources/replicasets/replicaset.go @@ -219,7 +219,7 @@ func Resolve(src orktypes.ReplicaSetTemplateSource, ownerName string, reg orktyp // ── Internal helpers ────────────────────────────────────────────────────────── func buildReplicaSet(owner domain.Object, spec ResolvedReplicaSetSpec, namespace string) *appsv1.ReplicaSet { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) logger.Debug(). Interface("env", spec.Env). Interface("envFrom", spec.EnvFrom). diff --git a/pkg/resources/resourcequotas/resourcequota.go b/pkg/resources/resourcequotas/resourcequota.go index 191fafb8a..a32948437 100644 --- a/pkg/resources/resourcequotas/resourcequota.go +++ b/pkg/resources/resourcequotas/resourcequota.go @@ -295,7 +295,7 @@ func buildResourceQuota( namespace string, hard map[string]string, ) *corev1.ResourceQuota { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) return &corev1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{ Name: spec.Name, diff --git a/pkg/resources/rolebindings/rolebinding.go b/pkg/resources/rolebindings/rolebinding.go index 683b0b19e..dc99430d6 100644 --- a/pkg/resources/rolebindings/rolebinding.go +++ b/pkg/resources/rolebindings/rolebinding.go @@ -208,7 +208,7 @@ func Resolve(src orktypes.RoleBindingTemplateSource, ownerName string) ResolvedR // ── Internal helpers ────────────────────────────────────────────────────────── func buildRoleBinding(owner domain.Object, spec ResolvedRoleBindingSpec, namespace string) *rbacv1.RoleBinding { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) return &rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: spec.Name, diff --git a/pkg/resources/roles/role.go b/pkg/resources/roles/role.go index 1ece8ba59..9968eb33f 100644 --- a/pkg/resources/roles/role.go +++ b/pkg/resources/roles/role.go @@ -189,7 +189,7 @@ func Resolve(src orktypes.RoleTemplateSource, ownerName string) ResolvedRoleSpec // ── Internal helpers ────────────────────────────────────────────────────────── func buildRole(owner domain.Object, spec ResolvedRoleSpec, namespace string) *rbacv1.Role { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) return &rbacv1.Role{ ObjectMeta: metav1.ObjectMeta{ Name: spec.Name, diff --git a/pkg/resources/secrets/secret.go b/pkg/resources/secrets/secret.go index 711714b4d..7353f7a3c 100644 --- a/pkg/resources/secrets/secret.go +++ b/pkg/resources/secrets/secret.go @@ -337,7 +337,7 @@ func resolveData( } func buildSecret(owner domain.Object, spec ResolvedSecretSpec, namespace string, data map[string][]byte, stringData map[string]string) *corev1.Secret { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) secretType := corev1.SecretTypeOpaque switch strings.ToLower(spec.Type) { case "kubernetes.io/tls": diff --git a/pkg/resources/serviceaccounts/serviceaccount.go b/pkg/resources/serviceaccounts/serviceaccount.go index 22dfd4491..08f5672ea 100644 --- a/pkg/resources/serviceaccounts/serviceaccount.go +++ b/pkg/resources/serviceaccounts/serviceaccount.go @@ -153,7 +153,7 @@ func Resolve(src orktypes.ServiceAccountTemplateSource, ownerName string) Resolv // ── Internal helpers ────────────────────────────────────────────────────────── func buildServiceAccount(owner domain.Object, spec ResolvedServiceAccountSpec, namespace string) *corev1.ServiceAccount { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) return &corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: spec.Name, diff --git a/pkg/resources/services/services.go b/pkg/resources/services/services.go index 9d4fae990..995af270b 100644 --- a/pkg/resources/services/services.go +++ b/pkg/resources/services/services.go @@ -209,7 +209,7 @@ func Resolve(src orktypes.ServiceTemplateSource, ownerName string) ResolvedServi // ── Internal helpers ────────────────────────────────────────────────────────── func buildService(owner domain.Object, spec ResolvedServiceSpec, namespace string) *corev1.Service { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) svcType := corev1.ServiceTypeClusterIP switch spec.Type { case "NodePort": diff --git a/pkg/resources/statefulsets/statefulset.go b/pkg/resources/statefulsets/statefulset.go index b0ddbb1a6..d89d01559 100644 --- a/pkg/resources/statefulsets/statefulset.go +++ b/pkg/resources/statefulsets/statefulset.go @@ -231,7 +231,7 @@ func resolveAccessModes(modes []string) []corev1.PersistentVolumeAccessMode { } func buildStatefulSet(owner domain.Object, spec ResolvedStatefulSetSpec, ns string) *appsv1.StatefulSet { - labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) + spec.Labels = labels.StampOrkestraLabels(spec.Labels, owner.GetName(), owner.GetAnnotations()) apiVersion := "" kind := "" if u, ok := owner.(*unstructured.Unstructured); ok { diff --git a/pkg/tools/generate/registry_generator.go b/pkg/tools/generate/registry_generator.go index 124feb862..ece010465 100644 --- a/pkg/tools/generate/registry_generator.go +++ b/pkg/tools/generate/registry_generator.go @@ -37,10 +37,12 @@ import ( func TypeRegistry(crds map[string]orktypes.CRDEntry, dryRun bool) (bool, error) { var ( - imports []importEntry - entries []registryEntry // typed CRDs → ObjectRegistry + ListRegistry + RegisterTypedScheme - hookEntries []hookEntry // Go hooks → HookRegistry - recEntries []reconcilerEntry // custom constructors → ReconcilerRegistry + imports []importEntry + entries []registryEntry // typed CRDs → ObjectRegistry + ListRegistry + RegisterTypedScheme + hookEntries []hookEntry // Go hooks → HookRegistry + recEntries []reconcilerEntry // custom constructors → ReconcilerRegistry + targetHookEntries []targetHookEntry // per-target hooks → TargetHookRegistry + targetRecEntries []targetRecEntry // per-target constructors → TargetReconcilerRegistry seenObjectAliases = map[string]string{} seenHookAliases = map[string]string{} @@ -158,12 +160,89 @@ func TypeRegistry(crds map[string]orktypes.CRDEntry, dryRun bool) (bool, error) Kind: crd.APITypes.Kind, }) } + + // ── Per-target hooks ────────────────────────────────────────────────── + // A target that declares reconciler.hooks with a different location than + // the CRD-level hooks binary needs its own import and a TargetHookRegistry + // entry. Targets that only override args share the CRD-level binary and + // are handled at runtime by mergeReconcilerConfig in EffectiveOperatorBox. + if crd.Serve != nil && crd.Serve.Target.Entries != nil { + crdLevelHookLocation := "" + if crd.OperatorBox.Reconciler != nil && crd.OperatorBox.Reconciler.Hooks != nil { + crdLevelHookLocation = crd.OperatorBox.Reconciler.Hooks.Location + } + for targetName, targetCfg := range crd.Serve.Target.Entries { + if targetCfg.OperatorBox == nil || targetCfg.OperatorBox.Reconciler == nil { + continue + } + h := targetCfg.OperatorBox.Reconciler.Hooks + if h == nil || h.Location == "" || h.Location == crdLevelHookLocation { + continue + } + if err := validateHookEntry(h, crd.Name+" target "+targetName); err != nil { + return false, err + } + hookAlias := resolveAlias(h.Alias, crd.Name+"hooks", h.Location) + if err := dedupeImport(seenHookAliases, hookAlias, h.Location, crd.Name); err != nil { + return false, err + } + if _, seen := seenHookAliases[hookAlias]; !seen { + imports = append(imports, importEntry{Alias: hookAlias, Location: h.Location}) + seenHookAliases[hookAlias] = h.Location + } + targetHookEntries = append(targetHookEntries, targetHookEntry{ + Alias: hookAlias, + Function: h.Function, + Group: crd.APITypes.Group, + Version: crd.APITypes.Version, + Kind: crd.APITypes.Kind, + TargetName: targetName, + }) + } + } + + // ── Per-target constructors ─────────────────────────────────────────── + // A target that declares reconciler.default: false with its own constructor + // gets a TargetReconcilerRegistry entry so startCRDWorkers can build a + // MuxReconciler with the right sub-reconciler per target. + if crd.Serve != nil && crd.Serve.Target.Entries != nil { + for targetName, targetCfg := range crd.Serve.Target.Entries { + if targetCfg.OperatorBox == nil || targetCfg.OperatorBox.Reconciler == nil { + continue + } + rec := targetCfg.OperatorBox.Reconciler + if rec.Default == nil || *rec.Default || rec.ConstructorDecl == nil { + continue + } + c := rec.ConstructorDecl + if err := validateConstructorEntry(c, crd.Name+" target "+targetName); err != nil { + return false, err + } + recAlias := resolveAlias(c.Alias, crd.Name+"rec", c.Location) + if err := dedupeImport(seenRecAliases, recAlias, c.Location, crd.Name); err != nil { + return false, err + } + if _, seen := seenRecAliases[recAlias]; !seen { + imports = append(imports, importEntry{Alias: recAlias, Location: c.Location}) + seenRecAliases[recAlias] = c.Location + } + targetRecEntries = append(targetRecEntries, targetRecEntry{ + Alias: recAlias, + Function: c.Function, + Group: crd.APITypes.Group, + Version: crd.APITypes.Version, + Kind: crd.APITypes.Kind, + TargetName: targetName, + }) + } + } } // ── Nothing to generate ─────────────────────────────────────────────────── // Pure dynamic template Katalogs produce zero entries — this is correct. // GenericReconciler handles them at runtime. Exit cleanly, no file written. - if len(entries) == 0 && len(recEntries) == 0 && len(hookEntries) == 0 { + if len(entries) == 0 && len(recEntries) == 0 && len(hookEntries) == 0 && + len(targetHookEntries) == 0 && len(targetRecEntries) == 0 { return false, nil } @@ -174,8 +253,10 @@ func TypeRegistry(crds map[string]orktypes.CRDEntry, dryRun bool) (bool, error) Entries: entries, HookEntries: hookEntries, RecEntries: recEntries, - NeedsRecImports: len(recEntries) > 0, - NeedsHookImports: len(hookEntries) > 0, + TargetHookEntries: targetHookEntries, + TargetRecEntries: targetRecEntries, + NeedsRecImports: len(recEntries) > 0 || len(targetRecEntries) > 0, + NeedsHookImports: len(hookEntries) > 0 || len(targetHookEntries) > 0, NeedsSchemeImports: len(entries) > 0, } diff --git a/pkg/tools/generate/registry_template.go b/pkg/tools/generate/registry_template.go index 522fb4748..1d669c535 100644 --- a/pkg/tools/generate/registry_template.go +++ b/pkg/tools/generate/registry_template.go @@ -25,6 +25,16 @@ import "text/template" // Required when reconciler.default: false and reconciler.constructor is declared. // Same pattern as HookRegistry — external function, import + closure. // +// TargetHookRegistry +// Required when a serve.target entry declares reconciler.hooks with a different +// location than the CRD-level hooks binary. Keyed by GVK then target name. +// addTargetHooks() reads this at startup to set TargetHookFactories. +// +// TargetReconcilerRegistry +// Required when a serve.target entry declares reconciler.default: false with +// its own constructor. Keyed by GVK then target name. +// addTargetConstructors() reads this at startup to set TargetReconcilerFactories. +// // RegisterTypedScheme // Called by NewSchemeRegistry for typed CRDs. // Each compiled API type package exports AddToScheme. This calls them all. @@ -122,12 +132,40 @@ func RegisterRuntimeObjects() { func(kube kubeclient.Interface, inf cache.SharedIndexInformer, ev event.Recorder) domain.Reconciler { return {{ .Alias }}.{{ .Function }}(kube, inf, ev) } +{{ end }}{{ end }} +{{ if .TargetHookEntries }}{{ range .TargetHookEntries }} + // {{ .Kind }}/{{ .TargetName }} — per-target Go hook factory + // Distinct hook binary for this target; TargetHookFactories carries it into startCRDWorkers. + { + gvk := schema.GroupVersionKind{Group: "{{ .Group }}", Version: "{{ .Version }}", Kind: "{{ .Kind }}"} + if orktypes.TargetHookRegistry[gvk] == nil { + orktypes.TargetHookRegistry[gvk] = map[string]func() domain.AnyReconcileHooks{} + } + orktypes.TargetHookRegistry[gvk]["{{ .TargetName }}"] = func() domain.AnyReconcileHooks { + return {{ .Alias }}.{{ .Function }}() + } + } +{{ end }}{{ end }} +{{ if .TargetRecEntries }}{{ range .TargetRecEntries }} + // {{ .Kind }}/{{ .TargetName }} — per-target custom reconciler constructor + // Distinct reconciler for this target; TargetReconcilerFactories carries it into startCRDWorkers. + { + gvk := schema.GroupVersionKind{Group: "{{ .Group }}", Version: "{{ .Version }}", Kind: "{{ .Kind }}"} + if orktypes.TargetReconcilerRegistry[gvk] == nil { + orktypes.TargetReconcilerRegistry[gvk] = map[string]orktypes.NewReconcilerFunc{} + } + orktypes.TargetReconcilerRegistry[gvk]["{{ .TargetName }}"] = func(kube kubeclient.Interface, inf cache.SharedIndexInformer, ev event.Recorder) domain.Reconciler { + return {{ .Alias }}.{{ .Function }}(kube, inf, ev) + } + } {{ end }}{{ end }} logger.Debug(). Int("objectRegistrySize", len(orktypes.ObjectRegistry)). Int("listRegistrySize", len(orktypes.ListRegistry)). {{ if .HookEntries }}Int("hookRegistrySize", len(orktypes.HookRegistry)).{{ end }} {{ if .RecEntries }}Int("reconcilerRegistrySize", len(orktypes.ReconcilerRegistry)).{{ end }} + {{ if .TargetHookEntries }}Int("targetHookRegistrySize", len(orktypes.TargetHookRegistry)).{{ end }} + {{ if .TargetRecEntries }}Int("targetRecRegistrySize", len(orktypes.TargetReconcilerRegistry)).{{ end }} Msg("Runtime objects registered") } `)) diff --git a/pkg/tools/generate/type.go b/pkg/tools/generate/type.go index 12296161f..300de15a9 100644 --- a/pkg/tools/generate/type.go +++ b/pkg/tools/generate/type.go @@ -27,8 +27,10 @@ type registryTemplateData struct { // SchemeEntries []registryEntry HookEntries []hookEntry RecEntries []reconcilerEntry - NeedsRecImports bool // true when RecEntries is non-empty - NeedsHookImports bool // true when HookEntries is non-empty + TargetHookEntries []targetHookEntry + TargetRecEntries []targetRecEntry + NeedsRecImports bool // true when RecEntries or TargetRecEntries is non-empty + NeedsHookImports bool // true when HookEntries or TargetHookEntries is non-empty NeedsSchemeImports bool // true when Entries is non-empty (metav1.AddToGroupVersion needed) } @@ -62,6 +64,27 @@ type reconcilerEntry struct { Kind string } +// targetHookEntry represents one TargetHookRegistry assignment in the generated file. +// TargetName is the serve.target map key that declares the distinct hook binary. +type targetHookEntry struct { + Alias string + Function string + Group string + Version string + Kind string + TargetName string +} + +// targetRecEntry represents one TargetReconcilerRegistry assignment in the generated file. +type targetRecEntry struct { + Alias string + Function string + Group string + Version string + Kind string + TargetName string +} + // Docs and dashboards type CRDMeta struct { Name string diff --git a/pkg/types/types.go b/pkg/types/types.go index 3efea4ac7..1e5b2edfd 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -21,6 +21,18 @@ var ListRegistry = map[schema.GroupVersionKind]func() runtime.Object{} var HookRegistry = map[schema.GroupVersionKind]func() domain.AnyReconcileHooks{} var ReconcilerRegistry = map[schema.GroupVersionKind]NewReconcilerFunc{} +// TargetHookRegistry and TargetReconcilerRegistry hold per-target factories for +// CRDs whose targets declare a distinct hook binary or custom constructor. +// Outer key: GVK. Inner key: target name (matches serve.target.). +// Populated by the generator alongside HookRegistry / ReconcilerRegistry and +// consumed by addTargetHooks() / addTargetConstructors() during Katalog validation. +// +// Targets that share the CRD-level binary (only overriding hooks.args) do NOT +// appear here — mergeReconcilerConfig in EffectiveOperatorBox handles them at +// reconcile time without any separate registration. +var TargetHookRegistry = map[schema.GroupVersionKind]map[string]func() domain.AnyReconcileHooks{} +var TargetReconcilerRegistry = map[schema.GroupVersionKind]map[string]NewReconcilerFunc{} + // SchemeAdderFns holds AddToScheme functions collected from generated init() // calls. Each generated zz_generated_runtime_registry.go appends to this slice // in its init(); NewSchemeRegistry drains it via RegisterTypedScheme. diff --git a/pkg/types/types_crd_entry.go b/pkg/types/types_crd_entry.go index b019198f4..31622273d 100644 --- a/pkg/types/types_crd_entry.go +++ b/pkg/types/types_crd_entry.go @@ -4,6 +4,7 @@ package types import ( "sort" + "github.com/orkspace/orkestra/domain" "github.com/orkspace/orkestra/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -255,6 +256,17 @@ type CRDEntry struct { // NotificationEnabled returns whether this CRD belongs to katalog with notification access NotificationEnabled *bool `yaml:"-" json:"-"` + // TargetHookFactories — per-target hook factories, keyed by target name. + // Populated by addTargetHooks() from TargetHookRegistry. + // Only set for targets that declare a distinct hook binary from the CRD-level. + // Targets that share the CRD-level binary (only overriding args) are absent — + // mergeReconcilerConfig handles them at reconcile time via EffectiveOperatorBox. + TargetHookFactories map[string]func() domain.AnyReconcileHooks `yaml:"-" json:"-"` + + // TargetReconcilerFactories — per-target constructor factories, keyed by target name. + // Populated by addTargetConstructors() from TargetReconcilerRegistry. + TargetReconcilerFactories map[string]NewReconcilerFunc `yaml:"-" json:"-"` + // RemoveFinalizers -> testing RemoveFinalizers bool `yaml:"removeFinalizers,omitempty" json:"removeFinalizers,omitempty"` @@ -283,6 +295,12 @@ type CRDEntry struct { // (preReconcile, status) fall back to the CRD-level values when absent // on the target — so a CRD-level gate or status config applies to all // surfaces unless a target explicitly overrides it. +// +// Reconciler merge: target args override CRD-level args, but identity fields +// (location, function, alias, resources) and tuning fields (workers, resync, +// queue) always come from the CRD-level when the target omits them. +// HookFactory is runtime-registered on the CRD-level box only and is always +// propagated so the hook binary is reachable from every target surface. func (c *CRDEntry) EffectiveOperatorBox(target string) *OperatorBoxConfig { if target == "" { return &c.OperatorBox @@ -296,15 +314,63 @@ func (c *CRDEntry) EffectiveOperatorBox(target string) *OperatorBoxConfig { if box.Status == nil { box.Status = c.OperatorBox.Status } - if box.Reconciler == nil { - box.Reconciler = c.OperatorBox.Reconciler - } + box.Reconciler = mergeReconcilerConfig(c.OperatorBox.Reconciler, box.Reconciler) + // HookFactory is set at load time on the CRD-level box only. + box.HookFactory = c.OperatorBox.HookFactory return &box } } return &c.OperatorBox } +// mergeReconcilerConfig merges a per-target reconciler on top of the CRD-level one. +// Workers, resync, queue, and profile are always taken from the CRD-level (they stay +// fixed). Hook identity fields (location, function, alias, resources) come from the +// CRD-level when the target omits them. Args are merged key-by-key with target winning. +func mergeReconcilerConfig(base, target *ReconcilerConfig) *ReconcilerConfig { + if target == nil { + return base + } + if base == nil { + return target + } + // Start from base so workers/resync/queue/profile/default/constructor are inherited. + merged := *base + if target.Hooks != nil { + if merged.Hooks == nil { + merged.Hooks = target.Hooks + } else { + h := *merged.Hooks + // Identity fields: target wins only when explicitly set. + if target.Hooks.Location != "" { + h.Location = target.Hooks.Location + } + if target.Hooks.Function != "" { + h.Function = target.Hooks.Function + } + if target.Hooks.Alias != "" { + h.Alias = target.Hooks.Alias + } + if len(target.Hooks.Resources) > 0 { + h.Resources = target.Hooks.Resources + } + // Args: merge key-by-key; target overrides CRD-level per key. + if len(target.Hooks.Args) > 0 { + args := make(map[string]interface{}, len(h.Args)+len(target.Hooks.Args)) + for k, v := range h.Args { + args[k] = v + } + for k, v := range target.Hooks.Args { + args[k] = v + } + h.Args = args + } + merged.Hooks = &h + } + } + return &merged +} + // ResolveTargetFromAnnotations extracts the effective target from a CR's annotations. // Resolution order: // 1. serve-alias annotation (most specific) @@ -597,6 +663,13 @@ func (c *CRDEntry) HasServeAliases() bool { return len(c.ServeAliases()) > 0 } +// HasServeTargetEntries reports whether any named target entries are declared +// under serve.target. Used by addTargetHooks and addTargetConstructors to skip +// CRDs that have no per-target operatorBox declarations. +func (c *CRDEntry) HasServeTargetEntries() bool { + return c.ServeEnabled() && c.Serve.Target.Entries != nil +} + // AliasNames returns a sorted slice of alias names for this CRD, or nil if none. func (c *CRDEntry) AliasNames() []string { aliases := c.ServeAliases() diff --git a/pkg/types/types_operatorbox.go b/pkg/types/types_operatorbox.go index 4c2eff172..829a8c944 100644 --- a/pkg/types/types_operatorbox.go +++ b/pkg/types/types_operatorbox.go @@ -310,6 +310,7 @@ func (box *OperatorBoxConfig) IsEmpty() bool { return box == nil } + // HookDeclaration declares where a Go hook function lives. // Read by ork generate to emit HookRegistry entries in zz_generated_runtime_registry.go. // The declared function must match the signature: func() domain.AnyReconcileHooks From b10ca9e92b7e77a1e3b3c515ecd306a80c489af3 Mon Sep 17 00:00:00 2001 From: ialexeze Date: Mon, 17 Aug 2026 14:27:43 +0000 Subject: [PATCH 2/4] feat(target-operatorbox): MuxReconciler dispatch, intent package, fixture, and override flag - Move target resolution and CR construction to pkg/intent/target/ - Add MuxReconciler that dispatches per-target constructors, falling back to GenericReconciler - Wire MuxReconciler in runtime_konstructor when HasTargetConstructorFactories() - Add TargetConstructorArgs and HasTargetConstructorFactories accessors on CRDEntry - Fix addReconcilers to skip per-target entries owned by addTargetConstructors - Add --override flag to ork serve apply for routing surface conflict override - Extend 03-hooks-targets fixture with v2-ctor target and constructor - Add issues.md noting surface cleanup gap for hook-managed resources --- cmd/cli/play_chain.go | 5 +- cmd/cli/serve_apply.go | 14 +- cmd/internal/runtime_konstructor.go | 25 +++ pkg/gateway/api/apply.go | 3 +- pkg/gateway/api/apply_target.go | 3 +- pkg/gateway/api/helper.go | 2 + pkg/gateway/api/target_test.go | 119 -------------- pkg/gateway/api/test_exports.go | 4 - pkg/intent/README.md | 29 ++++ pkg/intent/target/README.md | 34 ++++ pkg/intent/target/helper.go | 15 ++ pkg/intent/target/mux.go | 146 ++++++++++++++++++ pkg/intent/target/resolve.go | 27 ++++ pkg/{gateway/api => intent/target}/target.go | 16 +- .../target/target_test.go} | 111 ++++++++++++- pkg/katalog/pre_reconcile.go | 9 +- pkg/katalog/type.go | 8 + pkg/katalog/validate_hooks_reconcilers.go | 25 +-- .../fixture/03-hooks-targets/README.md | 76 +++++++-- .../03-hooks-targets/cmd/orkestra/main.go | 30 ++-- .../blockchainappwithtargets_reconciler.go | 96 ++++++++++++ .../fixture/03-hooks-targets/go.mod | 2 +- .../intent/intent-v2-ctor.json | 8 + .../intent/intent-v2-disabled.json | 8 + .../intent/intent-v2-disabled.yaml | 6 - .../intent/intent-v2-enabled.yaml | 2 +- .../fixture/03-hooks-targets/katalog.yaml | 29 ++-- .../typeregistry/zz_generated_typeregistry.go | 46 +++++- pkg/registry/simulate/helper.go | 3 +- .../kordinator/dependency_kordinator.go | 2 + pkg/runtime/reconciler/generic.go | 59 +++---- pkg/runtime/reconciler/generic_target.go | 50 ++++++ pkg/runtime/reconciler/run_surface_cleanup.go | 4 +- pkg/runtime/runners/docs/issues.md | 13 ++ pkg/types/methods.go | 47 ++++++ pkg/types/types.go | 18 +++ pkg/types/types_crd_entry.go | 24 --- pkg/types/types_operatorbox.go | 58 +++++++ 38 files changed, 908 insertions(+), 268 deletions(-) delete mode 100644 pkg/gateway/api/target_test.go create mode 100644 pkg/intent/README.md create mode 100644 pkg/intent/target/README.md create mode 100644 pkg/intent/target/helper.go create mode 100644 pkg/intent/target/mux.go create mode 100644 pkg/intent/target/resolve.go rename pkg/{gateway/api => intent/target}/target.go (96%) rename pkg/{gateway/api/target_helpers_test.go => intent/target/target_test.go} (76%) create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/constructor/blockchainappwithtargets_reconciler.go create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-ctor.json create mode 100644 pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-disabled.json delete mode 100644 pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-disabled.yaml create mode 100644 pkg/runtime/reconciler/generic_target.go create mode 100644 pkg/runtime/runners/docs/issues.md diff --git a/cmd/cli/play_chain.go b/cmd/cli/play_chain.go index a4bf75f05..aa6dd8595 100644 --- a/cmd/cli/play_chain.go +++ b/cmd/cli/play_chain.go @@ -14,6 +14,7 @@ import ( "github.com/orkspace/orkestra/pkg/katalog" "github.com/orkspace/orkestra/pkg/merger" "github.com/orkspace/orkestra/pkg/registry/simulate" + orktarget "github.com/orkspace/orkestra/pkg/intent/target" orktmpl "github.com/orkspace/orkestra/pkg/resources/template" orktypes "github.com/orkspace/orkestra/pkg/types" "gopkg.in/yaml.v3" @@ -62,7 +63,7 @@ func runCreateUpdateChain(k *katalog.Katalog, raw map[string]interface{}, tokenN // Stage 3: CR construction printStage(3, "CR construction") notes := k.Notes - obj, err := api.BuildCRFromTarget(raw, crd, notes) + obj, err := orktarget.BuildCRFromTarget(raw, crd, notes) if err != nil { printStageError(err.Error()) return nil, nil, "", err @@ -300,7 +301,7 @@ func runIntentPlay(katalogPath, intentFile string) (string, error) { return target, fmt.Errorf("intent file must declare a 'token' — token: ") } - obj, err := api.BuildCRFromTarget(raw, crd, k.Notes) + obj, err := orktarget.BuildCRFromTarget(raw, crd, k.Notes) if err != nil { return target, fmt.Errorf("CR construction: %w", err) } diff --git a/cmd/cli/serve_apply.go b/cmd/cli/serve_apply.go index da22e60e8..77e93b879 100644 --- a/cmd/cli/serve_apply.go +++ b/cmd/cli/serve_apply.go @@ -34,13 +34,17 @@ Example (explicit file): ork serve apply -f intent.yaml --api https://gateway.myorg.io --token "$ORK_TOKEN" Example (dry run — no CR applied): - ork serve apply -f cr.yaml --api https://gateway.myorg.io --token "$ORK_TOKEN" --dry-run`, + ork serve apply -f cr.yaml --api https://gateway.myorg.io --token "$ORK_TOKEN" --dry-run + +Example (override routing surface conflict): + ork serve apply -f intent.yaml --api https://gateway.myorg.io --token "$ORK_TOKEN" --override`, SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { file, _ := cmd.Flags().GetString("file") apiURL, _ := cmd.Flags().GetString("api") token, _ := cmd.Flags().GetString("token") dryRun, _ := cmd.Flags().GetBool("dry-run") + override, _ := cmd.Flags().GetBool("override") if file == "" { file = resolveDefaultIntentFile() @@ -60,8 +64,13 @@ Example (dry run — no CR applied): } endpoint := strings.TrimRight(apiURL, "/") + "/api/v1/apply" - if dryRun { + switch { + case dryRun && override: + endpoint += "?dryRun=true&override=true" + case dryRun: endpoint += "?dryRun=true" + case override: + endpoint += "?override=true" } req, err := http.NewRequestWithContext(cmd.Context(), http.MethodPost, endpoint, bytes.NewReader(body)) @@ -154,6 +163,7 @@ func init() { serveApplyCmd.Flags().StringP("api", "a", "http://localhost:8080", "Gateway base URL") serveApplyCmd.Flags().StringP("token", "t", "", "Bearer token for the gateway") serveApplyCmd.Flags().Bool("dry-run", false, "Preview without applying — the CR is not written to the cluster") + serveApplyCmd.Flags().Bool("override", false, "Override routing surface conflict — allows switching a resource to a different target") _ = serveApplyCmd.MarkFlagRequired("token") diff --git a/cmd/internal/runtime_konstructor.go b/cmd/internal/runtime_konstructor.go index 7d5a4b086..cec908787 100644 --- a/cmd/internal/runtime_konstructor.go +++ b/cmd/internal/runtime_konstructor.go @@ -105,6 +105,7 @@ import ( "github.com/orkspace/orkestra/domain" "github.com/orkspace/orkestra/pkg/event" "github.com/orkspace/orkestra/pkg/health" + orktarget "github.com/orkspace/orkestra/pkg/intent/target" "github.com/orkspace/orkestra/pkg/katalog" "github.com/orkspace/orkestra/pkg/konfig" "github.com/orkspace/orkestra/pkg/kubeclient" @@ -431,6 +432,30 @@ func konstructRuntime(kfg *konfig.Konfig, m *merger.Merger, ctx context.Context) } } + // Wrap with MuxReconciler when per-target constructors are declared. + // MuxReconciler dispatches each reconcile cycle to the matching target's + // domain.Reconciler, falling back to the base factory for CRs with no + // annotation or an unrecognised target name. + if crd.HasTargetConstructorFactories() { + baseFactory := factory + crdCopy := crd + factory = func() domain.Reconciler { + targets := make(map[string]domain.Reconciler, len(crdCopy.TargetReconcilerFactories)) + for targetName, ctor := range crdCopy.TargetReconcilerFactories { + var targetKube kubeclient.Interface = kube + if args := crdCopy.TargetConstructorArgs(targetName); len(args) > 0 { + targetKube = kube.WithArgs(kubeclient.Args(args)) + } + targets[targetName] = ctor(targetKube, infCopy, ev) + } + return orktarget.NewMuxReconciler(infCopy, targets, baseFactory()) + } + logger.Debug(). + Str("gvk", gvk). + Int("targets", len(crd.TargetReconcilerFactories)). + Msg("wiring MuxReconciler factory") + } + // Register informs the DependencyKordinator which informer and factory // belong to this CRD. Workers are not started yet — that happens in Start(). ktrlRegistry.Register(gvk, crd, inf, factory) diff --git a/pkg/gateway/api/apply.go b/pkg/gateway/api/apply.go index ab081f91c..6ef4dde50 100644 --- a/pkg/gateway/api/apply.go +++ b/pkg/gateway/api/apply.go @@ -31,6 +31,7 @@ import ( "github.com/orkspace/orkestra/pkg/kubeclient" "github.com/orkspace/orkestra/pkg/labels" "github.com/orkspace/orkestra/pkg/logger" + orktarget "github.com/orkspace/orkestra/pkg/intent/target" orktmpl "github.com/orkspace/orkestra/pkg/resources/template" orktypes "github.com/orkspace/orkestra/pkg/types" ) @@ -223,7 +224,7 @@ func applyHandler( return } - built, err := BuildCRFromTarget(raw, crd, notes) + built, err := orktarget.BuildCRFromTarget(raw, crd, notes) if err != nil { writeJSON(w, http.StatusBadRequest, ApplyResponse{ Message: err.Error(), diff --git a/pkg/gateway/api/apply_target.go b/pkg/gateway/api/apply_target.go index 0ccbca3c7..ff8efb734 100644 --- a/pkg/gateway/api/apply_target.go +++ b/pkg/gateway/api/apply_target.go @@ -15,6 +15,7 @@ import ( "github.com/orkspace/orkestra/pkg/kubeclient" "github.com/orkspace/orkestra/pkg/labels" "github.com/orkspace/orkestra/pkg/logger" + orktarget "github.com/orkspace/orkestra/pkg/intent/target" orktmpl "github.com/orkspace/orkestra/pkg/resources/template" orktypes "github.com/orkspace/orkestra/pkg/types" ) @@ -66,7 +67,7 @@ func ApplyTargetFields( return &ApplyResponse{Message: clusterErr.Error()}, http.StatusBadRequest } - obj, err := BuildCRFromTarget(fields, crd, notes) + obj, err := orktarget.BuildCRFromTarget(fields, crd, notes) if err != nil { return &ApplyResponse{Message: err.Error()}, http.StatusBadRequest } diff --git a/pkg/gateway/api/helper.go b/pkg/gateway/api/helper.go index 0a31da2b6..9e97f1d86 100644 --- a/pkg/gateway/api/helper.go +++ b/pkg/gateway/api/helper.go @@ -7,6 +7,7 @@ import ( "github.com/orkspace/orkestra/pkg/kubeclient" "github.com/orkspace/orkestra/pkg/logger" + orktarget "github.com/orkspace/orkestra/pkg/intent/target" orktmpl "github.com/orkspace/orkestra/pkg/resources/template" orktypes "github.com/orkspace/orkestra/pkg/types" "github.com/orkspace/orkestra/pkg/utils" @@ -41,6 +42,7 @@ var ( nestedSlice = utils.NestedSlice nestedMap = utils.NestedMap deleteNestedPath = utils.DeleteNestedPath + isTargetRequest = orktarget.IsTargetRequest ) // resolvePollURL builds the poll URL for the Gateway API response. diff --git a/pkg/gateway/api/target_test.go b/pkg/gateway/api/target_test.go deleted file mode 100644 index 4c2b9de39..000000000 --- a/pkg/gateway/api/target_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package api - -import ( - "testing" - - orktypes "github.com/orkspace/orkestra/pkg/types" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -func TestIsTargetRequest(t *testing.T) { - assert.True(t, IsTargetRequest(map[string]interface{}{ - "target": "app", - })) - // target wins even when apiVersion is also present (gradual migration path) - assert.True(t, IsTargetRequest(map[string]interface{}{ - "target": "app", - "apiVersion": "v1", - })) - assert.False(t, IsTargetRequest(map[string]interface{}{ - "apiVersion": "platform.myorg.io/v1", - "kind": "App", - })) - assert.False(t, IsTargetRequest(map[string]interface{}{})) -} - -func TestBuildCRFromTarget(t *testing.T) { - appCRD := &orktypes.CRDEntry{ - APITypes: orktypes.APITypes{ - Group: "platform.myorg.io", - Version: "v1", - Kind: "App", - Plural: "apps", - }, - GroupVersionKind: schema.GroupVersionKind{ - Group: "platform.myorg.io", Version: "v1", Kind: "App", - }, - Serve: &orktypes.ServeConfig{ - Target: orktypes.ServeTargetValue{Entries: map[string]*orktypes.ServeTargetConfig{ - "app": {Primary: true}, - }}, - Name: `{{ .repository | repoSlug }}`, - Namespace: `{{ .team }}-{{ .environment }}`, - Fields: map[string]orktypes.ServeFieldConfig{ - "repository": {}, - "image": {}, - "environment": {}, - "replicas": {}, - }, - Labels: map[string]orktypes.ServeFieldConfig{ - "team": {}, - }, - Annotations: map[string]orktypes.ServeFieldConfig{ - "jira-ticket": {}, - }, - }, - } - - t.Run("spec fields routed correctly", func(t *testing.T) { - raw := map[string]interface{}{ - "target": "app", - "repository": "myorg/payments-api", - "image": "ghcr.io/myorg/payments-api:v1", - "environment": "staging", - "replicas": float64(2), - "team": "payments", - "jira-ticket": "PLAT-1234", - } - - obj, err := BuildCRFromTarget(raw, appCRD, orktypes.NoteRegistry{}) - require.NoError(t, err) - - spec := obj.Object["spec"].(map[string]interface{}) - assert.Equal(t, "myorg/payments-api", spec["repository"]) - assert.Equal(t, "ghcr.io/myorg/payments-api:v1", spec["image"]) - assert.Equal(t, "staging", spec["environment"]) - assert.Equal(t, float64(2), spec["replicas"]) - - labels := obj.Object["metadata"].(map[string]interface{})["labels"].(map[string]interface{}) - assert.Equal(t, "payments", labels["team"]) - - annotations := obj.Object["metadata"].(map[string]interface{})["annotations"].(map[string]interface{}) - assert.Equal(t, "PLAT-1234", annotations["jira-ticket"]) - - // team and jira-ticket must NOT be in spec. - assert.Nil(t, spec["team"]) - assert.Nil(t, spec["jira-ticket"]) - }) - - t.Run("unknown fields ignored", func(t *testing.T) { - raw := map[string]interface{}{ - "target": "app", - "repository": "myorg/payments-api", - "team": "payments", - "environment": "staging", - "unknown-field": "should be ignored", - } - obj, err := BuildCRFromTarget(raw, appCRD, orktypes.NoteRegistry{}) - require.NoError(t, err) - - spec := obj.Object["spec"].(map[string]interface{}) - _, exists := spec["unknown-field"] - assert.False(t, exists) - }) - - t.Run("apiVersion and kind set from CRD entry", func(t *testing.T) { - raw := map[string]interface{}{ - "target": "app", - "repository": "myorg/payments-api", - "team": "payments", - "environment": "staging", - } - obj, err := BuildCRFromTarget(raw, appCRD, orktypes.NoteRegistry{}) - require.NoError(t, err) - assert.Equal(t, "platform.myorg.io/v1", obj.GetAPIVersion()) - assert.Equal(t, "App", obj.GetKind()) - }) -} diff --git a/pkg/gateway/api/test_exports.go b/pkg/gateway/api/test_exports.go index 196558067..32baa9fc7 100644 --- a/pkg/gateway/api/test_exports.go +++ b/pkg/gateway/api/test_exports.go @@ -17,10 +17,6 @@ func ParsePath(path string) (kind, ns, name string, err error) { return parsePath(path) } -func IsTargetRequest(raw map[string]interface{}) bool { - return isTargetRequest(raw) -} - func ExportedSchemaHandler(kat *katalog.Katalog) http.Handler { return schemaHandler(kat) } diff --git a/pkg/intent/README.md b/pkg/intent/README.md new file mode 100644 index 000000000..93370f15d --- /dev/null +++ b/pkg/intent/README.md @@ -0,0 +1,29 @@ +# pkg/intent + +`intent` is the experimental layer where the runtime meets the intent model. + +The gateway proved that operators can receive intent — a flat, human-vocabulary payload with no `apiVersion`, no `kind`, no `spec` — and translate it into a Kubernetes CR without callers ever seeing the manifest. `intent` explores what the runtime can do with that, once the gateway hands a CR off for reconciliation. + +The questions driving this layer: + +- **How far can intent travel?** The gateway stamps provenance; can the runtime route, dispatch, and gate on it without losing the original context? +- **What does per-target mean for the runtime?** The gateway resolves a target surface from the caller's vocabulary; what does the reconciler do differently for each surface? +- **Can the manifest stay an implementation detail end-to-end?** From intent → CR → reconciler, without the caller or the operator code needing to understand Kubernetes structure? + +This is not a finished answer — it is an active investigation. Code here reflects what has been learned so far. APIs may change. + +## Open questions + +**Where does intent live between delivery and replay?** + +Intent can arrive from anywhere — a CI pipeline, a Slack command, a browser form, a cron job. Unlike GitOps, where the manifest lives in a Git repository and can be replayed by re-applying the commit, intent has no durable store today. The CR carries provenance annotations (target, alias, source identity), but the CR is already the translation result — the original flat payload is gone. + +One direction: an intent registry backed by OCI. The gateway is the sole writer — it records the raw intent payload alongside the provenance it stamps on the CR, as an OCI artifact with matching annotations. The content address (SHA of the payload) gives natural deduplication. `ork serve replay --registry` reads from the store and re-runs each intent through the *current* gateway — so replay re-derives the CR from today's field translations, not the schema that existed when the intent was first delivered. Schema evolution becomes transparent to replay. + +This is qualitatively different from replaying a manifest from Git. Git replay re-applies what the cluster received then. Intent replay re-derives what the cluster should receive now. + +`pkg/registry` already uses OCI for operator pattern distribution. The annotation model it uses maps directly onto what intent provenance needs. + +| Sub-package | Responsibility | +|-------------|----------------| +| [target/](target/README.md) | Target resolution, intent-to-CR translation, per-target reconciler dispatch | diff --git a/pkg/intent/target/README.md b/pkg/intent/target/README.md new file mode 100644 index 000000000..998b05566 --- /dev/null +++ b/pkg/intent/target/README.md @@ -0,0 +1,34 @@ +# pkg/intent/target + +`target` is the runtime face of Orkestra's intent model. It sits between the gateway (where intent arrives) and the reconciler (where the CR is processed), owning the three responsibilities that make per-target dispatch work. + +## What lives here + +**Target resolution** — reads the serve-target and serve-alias annotations from a CR and returns the effective target name. Resolution order: alias (most specific) → target → empty string. Every reconcile cycle starts here so the right operatorBox and the right hooks are selected. + +**Intent-to-CR translation** — takes a flat intent payload (`{"target": "app", "repository": "...", "replicas": 2}`) and builds a full `Unstructured` CR from it. Routes each field to its declared destination: `serve.fields` → `spec.*`, `serve.labels` → `metadata.labels`, `serve.annotations` → `metadata.annotations`. Resolves `serve.name` and `serve.namespace` from the same payload via template expressions. Unknown fields are silently ignored — the caller's vocabulary and the CRD's structure don't have to match. + +**MuxReconciler** — dispatches `Reconcile(ctx, key)` to the right `domain.Reconciler` based on the CR's target annotation. Targets with a registered constructor (via `TargetReconcilerRegistry`) get their own reconciler instance. CRs with no annotation, or an unknown target, fall through to the CRD-level reconciler. Deletion cycles route to the reconciler that handled the last create (tracked in a `sync.Map`). All CRD-level infrastructure (queue injection, autoscale, resync, rollback notifiers, metrics) is forwarded to the fallback. + +## How the pieces connect + +``` +Gateway API (intent payload) + │ + ▼ +BuildCRFromTarget — flat fields → Unstructured CR + │ + ▼ (kubectl apply / SSA) + CR lands in cluster with orkestra.io/serve-target annotation + │ + ▼ +MuxReconciler.Reconcile + │ + ├── ResolveTargetFromAnnotations → "v2-ctor" + │ + ├── targets["v2-ctor"] → per-target domain.Reconciler + │ + └── fallback → CRD-level GenericReconciler +``` + +`MuxReconciler` is only wired when `CRDEntry.HasTargetConstructorFactories()` returns true — CRDs with only per-target hooks stay on `GenericReconciler`, which handles hook dispatch in `hooksFor()`. diff --git a/pkg/intent/target/helper.go b/pkg/intent/target/helper.go new file mode 100644 index 000000000..c215b1efa --- /dev/null +++ b/pkg/intent/target/helper.go @@ -0,0 +1,15 @@ +package target + +import "github.com/orkspace/orkestra/pkg/utils" + +var ( + validateK8sName = utils.ValidKubernetesName + isNestedPath = utils.IsNestedPath + setNestedPath = utils.SetNestedPath +) + +// mapContains is a nil-safe map membership check. +// Wraps utils.MapContains for local convenience. +func mapContains[V any](m map[string]V, key string) bool { + return utils.MapContains(m, key) +} \ No newline at end of file diff --git a/pkg/intent/target/mux.go b/pkg/intent/target/mux.go new file mode 100644 index 000000000..72ac8edcd --- /dev/null +++ b/pkg/intent/target/mux.go @@ -0,0 +1,146 @@ +package target + +import ( + "context" + "fmt" + "sync" + + "github.com/orkspace/orkestra/domain" + "github.com/orkspace/orkestra/pkg/runtime/autoscaler" + orkqueue "github.com/orkspace/orkestra/pkg/runtime/queue" + "k8s.io/client-go/tools/cache" +) + +// MuxReconciler dispatches Reconcile calls to per-target domain.Reconciler +// instances based on the serve-target annotation on the incoming CR. +// +// CRs with no target annotation (or an unknown target) are handled by the +// fallback reconciler — typically the CRD-level GenericReconciler. +// +// All CRD-level infrastructure concerns (queue injection, autoscale, resync, +// rollback notifiers, metrics) are forwarded to the fallback reconciler so +// startCRDWorkers can inject them via the same interface checks it uses for +// a plain GenericReconciler. +// +// The target cache is the only mutable state: it stores "ns/name" → target +// so that deletion reconcile cycles (where the object is gone and no annotation +// can be read) still route to the same reconciler that handled the last create. +type MuxReconciler struct { + informer cache.SharedIndexInformer + targets map[string]domain.Reconciler // target name → reconciler + fallback domain.Reconciler // handles no-target / unknown-target CRs + targetCache sync.Map // "ns/name" → string; evicted on reconcile-not-found +} + +func NewMuxReconciler( + informer cache.SharedIndexInformer, + targets map[string]domain.Reconciler, + fallback domain.Reconciler, +) *MuxReconciler { + return &MuxReconciler{ + informer: informer, + targets: targets, + fallback: fallback, + } +} + +var _ domain.Reconciler = (*MuxReconciler)(nil) + +// Reconcile looks up the CR by key, resolves its target, and delegates to the +// matching per-target reconciler (or the fallback when no match is found). +func (m *MuxReconciler) Reconcile(ctx context.Context, key string) error { + raw, exists, err := m.informer.GetIndexer().GetByKey(key) + if err != nil { + return fmt.Errorf("mux: getting %q from store: %w", key, err) + } + if !exists { + return m.reconcileNotFound(ctx, key) + } + + obj, ok := raw.(domain.Object) + if !ok { + return fmt.Errorf("mux: type assertion failed for %q (got %T)", key, raw) + } + + target := ResolveTargetFromAnnotations(obj.GetAnnotations()) + m.targetCache.Store(key, target) + return m.reconcilerFor(target).Reconcile(ctx, key) +} + +// reconcilerFor returns the reconciler registered for target, or the fallback. +func (m *MuxReconciler) reconcilerFor(target string) domain.Reconciler { + if target != "" { + if rec, ok := m.targets[target]; ok { + return rec + } + } + return m.fallback +} + +// reconcileNotFound routes deletion cycles to the reconciler that last handled +// this key. The cache entry is removed after routing so stale entries don't +// accumulate for long-lived operators. +func (m *MuxReconciler) reconcileNotFound(ctx context.Context, key string) error { + target := "" + if v, ok := m.targetCache.Load(key); ok { + target, _ = v.(string) + } + defer m.targetCache.Delete(key) + return m.reconcilerFor(target).Reconcile(ctx, key) +} + +// ── CRD-level infrastructure forwarding ────────────────────────────────────── +// startCRDWorkers performs type assertions on the reconciler it receives from +// ReconcilerFactory(). MuxReconciler forwards each interface to the fallback so +// queue injection, autoscale, resync, metrics, and rollback notifiers all work +// as if the fallback were the direct reconciler. +// +// Autoscale, queue depth, and resync are CRD-level concerns — they govern the +// worker pool, not individual targets. Per-target reconcilers do not participate +// in these infrastructure calls today. Might become per-target tomorrow. + +func (m *MuxReconciler) SetQueue(wq *orkqueue.Workqueue) { + if qi, ok := m.fallback.(interface{ SetQueue(*orkqueue.Workqueue) }); ok { + qi.SetQueue(wq) + } +} + +func (m *MuxReconciler) SetSpawnWorker(fn func()) { + if ws, ok := m.fallback.(interface{ SetSpawnWorker(func()) }); ok { + ws.SetSpawnWorker(fn) + } +} + +func (m *MuxReconciler) SetRollbackNotifiers(onTrigger, onClear func()) { + if rns, ok := m.fallback.(interface{ SetRollbackNotifiers(func(), func()) }); ok { + rns.SetRollbackNotifiers(onTrigger, onClear) + } +} + +func (m *MuxReconciler) GetAutoMetrics() *autoscaler.AutoMetrics { + if exporter, ok := m.fallback.(interface{ GetAutoMetrics() *autoscaler.AutoMetrics }); ok { + return exporter.GetAutoMetrics() + } + return nil +} + +func (m *MuxReconciler) WorkerInfo(configuredResync string, configuredWorkers, configuredQueueDepth int) *autoscaler.WorkerInfo { + if wip, ok := m.fallback.(interface { + WorkerInfo(string, int, int) *autoscaler.WorkerInfo + }); ok { + return wip.WorkerInfo(configuredResync, configuredWorkers, configuredQueueDepth) + } + return nil +} + +func (m *MuxReconciler) RunAutoscaler(ctx context.Context) { + if runner, ok := m.fallback.(interface{ RunAutoscaler(context.Context) }); ok { + runner.RunAutoscaler(ctx) + } +} + +func (m *MuxReconciler) StartResyncLoop(ctx context.Context) { + if rl, ok := m.fallback.(interface{ StartResyncLoop(context.Context) }); ok { + rl.StartResyncLoop(ctx) + } +} diff --git a/pkg/intent/target/resolve.go b/pkg/intent/target/resolve.go new file mode 100644 index 000000000..61fa42f1b --- /dev/null +++ b/pkg/intent/target/resolve.go @@ -0,0 +1,27 @@ +package target + +import "github.com/orkspace/orkestra/pkg/labels" + +// ResolveTargetFromAnnotations extracts the effective target from a CR's annotations. +// Resolution order: +// 1. serve-alias annotation (most specific) +// 2. serve-target annotation (primary target) +// 3. Empty string (no target found) +func ResolveTargetFromAnnotations(annotations map[string]string) string { + if annotations == nil { + return "" + } + + // 1. Check alias first (most specific) + if alias, ok := annotations[labels.AnnotationServeAlias]; ok && alias != "" { + return alias + } + + // 2. Fall back to target + if target, ok := annotations[labels.AnnotationServeTarget]; ok && target != "" { + return target + } + + return "" +} + diff --git a/pkg/gateway/api/target.go b/pkg/intent/target/target.go similarity index 96% rename from pkg/gateway/api/target.go rename to pkg/intent/target/target.go index 6faa77fc8..81bc65ba8 100644 --- a/pkg/gateway/api/target.go +++ b/pkg/intent/target/target.go @@ -1,4 +1,4 @@ -package api +package target import ( "fmt" @@ -6,16 +6,16 @@ import ( orktmpl "github.com/orkspace/orkestra/pkg/resources/template" orktypes "github.com/orkspace/orkestra/pkg/types" - "github.com/orkspace/orkestra/pkg/utils" + // "github.com/orkspace/orkestra/pkg/utils" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -// isTargetRequest reports whether raw is a target-mode request. +// IsTargetRequest reports whether raw is a target-mode request. // // Detection rule: presence of "target" key, regardless of whether // "apiVersion" is also present. This lets callers migrate incrementally // by adding "target" without immediately removing Kubernetes fields. -func isTargetRequest(raw map[string]interface{}) bool { +func IsTargetRequest(raw map[string]interface{}) bool { _, ok := raw["target"] return ok } @@ -157,13 +157,13 @@ func routeFields( } // ─── Labels ────────────────────────────────────────────────────── - if utils.MapContains(labelFields, key) { + if mapContains(labelFields, key) { labels[key] = fmt.Sprintf("%v", submitted) continue } // ─── Annotations ───────────────────────────────────────────────── - if utils.MapContains(annotationFields, key) { + if mapContains(annotationFields, key) { annotations[key] = fmt.Sprintf("%v", submitted) continue } @@ -180,8 +180,8 @@ func routeFields( // setSpecValue writes a value to a dot-notation spec path, creating intermediate // maps as needed. Flat paths (no dot) are assigned directly. func setSpecValue(spec map[string]interface{}, path string, value interface{}) error { - if utils.IsNestedPath(path) { - return utils.SetNestedPath(spec, path, value) + if isNestedPath(path) { + return setNestedPath(spec, path, value) } spec[path] = value return nil diff --git a/pkg/gateway/api/target_helpers_test.go b/pkg/intent/target/target_test.go similarity index 76% rename from pkg/gateway/api/target_helpers_test.go rename to pkg/intent/target/target_test.go index 54811da47..59e7bb572 100644 --- a/pkg/gateway/api/target_helpers_test.go +++ b/pkg/intent/target/target_test.go @@ -1,4 +1,4 @@ -package api +package target import ( "testing" @@ -435,3 +435,112 @@ func TestNewBuildCRFromTarget(t *testing.T) { assert.Equal(t, "payments-api", obj.GetName()) assert.Equal(t, "team-payments-staging", obj.GetNamespace()) } + +func TestIsTargetRequest(t *testing.T) { + assert.True(t, IsTargetRequest(map[string]interface{}{ + "target": "app", + })) + // target wins even when apiVersion is also present (gradual migration path) + assert.True(t, IsTargetRequest(map[string]interface{}{ + "target": "app", + "apiVersion": "v1", + })) + assert.False(t, IsTargetRequest(map[string]interface{}{ + "apiVersion": "platform.myorg.io/v1", + "kind": "App", + })) + assert.False(t, IsTargetRequest(map[string]interface{}{})) +} + +func TestBuildCRFromTarget(t *testing.T) { + appCRD := &orktypes.CRDEntry{ + APITypes: orktypes.APITypes{ + Group: "platform.myorg.io", + Version: "v1", + Kind: "App", + Plural: "apps", + }, + GroupVersionKind: schema.GroupVersionKind{ + Group: "platform.myorg.io", Version: "v1", Kind: "App", + }, + Serve: &orktypes.ServeConfig{ + Target: orktypes.ServeTargetValue{Entries: map[string]*orktypes.ServeTargetConfig{ + "app": {Primary: true}, + }}, + Name: `{{ .repository | repoSlug }}`, + Namespace: `{{ .team }}-{{ .environment }}`, + Fields: map[string]orktypes.ServeFieldConfig{ + "repository": {}, + "image": {}, + "environment": {}, + "replicas": {}, + }, + Labels: map[string]orktypes.ServeFieldConfig{ + "team": {}, + }, + Annotations: map[string]orktypes.ServeFieldConfig{ + "jira-ticket": {}, + }, + }, + } + + t.Run("spec fields routed correctly", func(t *testing.T) { + raw := map[string]interface{}{ + "target": "app", + "repository": "myorg/payments-api", + "image": "ghcr.io/myorg/payments-api:v1", + "environment": "staging", + "replicas": float64(2), + "team": "payments", + "jira-ticket": "PLAT-1234", + } + + obj, err := BuildCRFromTarget(raw, appCRD, orktypes.NoteRegistry{}) + require.NoError(t, err) + + spec := obj.Object["spec"].(map[string]interface{}) + assert.Equal(t, "myorg/payments-api", spec["repository"]) + assert.Equal(t, "ghcr.io/myorg/payments-api:v1", spec["image"]) + assert.Equal(t, "staging", spec["environment"]) + assert.Equal(t, float64(2), spec["replicas"]) + + labels := obj.Object["metadata"].(map[string]interface{})["labels"].(map[string]interface{}) + assert.Equal(t, "payments", labels["team"]) + + annotations := obj.Object["metadata"].(map[string]interface{})["annotations"].(map[string]interface{}) + assert.Equal(t, "PLAT-1234", annotations["jira-ticket"]) + + // team and jira-ticket must NOT be in spec. + assert.Nil(t, spec["team"]) + assert.Nil(t, spec["jira-ticket"]) + }) + + t.Run("unknown fields ignored", func(t *testing.T) { + raw := map[string]interface{}{ + "target": "app", + "repository": "myorg/payments-api", + "team": "payments", + "environment": "staging", + "unknown-field": "should be ignored", + } + obj, err := BuildCRFromTarget(raw, appCRD, orktypes.NoteRegistry{}) + require.NoError(t, err) + + spec := obj.Object["spec"].(map[string]interface{}) + _, exists := spec["unknown-field"] + assert.False(t, exists) + }) + + t.Run("apiVersion and kind set from CRD entry", func(t *testing.T) { + raw := map[string]interface{}{ + "target": "app", + "repository": "myorg/payments-api", + "team": "payments", + "environment": "staging", + } + obj, err := BuildCRFromTarget(raw, appCRD, orktypes.NoteRegistry{}) + require.NoError(t, err) + assert.Equal(t, "platform.myorg.io/v1", obj.GetAPIVersion()) + assert.Equal(t, "App", obj.GetKind()) + }) +} diff --git a/pkg/katalog/pre_reconcile.go b/pkg/katalog/pre_reconcile.go index dc04fb3c7..33306f3d5 100644 --- a/pkg/katalog/pre_reconcile.go +++ b/pkg/katalog/pre_reconcile.go @@ -6,6 +6,7 @@ import ( "github.com/orkspace/orkestra/domain" "github.com/orkspace/orkestra/pkg/external" + orktarget "github.com/orkspace/orkestra/pkg/intent/target" orktmpl "github.com/orkspace/orkestra/pkg/resources/template" orktypes "github.com/orkspace/orkestra/pkg/types" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -26,7 +27,7 @@ func (k *Katalog) EvaluatePreReconcile(ctx context.Context, crdName string, obj if !ok { return true, "" } - target := orktypes.ResolveTargetFromAnnotations(obj.GetAnnotations()) + target := orktarget.ResolveTargetFromAnnotations(obj.GetAnnotations()) box := entry.EffectiveOperatorBox(target) rc := box.PreReconcile if rc == nil || !rc.HasReconcileGate() { @@ -81,7 +82,7 @@ func (k *Katalog) EvaluateEnqueueFilter(ctx context.Context, crdName string, obj if !ok { return true } - target := orktypes.ResolveTargetFromAnnotations(obj.GetAnnotations()) + target := orktarget.ResolveTargetFromAnnotations(obj.GetAnnotations()) box := entry.EffectiveOperatorBox(target) rc := box.PreReconcile if rc == nil || !rc.HasEnqueueGate() { @@ -93,10 +94,10 @@ func (k *Katalog) EvaluateEnqueueFilter(ctx context.Context, crdName string, obj return true } if !k.Profiles.IsEmpty() { - resolver = resolver.WithProfiles(k.Profiles) + resolver = resolver.WithProfiles(k.UserProfiles()) } if !k.Notes.IsEmpty() { - resolver = resolver.WithUserNotes(k.Notes) + resolver = resolver.WithUserNotes(k.UserNotes()) } if intent := orktypes.ServeIntentFromObject(resolver.Data()); intent != nil { resolver = resolver.WithRequest(intent) diff --git a/pkg/katalog/type.go b/pkg/katalog/type.go index b4e949a1e..04b39b2c3 100644 --- a/pkg/katalog/type.go +++ b/pkg/katalog/type.go @@ -134,6 +134,14 @@ func (k *Katalog) UserNotes() orktypes.NoteRegistry { return k.Notes } +// UserProfiles returns all user defined profiles in the katalog +func (k *Katalog) UserProfiles() orktypes.ProfileRegistry { + if k == nil { + return orktypes.ProfileRegistry{} + } + return k.Profiles +} + // IsEmpty reports true when the katalog is nil. func (k *Katalog) IsEmpty() bool { return k == nil diff --git a/pkg/katalog/validate_hooks_reconcilers.go b/pkg/katalog/validate_hooks_reconcilers.go index ea0723bd2..72588cb5f 100644 --- a/pkg/katalog/validate_hooks_reconcilers.go +++ b/pkg/katalog/validate_hooks_reconcilers.go @@ -24,28 +24,9 @@ func (k *Katalog) addReconcilers() error { if !crd.IsDynamic() { if crd.DefaultReconcile() { - // Per-target operatorBoxes can declare reconciler.default: false with a - // constructor — apply the same registration check as the CRD-level path. - if crd.Serve != nil && crd.Serve.Target.Entries != nil { - for targetName, targetCfg := range crd.Serve.Target.Entries { - if targetCfg.OperatorBox == nil || targetCfg.OperatorBox.Reconciler == nil { - continue - } - rec := targetCfg.OperatorBox.Reconciler - if rec.Default != nil && !*rec.Default { - constructorFn, ok := orktypes.ReconcilerRegistry[crd.GroupVersionKind] - if !ok { - return fmt.Errorf( - "CRD %q target %q: reconciler.default: false but no constructor registered — "+ - "check reconciler.constructor in Katalog and re-run ork generate registry", - name, targetName, - ) - } - targetCfg.OperatorBox.Constructor = constructorFn - crd.Serve.Target.Entries[targetName] = targetCfg - } - } - } + // Per-target constructors (reconciler.default: false on a target operatorBox) + // are owned by addTargetConstructors, which reads TargetReconcilerRegistry. + // Nothing to do here for those targets. crd.OperatorBox = rc k.enabledCRDs[name] = crd continue diff --git a/pkg/kubeclient/fixture/03-hooks-targets/README.md b/pkg/kubeclient/fixture/03-hooks-targets/README.md index 2c7da0e8c..14faa55d3 100644 --- a/pkg/kubeclient/fixture/03-hooks-targets/README.md +++ b/pkg/kubeclient/fixture/03-hooks-targets/README.md @@ -1,9 +1,17 @@ -# Per-Target Args — BlockchainAppWithTargets +# Per-Target OperatorBox — BlockchainAppWithTargets -The same hook binary can behave differently on different surfaces. The platform -team declares two targets — `v2-enabled` and `v2-disabled` — each with its own -`operatorBox` and its own `args`. The hook reads `kube.Args()` and never knows -which surface it came from. +Three surfaces, one CRD, three different reconcile strategies — all dispatched +by the runtime based on the `orkestra.io/serve-target` annotation the gateway +stamps on the CR. + +| Target | Strategy | What it does | +|--------|----------|--------------| +| `v2-enabled` | Per-target hooks | Same binary as `v2-disabled`; args force `featureEnabled=true` and gate on business hours | +| `v2-disabled` | Per-target hooks | Same binary; args force `featureEnabled=false`, no gate | +| `v2-ctor` | Per-target constructor | Distinct `domain.Reconciler` implementation; reads `featureEnabled` from args, owns the full reconcile loop | + +**Hooks targets** share one binary. The Katalog's `args` determine what each +surface means — the hook reads `kube.Args()` and never knows which surface it came from: ```yaml serve: @@ -19,7 +27,7 @@ serve: reconciler: hooks: args: - featureEnabled: "true" # forced — no HTTP call needed + featureEnabled: "true" inBusinessHours: '{{ inBusinessHours }}' v2-disabled: @@ -27,12 +35,28 @@ serve: reconciler: hooks: args: - featureEnabled: "false" # forced off — no gate + featureEnabled: "false" inBusinessHours: '{{ inBusinessHours }}' ``` -The hook code is identical to `01-hooks`. The Katalog determines what each -surface means; the caller just picks a target: +**Constructor target** brings its own `domain.Reconciler`. The runtime wraps +all three in a `MuxReconciler` that routes each CR to the right reconciler at +dispatch time: + +```yaml + v2-ctor: + operatorBox: + reconciler: + default: false + constructor: + location: github.com/orkspace/orkestra-args-hooks-targets/constructor + function: NewBlockchainAppWithTargetsReconciler + alias: bcctor + args: + featureEnabled: "true" +``` + +The caller just picks a target: **Requirement:** `ork` CLI — install from [orkestra-install](https://github.com/orkspace/orkestra#getting-started) @@ -49,8 +73,31 @@ make registry ```bash make clean && make build ork validate katalog.yaml +``` + +### Simulate and Play without a cluster + + +#### Simulate + +```bash ork simulate -f simulate-v2-enabled.yaml ork simulate -f simulate-v2-disabled.yaml +ork simulate +``` + +#### Play + +- First check permissions: + +```bash +ork serve can-i --token dev --operation create --target v2-enabled +``` + +- Then Play: + +```bash +ork serve play -i intent/intent-v2-enabled.yaml --token dev ``` ## Step 3 — Run @@ -92,9 +139,18 @@ kubectl get blockchainappwithtargets 03-hooks-targets-my-chain \ Switch to `v2-disabled` (feature off, no gate): ```bash -ork serve apply -f intent/intent-v2-disabled.yaml --token $TOKEN --api http://localhost:8888 +ork serve apply -f intent/intent-v2-disabled.json --token $TOKEN --api http://localhost:8888 ``` +Switch to `v2-ctor` (constructor reconciler, feature on): + +```bash +ork serve apply -f intent/intent-v2-ctor.json --token $TOKEN --api http://localhost:8888 +``` + +The runtime routes this CR to `BlockchainAppWithTargetsReconciler` via `MuxReconciler` +instead of the CRD-level `GenericReconciler`. + > Switching targets cleans up the previous surface's resources automatically. > `keepPreviousSurface: true` on the target entry skips the cleanup when you > want both surfaces running simultaneously. diff --git a/pkg/kubeclient/fixture/03-hooks-targets/cmd/orkestra/main.go b/pkg/kubeclient/fixture/03-hooks-targets/cmd/orkestra/main.go index 7567184db..86cd09451 100644 --- a/pkg/kubeclient/fixture/03-hooks-targets/cmd/orkestra/main.go +++ b/pkg/kubeclient/fixture/03-hooks-targets/cmd/orkestra/main.go @@ -1,25 +1,25 @@ -// Code generated by "ork generate registry" on 2026-08-16T20:29:01Z. DO NOT EDIT. +// Code generated by "ork generate registry" on 2026-08-17T06:44:58Z. DO NOT EDIT. // Re-generate by running: ork generate registry --file package main import ( - "context" + "context" - "github.com/orkspace/orkestra/cmd/cli" - "github.com/orkspace/orkestra/pkg/konfig" - "github.com/orkspace/orkestra/pkg/logger" + "github.com/orkspace/orkestra/cmd/cli" + "github.com/orkspace/orkestra/pkg/konfig" + "github.com/orkspace/orkestra/pkg/logger" "github.com/orkspace/orkestra/pkg/utils" - _ "github.com/orkspace/orkestra-args-hooks-targets/pkg/typeregistry" + _ "github.com/orkspace/orkestra-args-hooks-targets/pkg/typeregistry" ) func main() { - kfg, err := konfig.Init() - if err != nil { - logger.Fatal().AnErr("failed to load configurations", err) - utils.Exit(err) - } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - cli.Execute(kfg, ctx) -} \ No newline at end of file + kfg, err := konfig.Init() + if err != nil { + logger.Fatal().AnErr("failed to load configurations", err) + utils.Exit(err) + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + cli.Execute(kfg, ctx) +} diff --git a/pkg/kubeclient/fixture/03-hooks-targets/constructor/blockchainappwithtargets_reconciler.go b/pkg/kubeclient/fixture/03-hooks-targets/constructor/blockchainappwithtargets_reconciler.go new file mode 100644 index 000000000..0470922b1 --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/constructor/blockchainappwithtargets_reconciler.go @@ -0,0 +1,96 @@ +package constructor + +import ( + "context" + "fmt" + + apiv1 "github.com/orkspace/orkestra-args-hooks-targets/api/v1alpha1" + "github.com/orkspace/orkestra/domain" + "github.com/orkspace/orkestra/pkg/event" + "github.com/orkspace/orkestra/pkg/kubeclient" + orkdeploy "github.com/orkspace/orkestra/pkg/resources/deployments" + orktmpl "github.com/orkspace/orkestra/pkg/resources/template" + "k8s.io/client-go/tools/cache" +) + +// BlockchainAppWithTargetsReconciler is the per-target constructor reconciler +// for the BlockchainAppWithTargets CRD. It reads featureEnabled from args +// (declared in katalog serve.target..operatorBox.reconciler.constructor.args) +// rather than calling a live feature-flag endpoint. +type BlockchainAppWithTargetsReconciler struct { + informer cache.SharedIndexInformer + kube kubeclient.Interface + ev event.Recorder +} + +// NewBlockchainAppWithTargetsReconciler is the constructor registered in +// serve.target.v2-ctor.operatorBox.reconciler.constructor. +func NewBlockchainAppWithTargetsReconciler( + kube kubeclient.Interface, + informer cache.SharedIndexInformer, + ev event.Recorder, +) domain.Reconciler { + return &BlockchainAppWithTargetsReconciler{ + informer: informer, + kube: kube, + ev: ev, + } +} + +func (r *BlockchainAppWithTargetsReconciler) Reconcile(ctx context.Context, key string) error { + raw, exists, err := r.informer.GetIndexer().GetByKey(key) + if err != nil { + return fmt.Errorf("cache lookup %q: %w", key, err) + } + if !exists { + return nil + } + + app, ok := raw.(*apiv1.BlockchainAppWithTargets) + if !ok { + return fmt.Errorf("unexpected type %T for key %q", raw, key) + } + app = app.DeepCopyObject().(*apiv1.BlockchainAppWithTargets) + + if app.DeletionTimestamp != nil { + return nil + } + + resolver, err := orktmpl.NewResolver(ctx, app) + if err != nil { + return fmt.Errorf("building resolver: %w", err) + } + kube := r.kube.ScopedFor(resolver.TemplateEvaluator()) + + featureEnabled := kube.Args().String("featureEnabled") + + annotation := "false" + if featureEnabled == "true" { + annotation = "true" + } + + replicas := int32(app.Spec.Replicas) + if replicas == 0 { + replicas = 1 + } + + spec := orkdeploy.ResolvedDeploymentSpec{ + Name: app.Name, + Namespace: app.Namespace, + Image: app.Spec.Image, + Replicas: replicas, + Annotations: map[string]string{ + "feature.demo/v2-enabled": annotation, + "orkestra.io/target": "v2-ctor", + }, + } + if err := orkdeploy.Apply(ctx, kube, app, spec); err != nil { + return fmt.Errorf("blockchainappwithtargets deployment: %w", err) + } + + return kube.PatchStatus(ctx, app, map[string]any{ + "phase": "Running", + "network": app.Spec.Network, + "featureEnabled": annotation, + }) +} diff --git a/pkg/kubeclient/fixture/03-hooks-targets/go.mod b/pkg/kubeclient/fixture/03-hooks-targets/go.mod index d8a638f9e..c56d3d032 100644 --- a/pkg/kubeclient/fixture/03-hooks-targets/go.mod +++ b/pkg/kubeclient/fixture/03-hooks-targets/go.mod @@ -5,6 +5,7 @@ go 1.26.6 require ( github.com/orkspace/orkestra v0.0.0 k8s.io/apimachinery v0.36.1 + k8s.io/client-go v0.36.1 ) require ( @@ -214,7 +215,6 @@ require ( k8s.io/apiextensions-apiserver v0.36.1 // indirect k8s.io/apiserver v0.36.1 // indirect k8s.io/cli-runtime v0.36.1 // indirect - k8s.io/client-go v0.36.1 // indirect k8s.io/component-base v0.36.1 // indirect k8s.io/klog/v2 v2.140.0 // indirect k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect diff --git a/pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-ctor.json b/pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-ctor.json new file mode 100644 index 000000000..4829fbeab --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-ctor.json @@ -0,0 +1,8 @@ +{ + "target": "v2-ctor", + "name": "03-hooks-targets-my-chain", + "image": "ethereum/client-go:v1.14.0", + "network": "testnet", + "nodeType": "full-node", + "replicas": 5 +} diff --git a/pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-disabled.json b/pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-disabled.json new file mode 100644 index 000000000..0d88e575b --- /dev/null +++ b/pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-disabled.json @@ -0,0 +1,8 @@ +{ + "target": "v2-disabled", + "name": "03-hooks-targets-my-chain", + "image": "ethereum/client-go:v1.14.0", + "network": "testnet", + "nodeType": "full-node", + "replicas": 3 +} diff --git a/pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-disabled.yaml b/pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-disabled.yaml deleted file mode 100644 index f576e38ce..000000000 --- a/pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-disabled.yaml +++ /dev/null @@ -1,6 +0,0 @@ -target: v2-disabled -name: 03-hooks-targets-my-chain -image: ethereum/client-go:v1.14.0 -network: testnet -nodeType: full-node -replicas: 5 diff --git a/pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-enabled.yaml b/pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-enabled.yaml index db32c01a7..0c298e903 100644 --- a/pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-enabled.yaml +++ b/pkg/kubeclient/fixture/03-hooks-targets/intent/intent-v2-enabled.yaml @@ -3,4 +3,4 @@ name: 03-hooks-targets-my-chain image: ethereum/client-go:v1.14.0 network: testnet nodeType: full-node -replicas: 5 +replicas: 2 diff --git a/pkg/kubeclient/fixture/03-hooks-targets/katalog.yaml b/pkg/kubeclient/fixture/03-hooks-targets/katalog.yaml index 819d8305a..c3d0bad73 100644 --- a/pkg/kubeclient/fixture/03-hooks-targets/katalog.yaml +++ b/pkg/kubeclient/fixture/03-hooks-targets/katalog.yaml @@ -43,19 +43,8 @@ spec: operatorBox: reconciler: - default: true - # hooks: - # location: github.com/orkspace/orkestra-args-hooks-targets/hooks - # function: BlockchainAppHooks - # alias: bchooks - # resources: - # - kind: Deployment - # args: - # featureEnabled: '{{ .external.flags.body }}' - # inBusinessHours: '{{ inBusinessHours }}' workers: 2 resync: 30s - status: fields: - path: phase @@ -69,6 +58,12 @@ spec: serve: enabled: true namespace: default + tokens: + dev: + permissions: + resources: + - create + - update fields: image: label: "Container image" @@ -91,7 +86,6 @@ spec: equals: "true" reconciler: hooks: - hooks: location: github.com/orkspace/orkestra-args-hooks-targets/hooks function: BlockchainAppHooks alias: bchooks @@ -113,3 +107,14 @@ spec: args: featureEnabled: "false" inBusinessHours: '{{ inBusinessHours }}' + + v2-ctor: + operatorBox: + reconciler: + default: false + constructor: + location: github.com/orkspace/orkestra-args-hooks-targets/constructor + function: NewBlockchainAppWithTargetsReconciler + alias: bcctor + args: + featureEnabled: "true" diff --git a/pkg/kubeclient/fixture/03-hooks-targets/pkg/typeregistry/zz_generated_typeregistry.go b/pkg/kubeclient/fixture/03-hooks-targets/pkg/typeregistry/zz_generated_typeregistry.go index 999a4201c..908a71e97 100644 --- a/pkg/kubeclient/fixture/03-hooks-targets/pkg/typeregistry/zz_generated_typeregistry.go +++ b/pkg/kubeclient/fixture/03-hooks-targets/pkg/typeregistry/zz_generated_typeregistry.go @@ -1,5 +1,5 @@ // pkg/typeregistry/zz_generated_typeregistry.go -// Code generated by "ork generate registry" on 2026-08-16T20:29:01Z. DO NOT EDIT. +// Code generated by "ork generate registry" on 2026-08-17T06:44:58Z. DO NOT EDIT. // Re-generate by running: ork generate registry --file // // This file registers compiled Go types and external functions. @@ -9,13 +9,17 @@ package typeregistry import ( "github.com/orkspace/orkestra/domain" + "github.com/orkspace/orkestra/pkg/event" + "github.com/orkspace/orkestra/pkg/kubeclient" "github.com/orkspace/orkestra/pkg/logger" orktypes "github.com/orkspace/orkestra/pkg/types" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/tools/cache" bcappwithtargetsv1 "github.com/orkspace/orkestra-args-hooks-targets/api/v1alpha1" + bcctor "github.com/orkspace/orkestra-args-hooks-targets/constructor" bchooks "github.com/orkspace/orkestra-args-hooks-targets/hooks" ) @@ -70,16 +74,46 @@ func RegisterRuntimeObjects() { orktypes.ListRegistry[schema.GroupVersionKind{Group: "demo.orkestra.io", Version: "v1alpha1", Kind: "BlockchainAppWithTargets"}] = func() runtime.Object { return &bcappwithtargetsv1.BlockchainAppWithTargetsList{} } - // BlockchainAppWithTargets — Go hook factory - // Calls bchooks.BlockchainAppHooks() to obtain typed ReconcileHooks. - orktypes.HookRegistry[schema.GroupVersionKind{Group: "demo.orkestra.io", Version: "v1alpha1", Kind: "BlockchainAppWithTargets"}] = - func() domain.AnyReconcileHooks { + // BlockchainAppWithTargets/v2-enabled — per-target Go hook factory + // Distinct hook binary for this target; TargetHookFactories carries it into startCRDWorkers. + { + gvk := schema.GroupVersionKind{Group: "demo.orkestra.io", Version: "v1alpha1", Kind: "BlockchainAppWithTargets"} + if orktypes.TargetHookRegistry[gvk] == nil { + orktypes.TargetHookRegistry[gvk] = map[string]func() domain.AnyReconcileHooks{} + } + orktypes.TargetHookRegistry[gvk]["v2-enabled"] = func() domain.AnyReconcileHooks { + return bchooks.BlockchainAppHooks() + } + } + + // BlockchainAppWithTargets/v2-disabled — per-target Go hook factory + // Distinct hook binary for this target; TargetHookFactories carries it into startCRDWorkers. + { + gvk := schema.GroupVersionKind{Group: "demo.orkestra.io", Version: "v1alpha1", Kind: "BlockchainAppWithTargets"} + if orktypes.TargetHookRegistry[gvk] == nil { + orktypes.TargetHookRegistry[gvk] = map[string]func() domain.AnyReconcileHooks{} + } + orktypes.TargetHookRegistry[gvk]["v2-disabled"] = func() domain.AnyReconcileHooks { return bchooks.BlockchainAppHooks() } + } + + // BlockchainAppWithTargets/v2-ctor — per-target custom reconciler constructor + // Distinct reconciler for this target; TargetReconcilerFactories carries it into startCRDWorkers. + { + gvk := schema.GroupVersionKind{Group: "demo.orkestra.io", Version: "v1alpha1", Kind: "BlockchainAppWithTargets"} + if orktypes.TargetReconcilerRegistry[gvk] == nil { + orktypes.TargetReconcilerRegistry[gvk] = map[string]orktypes.NewReconcilerFunc{} + } + orktypes.TargetReconcilerRegistry[gvk]["v2-ctor"] = func(kube kubeclient.Interface, inf cache.SharedIndexInformer, ev event.Recorder) domain.Reconciler { + return bcctor.NewBlockchainAppWithTargetsReconciler(kube, inf, ev) + } + } logger.Debug(). Int("objectRegistrySize", len(orktypes.ObjectRegistry)). Int("listRegistrySize", len(orktypes.ListRegistry)). - Int("hookRegistrySize", len(orktypes.HookRegistry)). + Int("targetHookRegistrySize", len(orktypes.TargetHookRegistry)). + Int("targetRecRegistrySize", len(orktypes.TargetReconcilerRegistry)). Msg("Runtime objects registered") } diff --git a/pkg/registry/simulate/helper.go b/pkg/registry/simulate/helper.go index 9a93d4e9d..9b1cc5259 100644 --- a/pkg/registry/simulate/helper.go +++ b/pkg/registry/simulate/helper.go @@ -9,6 +9,7 @@ import ( orklabels "github.com/orkspace/orkestra/pkg/labels" orktypes "github.com/orkspace/orkestra/pkg/types" + orktarget "github.com/orkspace/orkestra/pkg/intent/target" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/tools/cache" ) @@ -19,7 +20,7 @@ func effectiveOperatorBox(entry orktypes.CRDEntry, cr *unstructured.Unstructured return entry.EffectiveOperatorBox(target) } - effectiveTarget := orktypes.ResolveTargetFromAnnotations(cr.GetAnnotations()) + effectiveTarget := orktarget.ResolveTargetFromAnnotations(cr.GetAnnotations()) return entry.EffectiveOperatorBox(effectiveTarget) } diff --git a/pkg/runtime/kordinator/dependency_kordinator.go b/pkg/runtime/kordinator/dependency_kordinator.go index 8ecd0838b..6aba598cc 100644 --- a/pkg/runtime/kordinator/dependency_kordinator.go +++ b/pkg/runtime/kordinator/dependency_kordinator.go @@ -501,6 +501,8 @@ func (k *DependencyKordinator) startCRDWorkers(ctx context.Context, gvk string, crdCtx, cancel := context.WithCancel(ctx) wg := &sync.WaitGroup{} + + // rec := entry.ReconcilerFactory() // Inject the per-CRD workqueue so SetQueueDepthLimit and the resync goroutine diff --git a/pkg/runtime/reconciler/generic.go b/pkg/runtime/reconciler/generic.go index 28429d531..51e97aa72 100644 --- a/pkg/runtime/reconciler/generic.go +++ b/pkg/runtime/reconciler/generic.go @@ -76,7 +76,14 @@ type GenericReconciler[PTR domain.Object] struct { // construction time from the user's ReconcileHooks[PTR]. Stored as // ObjectHooks rather than ReconcileHooks[PTR] so the reconciler remains // compatible with the runtime registry path (PTR = domain.Object interface). - hooks domain.ObjectHooks + hooks domain.ObjectHooks + + // targetHooks holds per-target hook sets for CRDs that have distinct hook + // binaries per serve.target entry (TargetHookFactories non-empty). + // Built once at construction time; read concurrently during reconcile. + // When empty, all targets fall back to the CRD-level hooks field. + targetHooks map[string]domain.ObjectHooks + operatorBox orktypes.OperatorBoxConfig newObj func() PTR crd orktypes.CRDEntry @@ -169,6 +176,16 @@ func NewGenericReconciler[PTR domain.Object]( hooks = binder.BindToObjectHooks() } + // Build per-target hook sets for targets that declare a distinct hook binary. + // Targets that share the CRD-level binary only override args and use hooks above. + targetHooks := make(map[string]domain.ObjectHooks, len(crd.TargetHookFactories)) + for targetName, factory := range crd.TargetHookFactories { + anyH := factory() + if binder, ok := anyH.(domain.HookBinder); ok { + targetHooks[targetName] = binder.BindToObjectHooks() + } + } + if ev == nil { ev = discardRecorder{} } @@ -200,6 +217,7 @@ func NewGenericReconciler[PTR domain.Object]( event: ev, kube: kube, hooks: hooks, + targetHooks: targetHooks, newObj: newObj, workerSem: sem, autoMetrics: autoMet, @@ -240,19 +258,6 @@ func NewGenericReconciler[PTR domain.Object]( var _ domain.Reconciler = (*GenericReconciler[domain.Object])(nil) -// effectiveBox returns the operatorBox that governs this reconcile cycle. -// It reads the serve-target annotation (alias > target > empty) and delegates -// to CRDEntry.EffectiveOperatorBox. Falls back to the CRD-level box when the -// CR has no target annotation (e.g. direct kubectl apply). -// The system CleanupFinalizer is always included in the returned box. -func (r *GenericReconciler[PTR]) effectiveBox(obj PTR) orktypes.OperatorBoxConfig { - target := orktypes.ResolveTargetFromAnnotations(obj.GetAnnotations()) - box := *r.crd.EffectiveOperatorBox(target) - if !slices.Contains(box.Finalizers, labels.CleanupFinalizer) { - box.Finalizers = append(box.Finalizers, labels.CleanupFinalizer) - } - return box -} // Reconcile dispatches to the correct reconcile implementation. // Order: @@ -307,10 +312,12 @@ func (r *GenericReconciler[PTR]) reconcileCore(ctx context.Context, key string) } rawObj := obj.DeepCopyObject().(PTR) - // Resolve the effective operatorBox for this CR. CRs routed through the gateway - // carry a serve-target annotation; the box for that target governs this cycle. - // Falls back to the CRD-level box for direct kubectl applies (no annotation). - box := r.effectiveBox(rawObj) + // Resolve the effective operatorBox and target for this CR. CRs routed through + // the gateway carry a serve-target annotation; the box for that target governs + // this cycle. Falls back to the CRD-level box for direct kubectl applies. + box, target := r.effectiveBoxAndTarget(rawObj) + hooks := r.hooksFor(target) + ctx = r.withTargetArgs(ctx, box) // Normalize before mutation/validation/template rendering ───────────── // Normalize + base resolver @@ -385,7 +392,7 @@ func (r *GenericReconciler[PTR]) reconcileCore(ctx context.Context, key string) r.event.Eventf(obj, corev1.EventTypeNormal, "Deleting", fmt.Sprintf("Deleting %s %s/%s", r.crd.GVKString(), obj.GetNamespace(), obj.GetName())) - return r.handleDeletion(ctx, resolver, obj, box) + return r.handleDeletion(ctx, resolver, obj, box, hooks) } // Namespace guard — skip reconcile for CRs in restricted or non-allowed namespaces. @@ -485,7 +492,7 @@ func (r *GenericReconciler[PTR]) reconcileCore(ctx context.Context, key string) } // ── Step 5: Reconcile implementation ────────────────────────────────────── - if err := r.reconcileImpl(ctx, resolver, obj, box); err != nil { + if err := r.reconcileImpl(ctx, resolver, obj, box, hooks); err != nil { return err } @@ -503,7 +510,7 @@ func (r *GenericReconciler[PTR]) reconcileCore(ctx context.Context, key string) // 4. Reconcile dispatch // 5. Failure trigger check — record failure; trigger rollback if threshold met // 6. Status patch -func (r *GenericReconciler[PTR]) reconcileImpl(ctx context.Context, resolver *orktmpl.Resolver, obj PTR, box orktypes.OperatorBoxConfig) error { +func (r *GenericReconciler[PTR]) reconcileImpl(ctx context.Context, resolver *orktmpl.Resolver, obj PTR, box orktypes.OperatorBoxConfig, hooks domain.ObjectHooks) error { var err error // ── Phase 1: Rollback gate ──────────────────────────────────────────────── @@ -563,7 +570,7 @@ func (r *GenericReconciler[PTR]) reconcileImpl(ctx context.Context, resolver *or } hasTemplates := box.OnCreate != nil || box.OnReconcile != nil switch { - case r.hooks.OnReconcile != nil: + case hooks.OnReconcile != nil: // Go hooks — user-provided, full type-safe access. // Requires: ork generate registry to register in HookRegistry. // @@ -573,7 +580,7 @@ func (r *GenericReconciler[PTR]) reconcileImpl(ctx context.Context, resolver *or resolver, err = r.runTemplateReconcile(ctx, resolver, obj, box) } if err == nil { - err = r.hooks.OnReconcile(ctx, obj) + err = hooks.OnReconcile(ctx, obj) } if err == nil && r.crd.RunHooksFirst() && hasTemplates { resolver, err = r.runTemplateReconcile(ctx, resolver, obj, box) @@ -704,10 +711,10 @@ func (r *GenericReconciler[PTR]) namespaceAllowed( // handleDeletion runs cleanup then removes our finalizers. // Finalizers are never removed on error — object stays protected until // cleanup succeeds. -func (r *GenericReconciler[PTR]) handleDeletion(ctx context.Context, resolver *orktmpl.Resolver, obj PTR, box orktypes.OperatorBoxConfig) error { +func (r *GenericReconciler[PTR]) handleDeletion(ctx context.Context, resolver *orktmpl.Resolver, obj PTR, box orktypes.OperatorBoxConfig, hooks domain.ObjectHooks) error { switch { - case r.hooks.OnDelete != nil: - if err := r.hooks.OnDelete(ctx, obj); err != nil { + case hooks.OnDelete != nil: + if err := hooks.OnDelete(ctx, obj); err != nil { r.event.Eventf(obj, corev1.EventTypeWarning, r.crd.APITypes.Kind+"DeleteError", fmt.Sprintf("Deletion hook failed: %v", err)) return fmt.Errorf("deletion hook: %w", err) diff --git a/pkg/runtime/reconciler/generic_target.go b/pkg/runtime/reconciler/generic_target.go new file mode 100644 index 000000000..3bdab6518 --- /dev/null +++ b/pkg/runtime/reconciler/generic_target.go @@ -0,0 +1,50 @@ +package reconciler + +import ( + "context" + + "slices" + + "github.com/orkspace/orkestra/domain" + "github.com/orkspace/orkestra/pkg/kubeclient" + "github.com/orkspace/orkestra/pkg/labels" + orktarget "github.com/orkspace/orkestra/pkg/intent/target" + orktypes "github.com/orkspace/orkestra/pkg/types" +) + +// effectiveBoxAndTarget returns the operatorBox and resolved target name for +// this reconcile cycle. The target is read from the CR's serve-target annotation +// (alias resolved > raw target > empty). Falls back to the CRD-level box when +// the CR has no annotation (e.g. direct kubectl apply). +// The system CleanupFinalizer is always included in the returned box. +func (r *GenericReconciler[PTR]) effectiveBoxAndTarget(obj PTR) (orktypes.OperatorBoxConfig, string) { + target := orktarget.ResolveTargetFromAnnotations(obj.GetAnnotations()) + box := *r.crd.EffectiveOperatorBox(target) + if !slices.Contains(box.Finalizers, labels.CleanupFinalizer) { + box.Finalizers = append(box.Finalizers, labels.CleanupFinalizer) + } + return box, target +} + +// hooksFor returns the ObjectHooks for the given target name. +// If the target has a distinct hook binary (registered in TargetHookFactories), +// those hooks are returned. Otherwise falls back to the CRD-level hooks. +func (r *GenericReconciler[PTR]) hooksFor(target string) domain.ObjectHooks { + if target != "" { + if h, ok := r.targetHooks[target]; ok { + return h + } + } + return r.hooks +} + +// withTargetArgs returns a context whose kube client carries the per-target +// merged hooks.args for this reconcile cycle. When the effective box has no +// args override, the context is returned unchanged. +func (r *GenericReconciler[PTR]) withTargetArgs(ctx context.Context, box orktypes.OperatorBoxConfig) context.Context { + args := box.Reconciler.HooksArgs() + if len(args) == 0 { + return ctx + } + return kubeclient.WithKubeclient(ctx, r.kube.WithArgs(kubeclient.Args(args))) +} diff --git a/pkg/runtime/reconciler/run_surface_cleanup.go b/pkg/runtime/reconciler/run_surface_cleanup.go index 7e5af5db3..3e08bd0c1 100644 --- a/pkg/runtime/reconciler/run_surface_cleanup.go +++ b/pkg/runtime/reconciler/run_surface_cleanup.go @@ -6,7 +6,7 @@ import ( "github.com/orkspace/orkestra/pkg/labels" "github.com/orkspace/orkestra/pkg/logger" "github.com/orkspace/orkestra/pkg/runtime/runners" - orktypes "github.com/orkspace/orkestra/pkg/types" + orktarget "github.com/orkspace/orkestra/pkg/intent/target" ) // cleanupPreviousSurface deletes all resources belonging to the surface the CR @@ -25,7 +25,7 @@ func (r *GenericReconciler[PTR]) cleanupPreviousSurface( ctx context.Context, rawObj PTR, ) error { - target := orktypes.ResolveTargetFromAnnotations(rawObj.GetAnnotations()) + target := orktarget.ResolveTargetFromAnnotations(rawObj.GetAnnotations()) if target == "" || r.crd.KeepPreviousSurface(target) { return nil } diff --git a/pkg/runtime/runners/docs/issues.md b/pkg/runtime/runners/docs/issues.md new file mode 100644 index 000000000..fa24467e2 --- /dev/null +++ b/pkg/runtime/runners/docs/issues.md @@ -0,0 +1,13 @@ +# Known Issues — runners + +## Surface cleanup does not cover hook-managed resources + +`SweepOwnedNamespacedResources` and `SweepOwnedClusterScopedResources` find resources by the `orkestra-owner` label. This label is stamped automatically on resources created through the template engine (onCreate/onReconcile blocks). It is **not** stamped on resources created by Go hook code (`OnReconcile`, `OnDelete`), because the hook author writes plain Go — no automatic label injection happens. + +As a result, when a CR switches from one per-target hook surface to another, the previous surface's hook-managed resources are invisible to the sweep and are not cleaned up. + +**What the declaration already gives us:** `hooks.resources[].kind` is exactly the set of Kubernetes resource kinds the hook manages. This declaration is currently used for RBAC generation, webhook scope, and informer watches — but not for cleanup. + +**The extension:** when `cleanupPreviousSurface` detects a target switch and the previous target's operatorBox declares `hooks.resources[]`, the sweep should include those specific kinds scoped to the previous target's ownerKey. If the hook did not label its resources, the fallback is name-based deletion (resources named after the CR in the previous target's namespace and kinds). + +**Where to drive it:** `cleanupPreviousSurface` in `pkg/runtime/reconciler/run_surface_cleanup.go` has access to `r.crd`, which carries the previous target's operatorBox after `EffectiveOperatorBox(prevTarget)` is called. The hook's resource declarations are at `box.Reconciler.Hooks.Resources`. diff --git a/pkg/types/methods.go b/pkg/types/methods.go index a161057e3..3b5927b54 100644 --- a/pkg/types/methods.go +++ b/pkg/types/methods.go @@ -370,6 +370,15 @@ func (c *CRDEntry) HooksArgs() map[string]interface{} { return nil } +// HooksArgs returns the args map from this reconciler config's hooks declaration. +// Returns nil when no hooks are declared or no args are set. +func (r *ReconcilerConfig) HooksArgs() map[string]interface{} { + if r == nil || r.Hooks == nil { + return nil + } + return r.Hooks.Args +} + // HooksExternal returns the external call specs declared under reconciler.hooks.external. // Returns nil when no hooks declaration or no external calls are declared. func (c *CRDEntry) HooksExternal() []ExternalCallSpec { @@ -393,6 +402,44 @@ func (c *CRDEntry) ConstructorArgs() map[string]interface{} { return nil } +// TargetConstructorArgs returns the constructor args declared under +// serve.target.entries[targetName].operatorBox.reconciler.constructor.args. +// Returns nil when the target entry, its operatorBox, or its constructor declaration is absent. +func (c *CRDEntry) TargetConstructorArgs(targetName string) map[string]interface{} { + if c.Serve == nil || c.Serve.Target.Entries == nil { + return nil + } + entry, ok := c.Serve.Target.Entries[targetName] + if !ok || entry.OperatorBox == nil || entry.OperatorBox.Reconciler == nil { + return nil + } + if entry.OperatorBox.Reconciler.ConstructorDecl == nil { + return nil + } + return entry.OperatorBox.Reconciler.ConstructorDecl.Args +} + +// HasTargetConstructorFactories reports whether any serve target declares a +// custom constructor (reconciler.default: false with a constructor declaration). +func (c *CRDEntry) HasTargetConstructorFactories() bool { + if c.Serve == nil || c.Serve.Target.Entries == nil { + return false + } + for _, entry := range c.Serve.Target.Entries { + box := entry.OperatorBox + if box.IsEmpty() { + continue + } + rec := box.Reconciler + if rec.IsEmpty() || rec.IsDefault() || !rec.HasConstructorDecl() { + continue + } + return true + } + return false +} + + // IsEnabledAllEndpoints reports whether the all endpoints are disabled for this CRD. // Defaults to false when omitted. func (c *CRDEntry) IsEnabledAllEndpoints() bool { diff --git a/pkg/types/types.go b/pkg/types/types.go index 1e5b2edfd..e50c116e2 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -161,3 +161,21 @@ type Queue struct { // 0 → uses FAILURE_THRESHOLD env var. FailureThreshold int `yaml:"failureThreshold,omitempty" json:"failureThreshold,omitempty" validate:"omitempty,gte=0"` } + +// IsEmpty reports whether the queue configuration has no meaningful settings. +// Used to skip unnecessary config blocks in the Katalog. +func (q *Queue) IsEmpty() bool { + if q == nil { + return true + } + if q.Shared != nil { + return false + } + if q.MaxDepth != 0 { + return false + } + if q.FailureThreshold != 0 { + return false + } + return true +} \ No newline at end of file diff --git a/pkg/types/types_crd_entry.go b/pkg/types/types_crd_entry.go index 31622273d..16ff1ffd8 100644 --- a/pkg/types/types_crd_entry.go +++ b/pkg/types/types_crd_entry.go @@ -5,7 +5,6 @@ import ( "sort" "github.com/orkspace/orkestra/domain" - "github.com/orkspace/orkestra/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -371,29 +370,6 @@ func mergeReconcilerConfig(base, target *ReconcilerConfig) *ReconcilerConfig { return &merged } -// ResolveTargetFromAnnotations extracts the effective target from a CR's annotations. -// Resolution order: -// 1. serve-alias annotation (most specific) -// 2. serve-target annotation (primary target) -// 3. Empty string (no target found) -func ResolveTargetFromAnnotations(annotations map[string]string) string { - if annotations == nil { - return "" - } - - // 1. Check alias first (most specific) - if alias, ok := annotations[labels.AnnotationServeAlias]; ok && alias != "" { - return alias - } - - // 2. Fall back to target - if target, ok := annotations[labels.AnnotationServeTarget]; ok && target != "" { - return target - } - - return "" -} - type ConversionVersionSpec struct { Version string `json:"version" yaml:"version"` Spec map[string]interface{} `json:"spec" yaml:"spec"` diff --git a/pkg/types/types_operatorbox.go b/pkg/types/types_operatorbox.go index 829a8c944..8dd1b154d 100644 --- a/pkg/types/types_operatorbox.go +++ b/pkg/types/types_operatorbox.go @@ -182,6 +182,64 @@ type ReconcilerConfig struct { Queue Queue `yaml:"queue,omitempty" json:"queue,omitempty"` } +// IsDefault returns true when the reconciler should use the GenericReconciler. +// When Default is nil (not declared), it defaults to true. +func (r *ReconcilerConfig) IsDefault() bool { + if r == nil { + return true + } + if r.Default == nil { + return true + } + return *r.Default +} + +// HasHooksDecl reports whether a hook declaration exists. +func (r *ReconcilerConfig) HasHooksDecl() bool { + if r == nil { + return false + } + return r.Hooks != nil +} + +// HasConstructorDecl reports whether a constructor declaration exists. +func (r *ReconcilerConfig) HasConstructorDecl() bool { + if r == nil { + return false + } + return r.ConstructorDecl != nil +} + +// IsEmpty reports whether the reconciler config has no meaningful settings. +// Used to skip unnecessary config blocks in the Katalog. +func (r *ReconcilerConfig) IsEmpty() bool { + if r == nil { + return true + } + if r.Default != nil { + return false + } + if r.Hooks != nil { + return false + } + if r.ConstructorDecl != nil { + return false + } + if r.Profile != "" { + return false + } + if r.Workers != 0 { + return false + } + if r.Resync.Duration != 0 { + return false + } + if !r.Queue.IsEmpty() { + return false + } + return true +} + // OperatorBoxConfig is the per-CRD configuration block in a Katalog. It controls // which reconciler implementation runs, what resources to manage, and how lifecycle // hooks, status, admission, autoscaling, and rollback behave. From e0f88bf7ad1cdd14cc755e4df2ab1ce9019b37c8 Mon Sep 17 00:00:00 2001 From: ialexeze Date: Mon, 17 Aug 2026 14:28:05 +0000 Subject: [PATCH 3/4] docs(reusability): add Reusability and Composition concepts section Covers the system-level view of reuse across every layer of Orkestra: core infrastructure, building blocks (Motifs/Katalogs/Komposers/include), user-defined vocabulary (notes and profiles), args, and per-target reconcile strategies. Each page links to related concept pages. --- documentation/concepts/index.md | 8 + documentation/concepts/reusability/01-core.md | 57 +++++++ .../reusability/02-building-blocks.md | 141 ++++++++++++++++++ .../concepts/reusability/03-user-defined.md | 98 ++++++++++++ documentation/concepts/reusability/04-args.md | 86 +++++++++++ .../concepts/reusability/05-targets.md | 139 +++++++++++++++++ documentation/concepts/reusability/index.md | 35 +++++ 7 files changed, 564 insertions(+) create mode 100644 documentation/concepts/reusability/01-core.md create mode 100644 documentation/concepts/reusability/02-building-blocks.md create mode 100644 documentation/concepts/reusability/03-user-defined.md create mode 100644 documentation/concepts/reusability/04-args.md create mode 100644 documentation/concepts/reusability/05-targets.md create mode 100644 documentation/concepts/reusability/index.md diff --git a/documentation/concepts/index.md b/documentation/concepts/index.md index 6b18d3a66..9054563bb 100644 --- a/documentation/concepts/index.md +++ b/documentation/concepts/index.md @@ -110,6 +110,14 @@ The [Operator of Operators](operator-of-operators/) pattern lets one Orkestra op --- +## Reusability and Composition + +[Reusability and Composition](reusability/) is the system-level view of how every layer in Orkestra — the runtime, Motifs, Katalogs, Komposers, notes, profiles, args, and targets — follows the same principle: share what is common, declare what varies. + +→ [Read: Reusability and Composition](reusability/) + +--- + ## Every CRD is a Live API Every CRD you declare in a Katalog becomes a live HTTP API outside the cluster — health, config, CR list, CR detail, and events, all served from in-memory cache on port 8080. This is the transport layer for the Control Center, ONCOP, and operator autoscaling. diff --git a/documentation/concepts/reusability/01-core.md b/documentation/concepts/reusability/01-core.md new file mode 100644 index 000000000..d7eb2c676 --- /dev/null +++ b/documentation/concepts/reusability/01-core.md @@ -0,0 +1,57 @@ +# The Core — Runtime, Gateway, and Control Center + +The first and most consequential reuse in Orkestra is the core infrastructure itself. + +Every operator built on Orkestra runs on the same engine. The reconcile loop, event watching, retry behaviour, leader election, health tracking, dependency ordering — none of this is written by the operator author. It is supplied by the runtime, once, and shared by every operator that runs on it. + +This means an operator author's entire responsibility is: what should happen when a CR arrives or changes. The runtime handles everything before and after that point. + +--- + +## One runtime, many operators + +The same runtime processes events for every CRD it is configured to watch. A cluster can run operators for databases, pipelines, tenant namespaces, and feature deployments — all on the same engine, with the same operational model. + +**One upgrade, all operators.** When the runtime gains a capability — gateway integration, per-target routing, autoscaling, dependency ordering — every operator using it gains it immediately, without changes to operator code. + +**One operational model.** Health endpoints, reconcile stats, dependency graphs, graceful shutdown — identical across every operator. A platform team running ten operators does not learn ten operational models; they learn one. + +**One test surface.** The runtime's behaviour is tested once. Operator authors test their own logic — not the retry mechanics, not informer cache semantics, not the workqueue. Those come from the shared layer and are not the operator's concern. + +--- + +## The gateway is shared infrastructure + +The gateway — intent translation, token validation, schema catalog, cluster routing, surface conflict detection — is part of the shared layer. An operator author does not build a REST API for their CRD. They declare `serve:` in their Katalog and the gateway handles every caller concern. + +This means the same gateway instance serves every CRD in every Katalog the runtime loads. One token, one schema catalog endpoint, one `ork serve apply` command — regardless of which operator or which CRD is being called. + +The gateway is also where targets become visible to callers. A caller picks a named surface. The gateway stamps the routing decision onto the CR before apply. The runtime reads it and reconciles accordingly. From the caller's side, there is one API and one schema. The routing is transparent. + +--- + +## The Control Center spans all runtimes + +The Control Center connects to any number of running operators — local or remote, across clusters. It reads the same endpoint every Orkestra runtime exposes and surfaces reconcile activity, health state, CR listings, and dependency graphs for all of them in one place. + +An operator becomes visible in the Control Center the moment it starts. Nothing to register, no plugin to write, no dashboard to configure. The shared runtime contract does it automatically. + +This is reuse at the observability level: one interface, any number of operators, zero per-operator configuration. + +--- + +## Where the operator author's work begins + +The runtime delivers a CR to the operator at the point of reconcile. Everything that led to that moment — watching for changes, deduplicating events, managing retries, ordering dependencies — was the runtime's responsibility. Everything from that point forward is the operator author's. + +Expressed in the Katalog alone, that boundary is a set of declarations: what to validate, what to mutate, what status fields to compute, what external calls to make, when to gate reconciliation. No Go code required. + +When Go is needed, the operator author writes hooks or a constructor. Hooks are called by the runtime for each reconcile event. A constructor produces a reconciler that the runtime runs. In both cases, the runtime owns the loop and the operator owns the logic. + +--- + +## Related topics + +- [Orkestra Core](../../orkestra-core/index.md) — runtime, gateway, and Control Center in depth +- [OperatorBox](../operatorbox/index.md) — the execution unit each CRD becomes inside the runtime +- [Live API](../live-api/index.md) — the HTTP contract every runtime exposes for the Control Center and ONCOP diff --git a/documentation/concepts/reusability/02-building-blocks.md b/documentation/concepts/reusability/02-building-blocks.md new file mode 100644 index 000000000..0ac29231b --- /dev/null +++ b/documentation/concepts/reusability/02-building-blocks.md @@ -0,0 +1,141 @@ +# Building Blocks — Motifs, Katalogs, Komposers + +Orkestra's composition model is layered. Each layer is a reusable unit that can be authored independently, versioned, distributed, and composed into a larger whole. + +```text +Motif — a named, reusable fragment of a Katalog + ↓ imported by +Katalog — a complete operator declaration + ↓ imported by +Komposer — aggregates multiple Katalogs into one runtime +``` + +--- + +## Motifs + +A Motif is a Katalog fragment packaged for reuse. It can contain anything a Katalog section can: validation rules, mutation rules, hook declarations, resource templates, notes, profiles, external calls. + +An operator team publishes a Motif for tenant isolation and RBAC. Every team that provisions namespaces imports it with their own parameters. When the policy changes, the Motif is updated once — no changes in any consumer Katalog. + +```yaml +# katalog.yaml +spec: + crds: + namespace-provisioner: + imports: + - motif: ../motifs/tenant-isolation/motif.yaml + with: + namespace: "{{ .spec.targetNamespace }}" + team: "{{ .spec.team }}" + + - motif: ../motifs/tenant-rbac/motif.yaml + with: + team: "{{ .spec.team }}" + targetNamespace: "{{ .spec.targetNamespace }}" + owner: "{{ .spec.owner }}" +``` + +`imports:` is declared per-CRD. Each import names a Motif (local path or OCI reference) and passes `with:` values — static strings or template expressions evaluated per-CR. The consumer declares which version to pull. The Motif author publishes updates independently. + +--- + +## Include — sharing within a unit + +`include:` reads a file in the same directory tree and merges it at load time. By the time the runtime starts, every `include:` has been resolved — the runtime sees only the merged result. + +Include is available almost everywhere a Katalog can declare structure: + +| Location | What include merges | +|----------|---------------------| +| Validation rules | A shared `validation-rules.yaml` across multiple CRDs | +| Mutation rules | Common defaulting logic extracted to a file | +| External call configs | Reusable HTTP call declarations | +| Notes | A function library shared by multiple CRDs in the same Katalog | +| Profiles | Profile sets declared once, included where needed | +| Serve target entries | Token and config declarations for a named surface | +| Conversion webhooks | Shared conversion logic | + +A Katalog with dozens of CRDs does not repeat common declarations. Validation rules that apply to every CRD live in one file. Profile sets are declared once. External call patterns are shared. + +--- + +## E2E and Simulate — test composition + +The same composition model extends to tests. A Simulate file can import other Simulate files; an E2E suite can import other E2E suites. A platform team can aggregate test coverage across multiple operator packages without duplicating test declarations. + +```yaml +# platform-e2e.yaml +apiVersion: orkestra.orkspace.io/v1 +kind: E2E +metadata: + name: platform-suite + +imports: + - ./operators/database/e2e.yaml + - ./operators/cache/e2e.yaml + - ./operators/network-policy/e2e.yaml +``` + +Each entry is a bare file path. The aggregated suite runs all imported suites in sequence. Assertions within each imported file remain scoped to their CRDs — the aggregator does not merge or flatten them. + +--- + +## Komposer + +A Komposer is the top-level aggregator. It imports multiple Katalogs from local files and merges them into a single runtime. + +```yaml +# komposer.yaml +apiVersion: orkestra.orkspace.io/v1 +kind: Komposer +metadata: + name: platform-operators + +imports: + registry: + - oci://ghcr.io/myorg/patterns/deployment-stack:v1.0.0 + files: + - ./database-operator/katalog.yaml + - ./cache-operator/katalog.yaml + - ./network-policy/katalog.yaml +``` + +The operator teams maintain and version their Katalogs independently. The platform team composes them in the Komposer. + +### Overriding a public pattern + +A Katalog declares the schema it was written for — the CRD's API types, the field paths its hooks read. When you import a public Katalog pattern from a registry, those API types may not match your internal CRD. + +The Komposer lets you replace the `apiTypes` block — the schema mapping — without touching any of the pattern's logic. Hook behaviour, validation rules, profiles, and gateway declarations are inherited unchanged. Only the API shape is replaced with your own. + +This is how a community-published operator pattern becomes an internal operator: import the pattern, declare your CRD's schema in the override, keep everything else. + +```yaml +# komposer.yaml +apiVersion: orkestra.orkspace.io/v1 +kind: Komposer +metadata: + name: platform-operators + +imports: + registry: + - oci://ghcr.io/postgres/patterns/postgres:v1.0.0 + +# Replace the upstream apiTypes with internal ones +spec: + crds: + postgres: + apiTypes: + group: myorg.io + version: v1 + kind: MyOrgDatabase +``` + +--- + +## Related topics + +- [Orkestra Registry](../../orkestra-registry/index.md) — community Motifs, Katalogs, and Komposers available to import +- [Composition](../composition/index.md) — imports and include explained in depth +- [Schema reference](../../reference/schema/index.md) — Motif, Katalog, Komposer, E2E, and Simulate schemas diff --git a/documentation/concepts/reusability/03-user-defined.md b/documentation/concepts/reusability/03-user-defined.md new file mode 100644 index 000000000..38f08b715 --- /dev/null +++ b/documentation/concepts/reusability/03-user-defined.md @@ -0,0 +1,98 @@ +# User-Defined Reuse — Notes and Profiles + +Two concepts in Orkestra let operator authors define their own reusable vocabulary: **notes** and **profiles**. Both are declared once and used anywhere expressions or configuration blocks appear. + +--- + +## Notes — a shared function library + +Notes are named template functions available inside every `{{ }}` expression in a Katalog: status fields, validation conditions, enqueue gates, reconcile gates, external call URLs, args values, and more. + +A note defined once is callable from every expression in the Katalog: + +```yaml +notes: + functions: + - name: inBusinessHours + expression: '{{ and weekday (timeInWindow "09:00" "18:00") }}' + + - name: replicasByTier + expression: '{{ if eq .spec.tier "enterprise" }}10{{ else if eq .spec.tier "standard" }}3{{ else }}1{{ end }}' + + - name: primaryRegion + expression: '{{ index .spec.regions 0 }}' +``` + +Every CRD in that Katalog can now use `{{ inBusinessHours }}`, `{{ replicasByTier }}`, `{{ primaryRegion }}` — in enqueue gates, in status fields, in validation conditions, in external call configs. The logic is declared once and has one place to change. + +Notes are also the primary way to encapsulate multi-step template logic that would otherwise be repeated inline. A `replicasByTier` note is clearer than a nested `{{ if eq }}` block copied into three status fields and two validation rules. + +See [Notes](../notes/index.md) for the full contract: purity, nil-safety, built-in library, and how notes interact with the resolver. + +--- + +## Profiles — named configuration presets + +A profile is a named preset that expands into a complete configuration block at load time. Where notes abstract template expressions, profiles abstract YAML structure. + +A profile named `high-throughput` might expand into: + +```yaml +operatorBox: + reconciler: + workers: 8 + resync: 10s + queue: + maxDepth: 2000 + preReconcile: + reconcileGate: + when: + - field: '{{ .status.phase }}' + equals: "Ready" +``` + +Declare it once. Every CRD that needs high-throughput reconciliation references the profile name — no repeated YAML, no drift between CRDs that should behave identically. + +Profiles are resolved at load time. The runtime never sees a profile reference — only the expanded values. This means profiles compose cleanly with Komposer overrides: an override applied on top of profile expansion works on concrete values, not abstract names. + +See [User-defined profiles](../profiles/10-user-defined-profiles.md) for profile definition, scoping, and how they interact with Komposer overrides. + +--- + +## Notes and profiles together + +Notes and profiles solve different parts of the same problem. Profiles handle structural repetition — the same YAML blocks across multiple CRDs. Notes handle expression repetition — the same template logic in multiple `{{ }}` contexts. + +A Katalog that uses both eliminates repetition at both levels: + +```yaml +notes: + functions: + - name: inBusinessHours + expression: '{{ and weekday (timeInWindow "09:00" "18:00") }}' + +profiles: + - name: gated-reconcile + spec: + preReconcile: + reconcileGate: + when: + - field: '{{ inBusinessHours }}' + equals: "true" + +crds: + app-eu: + profile: gated-reconcile + app-us: + profile: gated-reconcile +``` + +The business-hours logic is declared once as a note. The gate structure is declared once as a profile. Both CRDs get identical behaviour from one source of truth. + +--- + +## Related topics + +- [Notes](../notes/index.md) — full contract: built-in functions, purity, nil-safety, and resolver behaviour +- [Profiles](../profiles/index.md) — all built-in profiles and how to define your own +- [Time-Dependent Workloads](../temporal/index.md) — time notes and business-hours patterns in practice diff --git a/documentation/concepts/reusability/04-args.md b/documentation/concepts/reusability/04-args.md new file mode 100644 index 000000000..e448167bf --- /dev/null +++ b/documentation/concepts/reusability/04-args.md @@ -0,0 +1,86 @@ +# Args — One Binary, Many Behaviours + +`args:` lets a single reconcile implementation behave differently across environments, tiers, or gateway surfaces — with all variation declared in the Katalog. + +--- + +## What args replace + +Without args, variation means code changes: build flags, environment variables read at startup, separate binaries per tier, configuration files baked into images. Each approach ties deployment variation to code or build decisions. + +With args, the hook or constructor implementation stays the same everywhere. The Katalog declares what values to pass. The same binary runs in staging and production, in an EU cluster and a US cluster, for a free-tier customer and an enterprise customer — and behaves differently because the args differ, not the code. + +--- + +## Static and dynamic values + +Args have two modes that compose freely: + +**Static** values — strings, integers, booleans without `{{ }}` — are fixed at startup. Every CR reconciled by that operator sees the same value. Tier names, region identifiers, feature flags, resource limits: these belong here. + +**Dynamic** values — strings containing `{{ }}` — are evaluated per-CR at reconcile time using the full expression language, including user-defined notes. The expression has access to the CR's spec, status, and metadata, and to the intent payload when the CR arrived via the gateway. + +```yaml +args: + tier: enterprise # static — same for every CR + maxReplicas: 50 # static + region: "eu-west-1" # static + + tenantId: "{{ .spec.tenantId }}" # dynamic — per-CR field + inBusinessHours: '{{ inBusinessHours }}' # dynamic — note function + featureEnabled: "{{ .external.flags.body }}" # dynamic — from external call +``` + +Orkestra evaluates any string containing `{{ }}` at any depth in the args map. The hook or constructor receives the fully resolved values — it never sees template syntax. + +--- + +## Hook args and constructor args + +Args work the same way for hooks and constructors, with one difference in timing. + +**Hook args** are evaluated fresh at each reconcile event. The hook receives values for the specific CR at that specific moment. A business-hours flag re-evaluates every cycle. A per-CR field that changes between reconciles reflects the new value. Hook args are for anything that needs to track CR state or time. + +**Constructor args** are evaluated once when the operator starts, before any CR is reconciled. They are best for configuration that is stable for the lifetime of the operator — tier settings, region identifiers, resource limits. A constructor reads its args and builds a reconciler configured for the environment it is running in. + +--- + +## Args and targets + +Per-target declarations can carry their own `args:`, which are merged with the CRD-level args. Target-specific args override the keys they declare; all other keys are inherited. This lets the same hook or constructor serve multiple gateway surfaces from one Katalog — the surface determines the args, and the args determine the behaviour. + +```yaml +operatorBox: + reconciler: + hooks: + args: + featureEnabled: "{{ .external.flags.body }}" # CRD-level default + +serve: + target: + v2-enabled: + operatorBox: + reconciler: + hooks: + args: + featureEnabled: "true" # always on for this surface + + v2-disabled: + operatorBox: + reconciler: + hooks: + args: + featureEnabled: "false" # always off for this surface +``` + +The hook reads `featureEnabled` and acts on it. It has no awareness of which target surface the CR arrived from. The Katalog determines what value it sees. + +See [Reconcile Strategies](05-targets.md) for the full picture of per-target reconcile strategies. + +--- + +## Related topics + +- [Typed Operators — Reusability](../typed-operators/06-reusability.md) — one binary serving multiple CRD kinds and multiple environments via args +- [OperatorBox](../operatorbox/index.md) — where args are declared and how they are resolved +- [Reconcile Strategies](05-targets.md) — per-target args and full operatorBox variation diff --git a/documentation/concepts/reusability/05-targets.md b/documentation/concepts/reusability/05-targets.md new file mode 100644 index 000000000..358d1ee7c --- /dev/null +++ b/documentation/concepts/reusability/05-targets.md @@ -0,0 +1,139 @@ +# Targets — One CRD, Multiple Reconcile Strategies + +In the `mixed-operator pattern` example pack, a single runtime runs three different CRDs — each with its own operatorBox. One CRD is purely declarative. One uses a typed hooks binary. One uses a constructor. The Komposer composes them and they run together. + +Targets are the same idea, applied to a single CRD. + +Instead of three CRDs with three different operatorBoxes, you have one CRD with three named surfaces — and each surface has its own operatorBox. Each target defines what gets created, how reconciliation works, and under what conditions it runs. The schema is shared. The gateway surface is shared. What varies is declared per target. + +!!! tip "Try the Mixed Operator pattern" + ```bash + ork init --pack advanced/11-mixed-operator-pattern + ``` + Follow the steps in the README. + +--- + +## Each target has its own operatorBox + +A target's operatorBox works exactly like a CRD-level operatorBox. It can be declarative — creating resources without any Go code. It can declare hooks. It can declare a constructor. It can import motifs. It can define preReconcile gates that control when reconciliation runs. + +```yaml +serve: + target: + standard: + operatorBox: + onCreate: + deployments: + - image: "{{ .spec.image }}" # declarative — no hooks needed + + managed: + operatorBox: + preReconcile: + enqueueGate: + when: + - field: '{{ inBusinessHours }}' + equals: "true" + reconciler: + hooks: + location: github.com/myorg/my-operator/hooks + function: ManagedHooks + alias: managed + args: + tier: managed + + custom: + operatorBox: + reconciler: + default: false + constructor: + location: github.com/myorg/my-operator/reconciler + function: NewCustomReconciler + alias: custom +``` + +A caller picks a surface. The runtime applies that surface's operatorBox — its gates, its hooks or constructor, its declared resources — for every CR that arrives from that surface. + +--- + +## What a target's operatorBox can declare + +Anything a CRD-level operatorBox can declare is available per target: + +| Declaration | What it does | +|-------------|-------------| +| `onCreate` resources | Declaratively create Deployments, Services, ConfigMaps, etc. when the CR is created | +| `reconciler.hooks` | A typed hook binary that runs on each reconcile event | +| `reconciler.constructor` | A custom reconciler that owns the full reconcile loop | +| `preReconcile.enqueueGate` | A condition that must be true before a CR is enqueued | +| `preReconcile.reconcileGate` | A condition that must be true before reconciliation proceeds | +| `imports` | One or more Motifs, composing shared behaviour into the target | +| `args` | Configuration values passed to hooks or the constructor | +| `status.fields` | Status fields written after each reconcile cycle | + +The CRD-level operatorBox is still the base. A target's operatorBox extends or overrides it for that surface. + +--- + +## Targets in a Katalog + +```yaml +spec: + crds: + blockchainapp: + apiTypes: ... + + operatorBox: # base — applies to all targets unless overridden + reconciler: + workers: 2 + resync: 30s + + serve: + target: + v2-enabled: + primary: true + operatorBox: + preReconcile: + enqueueGate: + when: + - field: '{{ inBusinessHours }}' + equals: "true" + reconciler: + hooks: + location: github.com/myorg/blockchain/hooks + function: BlockchainHooks + alias: bchooks + args: + featureEnabled: "true" + + v2-disabled: + operatorBox: + reconciler: + hooks: + location: github.com/myorg/blockchain/hooks + function: BlockchainHooks + alias: bchooks + args: + featureEnabled: "false" + + v2-custom: + operatorBox: + reconciler: + default: false + constructor: + location: github.com/myorg/blockchain/reconciler + function: NewBlockchainReconciler + alias: bcctor +``` + +Three surfaces. One CRD. The same schema, the same informer, the same gateway — with different operatorBoxes behind each surface. + +When a CR moves from one surface to another, the runtime cleans up what the previous surface created before the new surface takes over. + +--- + +## Related topics + +- [OperatorBox](../operatorbox/index.md) — the full operatorBox schema: onCreate, reconciler, preReconcile, status, and imports +- [Reconciler Model](../reconciler-model/index.md) — how a CR moves through the reconcile loop from enqueue to status patch +- [Args](04-args.md) — configuration-only variation across targets without changing reconcile logic diff --git a/documentation/concepts/reusability/index.md b/documentation/concepts/reusability/index.md new file mode 100644 index 000000000..54d7979b3 --- /dev/null +++ b/documentation/concepts/reusability/index.md @@ -0,0 +1,35 @@ +# Reusability and Composition in Orkestra + +Reusability is not a single feature in Orkestra — it is the organizing principle the entire framework is built on. Every layer, from the runtime engine to individual field values, follows the same idea: share what is common, declare what varies. + +This shapes how Orkestra operators are built, composed, and operated. An operator author does not write infrastructure. A platform team does not duplicate configuration. An organization does not repeat reconcile logic across operators that differ only in configuration or routing. + +| Layer | What is shared | What varies | +|-------|---------------|-------------| +| Core | Reconcile engine, gateway, observability | Operator declarations, hook logic | +| Composition | Motif libraries, Katalog packages | Local overrides, environment values | +| Vocabulary | Notes (functions), Profiles (presets) | The expressions and CRDs that use them | +| Parameterisation | Reconcile logic | Args declared per-environment or per-target | +| Reconcile strategies | CRD schema, informer, gateway surface | What happens when a CR arrives | + +--- + +## Pages in this section + +| Page | What it covers | +|------|----------------| +| [The Core](01-core.md) | The runtime as shared foundation — one engine for every operator; gateway and Control Center across all of them | +| [Building Blocks](02-building-blocks.md) | Motifs, Katalogs, Komposers — composition, include, test aggregation | +| [User-Defined Reuse](03-user-defined.md) | Notes and Profiles — vocabulary and presets defined once, used everywhere | +| [Args](04-args.md) | Args — the same logic, different behaviour per environment or surface | +| [Reconcile Strategies](05-targets.md) | Reconcile strategies — multiple behaviours from a single CRD via named targets | + +--- + +## Where to go next + +- [Orkestra Core](01-core.md) +- [Composition](02-building-blocks.md) +- [Vocabulary](03-user-defined.md) +- [Parameterisation](04-args.md) +- [Reconcile Strategies](05-targets.md) From 4c9028270abaf13aa40b6575e0f2cb4269fa176f Mon Sep 17 00:00:00 2001 From: ialexeze Date: Mon, 17 Aug 2026 19:04:51 +0000 Subject: [PATCH 4/4] fix(target-operatorbox): wire per-target constructor from ReconcilerRegistry Per-target entries with reconciler.default: false and a ConstructorDecl declared were not getting their Constructor wired in addReconcilers(). The function skipped the entire CRD when the top-level DefaultReconcile() returned true, never inspecting target-level entries. Added wirePerTargetConstructors which iterates per-target entries, finds those with default: false and a ConstructorDecl, and wires the constructor from ReconcilerRegistry using the CRD-level GVK. --- pkg/katalog/validate_hooks_reconcilers.go | 40 ++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/pkg/katalog/validate_hooks_reconcilers.go b/pkg/katalog/validate_hooks_reconcilers.go index 72588cb5f..53625f80a 100644 --- a/pkg/katalog/validate_hooks_reconcilers.go +++ b/pkg/katalog/validate_hooks_reconcilers.go @@ -24,9 +24,12 @@ func (k *Katalog) addReconcilers() error { if !crd.IsDynamic() { if crd.DefaultReconcile() { - // Per-target constructors (reconciler.default: false on a target operatorBox) - // are owned by addTargetConstructors, which reads TargetReconcilerRegistry. - // Nothing to do here for those targets. + // Wire per-target entries that opt out of the default reconciler + // (reconciler.default: false). These use the same ReconcilerRegistry + // keyed by the CRD's GVK, but are stored on the target's OperatorBox. + if err := wirePerTargetConstructors(name, &crd); err != nil { + return err + } crd.OperatorBox = rc k.enabledCRDs[name] = crd continue @@ -153,6 +156,36 @@ func (k *Katalog) addTargetHooks() error { } // --------------------------------------------------------------------------------- +// wirePerTargetConstructors wires constructors for per-target entries that have +// reconciler.default: false set on their own OperatorBox. These targets share the +// CRD-level GVK and look up their constructor in ReconcilerRegistry, storing it +// directly on targetCfg.OperatorBox.Constructor. +func wirePerTargetConstructors(crdName string, crd *orktypes.CRDEntry) error { + if !crd.HasServeTargetEntries() { + return nil + } + for targetName, targetCfg := range crd.Serve.Target.Entries { + if targetCfg.OperatorBox == nil || targetCfg.OperatorBox.Reconciler == nil { + continue + } + rec := targetCfg.OperatorBox.Reconciler + if rec.Default == nil || *rec.Default || rec.ConstructorDecl == nil { + continue + } + fn, ok := orktypes.ReconcilerRegistry[crd.GroupVersionKind] + if !ok { + return fmt.Errorf( + "CRD %q target %q: reconciler.default: false declared but "+ + "no ReconcilerRegistry entry for this GVK — re-run ork generate registry", + crdName, targetName, + ) + } + targetCfg.OperatorBox.Constructor = fn + crd.Serve.Target.Entries[targetName] = targetCfg + } + return nil +} + // addTargetConstructors wires per-target constructor factories from // TargetReconcilerRegistry onto CRDEntry.TargetReconcilerFactories. func (k *Katalog) addTargetConstructors() error { @@ -194,4 +227,3 @@ func (k *Katalog) addTargetConstructors() error { } return nil } -