Skip to content

Operator architecture

The operator runs a single controller-runtime manager binary with seven controllers — one per CRD — sharing a typed HTTP client to the OrbitalReg REST API.

Canonical source: docs/operator/architecture.md.

Manager wiring

┌─────────────────────────────────────────────────────────┐
│  controller-runtime manager                             │
│                                                         │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐   │
│  │ Project ctrl │ │ Repo ctrl    │ │ SA ctrl      │   │
│  └──────┬───────┘ └──────┬───────┘ └──────┬───────┘   │
│         │                │                │            │
│         └────────────────┴────────────────┘            │
│                          │                             │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐   │
│  │ Token ctrl   │ │ Retention    │ │ SecBlock     │   │
│  │              │ │ Policy ctrl  │ │ ctrl         │   │
│  └──────┬───────┘ └──────┬───────┘ └──────┬───────┘   │
│         │                │                │            │
│  ┌──────▼────────────────┴────────────────▼──────┐    │
│  │ Webhook subscription ctrl                     │    │
│  └────────────────────────┬───────────────────────┘    │
└──────────────────────────┼─────────────────────────────┘

              ┌────────────▼────────────┐
              │ shared HTTP client      │
              │ (Bearer token + retry)  │
              └────────────┬────────────┘

                ┌──────────▼───────────┐
                │ OrbitalReg API       │
                │ (REST, JSON)         │
                └──────────────────────┘

Adoption + drift loop

Each controller's Reconcile follows the same shape:

go
func (r *Reconciler) Reconcile(ctx, req) (Result, error) {
    cr := &v1alpha1.MyCR{}
    r.Get(ctx, req.NamespacedName, cr)

    // 1) Handle deletion via finalizer
    if !cr.DeletionTimestamp.IsZero() {
        return r.finalize(ctx, cr)
    }

    // 2) Resolve parent refs (project, repo, etc.)
    parent, err := r.resolveRefs(ctx, cr)
    if err != nil {
        return r.markUnresolved(ctx, cr, err)
    }

    // 3) Adopt or create upstream row
    upstream, err := r.adoptOrCreate(ctx, cr, parent)
    if err != nil {
        return r.markCreateFailed(ctx, cr, err)
    }

    // 4) Compute state→plan diff and PATCH
    if drift := diff(cr.Spec, upstream); !drift.IsZero() {
        if err := r.patch(ctx, upstream.ID, drift); err != nil {
            return r.markUpdateFailed(ctx, cr, err)
        }
    }

    // 5) Mark Synced + Ready
    return r.markSynced(ctx, cr)
}

The exact diff() implementation is per-resource — for example, retention policies normalise whitespace-only rule drift away so a JSONB round-trip doesn't loop, and webhook subscriptions only push the HMAC secret when the referenced Kubernetes Secret's metadata.resourceVersion advances.

Token reconciliation specifics

OrbitalRegServiceAccountToken is the only namespace-scoped CRD — because it materialises its plaintext token into a Kubernetes Secret in the same namespace. The reconcile loop:

  1. Resolve spec.serviceAccountRef → upstream SA UUID
  2. Mint a token if none exists (capturing the plaintext from the one-shot response)
  3. Materialise the plaintext into a per-namespace Secret with token + endpoint keys, owner-referenced to the CR
  4. On (name, scopes, expiresIn) drift, rotate the token (rolling-back the new mint if the Secret write fails)

status.specHash carries a SHA-256 of the spec fields that, when they change, force a rotation. This ensures rotations are deterministic — the same spec mutation triggers the same rotation, regardless of when the next reconcile fires.

Validation webhooks

A separate --enable-webhooks mode runs a ValidatingAdmissionWebhook for each CRD. Each webhook implements admission.CustomValidator and rejects cross-field mistakes the CRD schema markers can't reach:

  • kind=remote without an https:// upstreamUrl
  • blockType=sha256 with a non-hex pattern
  • expiresIn=30m typos that would mint a half-hour token instead of 30 days
  • Reserved slug or SA-name prefixes that would round-trip an opaque 403 from the upstream POST

Validation webhooks are off by default for the zero-config single-binary deployment. Enable via the chart's webhooks.enabled=true (which also wires up the cert-manager Certificate and Service for the webhook endpoint).

Helm chart layout

charts/orbitalreg-operator/
├── Chart.yaml
├── values.yaml
├── crds/                   (templated, not install-only)
│   ├── orbitalreg-project.yaml
│   └── …                   (one per CRD)
├── files/
│   └── crds/               (sync target — kept in lock-step with config/crd/bases)
└── templates/
    ├── service-account.yaml
    ├── cluster-role.yaml
    ├── cluster-role-binding.yaml
    ├── deployment.yaml
    ├── webhooks/           (cert-manager + Service + Webhooks)
    └── NOTES.txt

CRDs are templated (rather than the install-only crds/ directory) so helm upgrade rolls forward CRD schema changes the same way it rolls forward Deployments. Each CRD carries helm.sh/resource-policy: keep so a helm uninstall does not cascade-delete every CR + minted Secret.

Failure modes

Reason codeMeaning
RefUnresolvedspec.projectRef (etc.) doesn't match a sibling CR
CreateFailedUpstream POST returned non-2xx
UpdateFailedUpstream PATCH returned non-2xx
ReconcilingMid-flight; transient
SecretWriteFailedToken-controller-only — Secret materialisation failed
CredsInvalidThe configured API token is rejected

The Troubleshooting page indexes each reason code → resolution.

Released under the Apache-2.0 License.