Reusability

5 min read

A typed operator in Orkestra is reusable at two independent levels: API and configuration.


API reusability — one binary, many CR types

Whatever you declare under apiTypes is what Orkestra delivers to your hook or constructor. Multiple teams can each ship a Katalog with their own CRD name and Kind — all in the same cluster, all backed by the same binary.

This is the actual reusability: the Kind is different in each Katalog. Team A ships a PostgresCluster operator, team B ships a ManagedDatabase operator — both running in the same cluster, both backed by the same hooks binary.

# team-a/katalog.yaml
crds:
  postgres-cluster:
    apiTypes:
      group: db.team-a.io
      version: v1
      kind: PostgresCluster
      plural: postgresclusters
      object: PostgresCluster
      objectList: PostgresClusterList
      location: github.com/myorg/db-hooks/api/v1
    operatorBox:
      reconciler:
        hooks:
          location: github.com/myorg/db-hooks/hooks
          function: DatabaseHooks

# team-b/katalog.yaml — same cluster, different CRD and Kind, same binary
crds:
  managed-database:
    apiTypes:
      group: db.team-b.io
      version: v1alpha1
      kind: ManagedDatabase
      plural: manageddatabases
      object: ManagedDatabase
      objectList: ManagedDatabaseList
      location: github.com/myorg/db-hooks/api/v1alpha1
    operatorBox:
      reconciler:
        hooks:
          location: github.com/myorg/db-hooks/hooks
          function: DatabaseHooks   # same binary

The Go types at location are the author’s — they can differ between Katalogs. Orkestra decodes the API server response into whichever type the hook declared. The hook is unaware of which Katalog loaded it or which Kind triggered the reconcile.


Configuration reusability — one binary, many deployments

args: lets a single hook or constructor handle different environments, tiers, or tenants — all configured from the Katalog without code changes.

Args have two modes: static values (integers, booleans, and plain strings without {{ }}) that are fixed at startup, and dynamic values (strings with {{ }}) that are evaluated per-CR at reconcile time using the full note FuncMap. Both modes compose freely in the same args: block — including inside nested maps, which are recursed into so any string at any depth can be a template.

A platform constructor that serves three tiers from one binary:

crds:
  app-starter:
    operatorBox:
      reconciler:
        constructor:
          location: github.com/myorg/platform/constructor
          function: PlatformConstructor
          args:
            tier: starter
            regions:
              - name: eu-west-1
                primary: true
            resources:
              requests: { cpu: "100m", memory: "128Mi" }
              limits:   { cpu: "500m", memory: "512Mi" }
            features:
              hpa: false
              pdb: false

  app-standard:
    operatorBox:
      reconciler:
        constructor:
          location: github.com/myorg/platform/constructor
          function: PlatformConstructor   # same binary
          args:
            tier: standard
            regions:
              - name: eu-west-1
                primary: true
            resources:
              requests: { cpu: "200m", memory: "256Mi" }
              limits:   { cpu: "1",    memory: "1Gi"   }
            features:
              hpa: true
              hpaMin: 2
              hpaMax: 10
              pdb: true

  app-enterprise:
    operatorBox:
      reconciler:
        constructor:
          location: github.com/myorg/platform/constructor
          function: PlatformConstructor   # same binary
          args:
            tier: enterprise
            regions:
              - name: eu-west-1
                primary: true
              - name: us-east-1
                primary: false
              - name: ap-southeast-1
                primary: false
            resources:
              requests: { cpu: "500m", memory: "512Mi" }
              limits:   { cpu: "4",    memory: "4Gi"   }
            features:
              hpa: true
              hpaMin: 3
              hpaMax: 50
              pdb: true
              multiRegion: true
            compliance:
              soc2: true
              auditRetentionDays: 90

Three CRDs, one constructor, all differences in YAML. The constructor reads the args via kube.Args() and branches on them — no environment variables, no hardcoded tiers, no duplicate binaries.

The tier and feature flags are static. You can also mix in dynamic args that pull from each individual CR at reconcile time:

args:
  tier: enterprise
  # static — same for every CR
  features:
    hpa: true
    hpaMin: 3
  # dynamic — resolved from each CR individually
  tenantId: "{{ .spec.tenantId }}"
  region:   '{{ default "eu-west-1" .spec.region }}'

A single Katalog declaration serves all CRs of that type. Each reconcile resolves tenantId and region from the CR being reconciled — no per-tenant Katalog needed.

func PlatformConstructor(
    kube kubeclient.KubeClient,
    informer cache.SharedIndexInformer,
    ev event.Recorder,
) domain.Reconciler {
    var cfg PlatformArgs
    if err := kube.Args().BindArgs(&cfg); err != nil {
        // surfaced by ork validate before the operator starts
        panic(fmt.Sprintf("invalid platform constructor args: %v", err))
    }
    return &PlatformReconciler{kube: kube, informer: informer, event: ev, cfg: cfg}
}

cfg carries the full tier configuration — resources, features, compliance — resolved once at startup from the Katalog declaration.


Composing both dimensions

Both reusability axes are independent and composable. The same binary can target different API types in different Katalogs AND receive different args in each:

# eu-production/katalog.yaml
crds:
  database:
    apiTypes:
      kind: Database
      location: github.com/myorg/db-operator/api/v1
    operatorBox:
      reconciler:
        hooks:
          function: DatabaseHooks
          args:
            region: eu-west-1
            backup: { enabled: true, scheduleHour: 2 }

# us-production/katalog.yaml — different region and schedule
crds:
  database:
    apiTypes:
      kind: Database
      location: github.com/myorg/db-operator/api/v1
    operatorBox:
      reconciler:
        hooks:
          function: DatabaseHooks   # same binary
          args:
            region: us-east-1
            backup: { enabled: true, scheduleHour: 4 }

Komposer overrides

A Komposer can override specific args without redeclaring the full map. Only declared keys are overridden; siblings are preserved.

# komposer.yaml — enterprise override for SOC2 audit retention
spec:
  crds:
    app-enterprise:
      operatorBox:
        reconciler:
          constructor:
            args:
              compliance:
                auditRetentionDays: 365   # overrides the Katalog value
                                          # soc2: true is preserved from Katalog

This follows the same server-side apply semantics as all other Orkestra merge operations — declare what changes, leave the rest untouched.


What you maintain in one place

LayerWhere it lives
Reconcile logicYour Go binary
API type shapeapiTypes.location in the Katalog
Environment/tier/tenant configurationargs: in the Katalog
Per-deployment overridesKomposer

The Go binary knows nothing about which tier it serves, which region it runs in, or which team deployed it. All of that is in YAML, where operators see and version it alongside their CRD definitions.