Operator components (C4 L3)¶
Inside the Tend operator — the product core. It is the only writer of
authoritative app state (Principle IV): everything an
app is gets declared on the App CR, and everything an app gets is
rendered from it by the reconcilers below. Source lives in
operator/ (Go, Kubebuilder / controller-runtime).
C4Component
title Tend operator — components
Container_Boundary(op, "Tend operator (tend-system)") {
Component(appRec, "AppReconciler", "app_controller.go", "Watches App CRs. Provisions the workload namespace, renders the perimeter and quotas, records build reports, cleans up via the tendtech.io/cleanup finalizer")
Component(sandbox, "Sandbox provisioning", "sandbox.go", "Namespace labels (tendtech.io/app, owner, env), data-gateway ConfigMap (DATA_URL/DATA_SCOPE), renders default-deny + platform-gateways-allow + edge-ingress-allow CiliumNetworkPolicies; clones the Harbor pull secret")
Component(quota, "Cap rendering", "quota.go", "ResourceQuota + LimitRange from spec.resourceCaps")
Component(promo, "Promotion", "promotion.go, promote_api.go", "422 strictness gate on the four governance fields; raises the promotion PR with evidence; approve/reject state transitions")
Component(costRec, "CostReconciler", "cost.go", "Polls OpenCost allocation; drives normal → warning-75 → warning-90 → suspended; enforces via pauseArgoSync + scaleNamespace; consumes tendtech.io/resume")
Component(migrate, "Scope migration", "migrate.go", "One-time 001→003 dataScopes migration ([]string → struct)")
}
Component_Ext(crd, "App CR", "tendtech.io/v1alpha1", "single source of truth")
Component_Ext(tekton, "Tekton PipelineRun", "build results", "results: image, attestation digest, prompt hash, model")
Component_Ext(opencost, "OpenCost", "allocation API")
Component_Ext(argo, "Argo CD Application", "per-app sync")
Rel(appRec, crd, "reconciles / writes status")
Rel(appRec, sandbox, "ensureNamespace, ensureSandbox")
Rel(appRec, quota, "ensureResourceCaps")
Rel(appRec, promo, "promotion state machine")
Rel(appRec, tekton, "reads results of the latest completed run")
Rel(costRec, opencost, "pollAllocation")
Rel(costRec, crd, "writes status.costStatus / suspension")
Rel(costRec, argo, "pause / restore sync on suspend / resume")
UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="1")
AppReconciler — declared intent becomes a governed namespace¶
One reconcile pass takes an App CR to a fully governed workspace:
- Namespace — creates
tend-app-{name}labeledtendtech.io/app,tendtech.io/owner,tendtech.io/env=sandbox, back-referenced to the CR withtendtech.io/app-ref. Thetendtech.io/cleanupfinalizer guarantees release on App deletion. - Perimeter — renders the three Cilium policies (default-deny floor +
gateway-only allow) into the namespace, substituting
__APP_NAMESPACE__in the templates underdeploy/policies/cilium/. It also deletes the retired 001tend-allow-synthetic-datapolicy — reconciliation removes stale enforcement, not just adds new. - Caps — renders ResourceQuota and LimitRange from
spec.resourceCaps; Kyverno'sworkload-within-capspolicy makes sure every pod declares requests/limits so the quota can actually bite. - Data wiring — writes the
tend-sandbox-dataConfigMap holdingDATA_URL/DATA_SCOPE, so a scope or gateway relocation reconciles through configuration alone — the app template never changes. - Build reports — the pipeline never writes the App CR; it publishes its
outcome as PipelineRun results, and the operator reads the latest completed
run and stamps the report annotations itself
(
tendtech.io/sandbox-image,tendtech.io/attestation-digest,tendtech.io/prompt-hash,tendtech.io/model,tendtech.io/built-at), which the reconciler copies intostatus.sandboxArtifactandattestationRefs.
Everything the operator renders carries the
tendtech.io/generated-by=tend-operator label. That label is a checkable claim:
hack/policy-provenance.sh (and CI) asserts zero hand-written per-app
policy objects exist — with a deliberate hand-written counter-example to
prove the check itself works.
Promotion — the 422 gate and the platform-raised PR¶
Promotion is a small state machine on status.state
(see App lifecycle) with one strict entrance: the request
is refused with HTTP 422 unless owner, expiry, dataScopes, and purpose
are all present. Purpose is deliberately never defaulted — a human must
classify the app (including the decisions-about-people flag) before it can
reach production. On a valid request the operator raises the PR in Gitea
from a fixed sandbox snapshot (status.promotion.snapshotRef), attaches the
evidence record, and records the approver group. Merge and rejection flow
back into status.promotion.decision.
CostReconciler — a second state machine, deliberately separate¶
The cost reconciler polls OpenCost's allocation API per app namespace and
drives status.costStatus.state through
normal → warning-75 → warning-90 → suspended, firing each transition once
per crossing (the warning75At/warning90At timestamps make oscillation
safe). Enforcement order matters: it pauses the app's Argo CD sync first —
otherwise selfHeal would immediately undo the scale-down — then scales
workloads to zero and stamps status.suspension.
Two asymmetries are intentional (ADR-0012):
- Fail closed nowhere here. Missing telemetry sets state
unknownand enforces nothing — an app is never punished because OpenCost was down. - Resume doesn't hand-scale. The operator restores the sync policy and lets GitOps bring replicas back, keeping git the source of what runs.
What is not in the operator¶
No webhook server — admission validation and defaulting live in Kyverno
(ADR-0006), because the
creator's identity only exists in the admission request's userInfo and the
platform already has one policy engine. No database — state is the CR. No
deploy pipeline — delivery belongs to Argo CD. The operator's envtest suites
(reconciliation, defaulting, promotion, cost state machine) are the
constitution's required proof for its logic.