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:
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:
- Resolve
spec.serviceAccountRef→ upstream SA UUID - Mint a token if none exists (capturing the plaintext from the one-shot response)
- Materialise the plaintext into a per-namespace Secret with
token+endpointkeys, owner-referenced to the CR - 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=remotewithout anhttps://upstreamUrlblockType=sha256with a non-hex patternexpiresIn=30mtypos 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.txtCRDs 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 code | Meaning |
|---|---|
RefUnresolved | spec.projectRef (etc.) doesn't match a sibling CR |
CreateFailed | Upstream POST returned non-2xx |
UpdateFailed | Upstream PATCH returned non-2xx |
Reconciling | Mid-flight; transient |
SecretWriteFailed | Token-controller-only — Secret materialisation failed |
CredsInvalid | The configured API token is rejected |
The Troubleshooting page indexes each reason code → resolution.