Skip to content

Containers (C4 L2)

The level-2 view: the deployable pieces inside Tend and how they talk. One diagram per plane — build & delivery, the runtime data path, and governance & observation — followed by the full inventory.

Everything below runs in-cluster. Two components hold off-cluster egress, and the inference gateway is neither of them: the model gateway reaches the Tier 1 provider endpoint and its auth hosts in the customer's own cloud tenancy, and the package mirror reaches exactly one upstream, registry.npmjs.org, so a build can resolve language packages. The inference gateway's own upstream is in-cluster (http://model-gateway.tend-system.svc:8080) and it has no off-cluster allowance at all. The only trusted writer of authoritative app state is the operator (Principle IV).

Build & delivery plane

From a prompt to a running, attested workload — without the builder or the user ever deploying anything.

C4Container
    title Build & delivery — prompt to governed workload

    Person(user, "Business power user", "describes the app")

    Container_Boundary(build, "Build & delivery") {
        Container(adapter, "Builder adapter", "Go CLI", "The narrow builder contract: inference via gateway, commit with prompt-hash + model trailers, never deploy")
        Container(gitea, "Gitea", "git host, tend-system", "One repo per app (sandbox / production branches) plus the platform repo Argo CD watches")
        Container(tekton, "Tekton + Chains", "tekton-pipelines", "tend-build-attest: fetch-source → build-image (buildah, Harbor-only) → sign-and-attest; Chains signs SLSA v1 provenance")
        Container(harbor, "Harbor", "OCI registry", "Digest-pinned images with cosign signatures and attestations stored beside them; the only IMAGE source. Language packages resolve through the in-boundary package mirror instead")
        Container(argocd, "Argo CD", "argocd", "App-of-apps for the platform; ApplicationSets render each app's sandbox and production overlays")
        Container(kyverno, "Kyverno", "admission, kyverno", "Policy baseline incl. verify-attestation: unattested images refused in every namespace labelled tendtech.io/app — sandbox and production alike. The label is the selector; a namespace without it is not selected")
    }

    Container(appns, "App namespace", "tend-app-{name}", "The app's governed workload")
    Container(operator, "Tend operator", "Go / Kubebuilder, tend-system", "Records the built artifact on App status; owns namespaces, perimeter, quotas")

    Rel(user, adapter, "prompt")
    Rel(adapter, gitea, "commits to sandbox branch", "git + trailers")
    Rel(gitea, tekton, "webhook on push", "EventListener")
    Rel(tekton, harbor, "push image + signed attestation", "OCI")
    Rel(argocd, gitea, "watches app + platform repos", "git")
    Rel(argocd, appns, "syncs manifests")
    Rel(kyverno, appns, "admission gate: attested, Harbor-only, no secrets, within caps")
    Rel(operator, appns, "provisions ns, perimeter, quotas")
    Rel(operator, tekton, "reads the latest completed PipelineRun's results", "image, attestation digest, prompt hash, model")

    UpdateLayoutConfig($c4ShapeInRow="3", $c4BoundaryInRow="1")

The separation is deliberate: the builder produces, Argo CD deploys, Kyverno decides what may run. No component holds more than one of those powers, and the build ServiceAccount has no Kubernetes deploy rights at all — no RoleBinding to create Deployments or Argo Applications (its Gitea credential is still the admin account; narrowing it is open hardening) (Principle VI).

Runtime data path

What a running app can reach — which is exactly two services, and nothing else (governance model).

C4Container
    title Runtime data path — gateway-only

    Container_Boundary(rt, "App namespace (tend-app-{name})") {
        Container(app, "App workload", "generated code — untrusted", "Perimeter: egress ONLY to the two gateways, Keycloak's token endpoint, cluster DNS and its own pods; ingress from its own pods, node probes, and the platform ingress controller")
    }

    Container_Boundary(gw, "Platform gateways (tend-system)") {
        Container(dgw, "Data gateway", "Go, :8080", "The sole data path: validates projected SA token (TokenReview), evaluates declared scopes per request, logs every allow AND deny")
        ContainerDb(logstore, "Access-log store", "SQLite on PVC", "Append-only, retained for app lifetime; can't log → don't serve")
        Container(igw, "Inference gateway", "Go, :8080", "OpenAI-compatible; validates builder OIDC (Keycloak); the only model path")
    }

    Container(keycloak, "Keycloak", "OIDC, tend-system", "Platform identity: builder client-credentials, approver groups, platform query-API tokens")
    SystemDb_Ext(sources, "Data sources", "synthetic (in-cluster), Postgres, S3", "reached only via gateway connectors")
    Container(mgw, "Model gateway", "Bifrost, tend-system", "Provider adaptation, per-app virtual keys, WIF credentials; the only component with off-cluster egress on this path")
    System_Ext(model, "Tier 1 managed endpoint", "customer cloud tenancy; reached over public-internet TLS, not private interconnect")

    Rel(app, dgw, "Bearer: projected SA token (aud tend-data-gateway)", "HTTP :8080")
    Rel(dgw, logstore, "append decision", "every request")
    Rel(dgw, sources, "connector call (only if scope declared)")
    Rel(app, igw, "reachable by perimeter", "HTTP :8080")
    Rel(igw, mgw, "chat completions", "HTTP :8080, in-cluster")
    Rel(mgw, model, "chat completions", "off-cluster egress, TCP 443")
    Rel(igw, keycloak, "validate OIDC (issuer, aud, JWKS)")
    Rel(dgw, keycloak, "OIDC for platform log queries")

    UpdateLayoutConfig($c4ShapeInRow="2", $c4BoundaryInRow="1")

The app authenticates with a projected ServiceAccount token (audience tend-data-gateway) — no per-app secret material exists to leak (ADR-0009). A scope change never alters the network: the perimeter is a static shape, and enforcement moves at the gateway's per-request decisions (ADR-0007).

Governance & observation plane

How declared intent on the App CR becomes enforcement, and how everything that happened becomes queryable.

C4Container
    title Governance & observation

    Container(crd, "App CR", "tendtech.io/v1alpha1", "Single source of truth: owner, expiry, scopes, caps, purpose, state, artifact refs")
    Container(operator, "Tend operator", "Go / Kubebuilder", "AppReconciler: namespace, Cilium perimeter, ResourceQuota/LimitRange, data ConfigMap. CostReconciler: OpenCost poll → warn/suspend state machine")
    Container(opencost, "OpenCost + Prometheus", "opencost ns", "Per-namespace cost allocation")
    Container(evapi, "Evidence & estate API", "Go, :8080", "READ: assembles per-app history from App CR + Gitea + Harbor + gateway logs + Argo CD. WRITE: one OIDC-guarded lifecycle-action endpoint that stamps an action annotation on the App CR — it never writes status")
    Container(viewer, "Estate viewer", "Go html/template, SSR", "Estate table: owner, expiry, state, cost, AI-Act triage — browser never calls APIs directly")
    Container(gitea2, "Gitea", "git host", "commit / promotion history")
    Container(harbor2, "Harbor", "registry", "artifacts + attestations")
    Container(dgw2, "Data gateway", "access-log query API", "per-app allow/deny history (OIDC-guarded)")

    Rel(operator, crd, "reconciles from / writes status")
    Rel(operator, opencost, "polls allocation API")
    Rel(evapi, crd, "reads")
    Rel(evapi, gitea2, "reads")
    Rel(evapi, harbor2, "reads")
    Rel(evapi, dgw2, "queries logs (client-credentials OIDC)")
    Rel(viewer, evapi, "GET /estate", "in-cluster")

    UpdateLayoutConfig($c4ShapeInRow="4", $c4BoundaryInRow="1")

Container inventory

Container Tech Namespace Responsibility Reference
Tend operator Go, Kubebuilder / controller-runtime tend-system Reconciles App CRs: namespace, perimeter, quotas, artifact recording, cost state machine App CRD
Inference gateway Go, stdlib net/http tend-system Only model path; builder and per-app OIDC; forwards to the model gateway. The tier is recorded on every response; Tier 2 selects no alternative route Inference gateway
Data gateway Go + SQLite on PVC tend-system Only data path; per-request scope enforcement; lifetime access log Data gateway API
Evidence & estate API Go, stateless tend-system Evidence assembly, estate listing, and the OIDC-guarded lifecycle actions (suspend/resume/retire/restore), which patch an annotation on the App CR and never write status Evidence API
Estate viewer Go html/template, SSR tend-system Estate UI (pre-Backstage) Evidence API
Builder adapter Go CLI runs at the builder The three-clause builder contract ADR-0013
Gitea git host tend-system App repos + platform repo (GitOps source)
Keycloak OIDC tend-system Identity: builder client, approver groups, platform tokens
Harbor OCI registry tend-system Images + co-located signatures/attestations; only dependency source Evidence chain
Argo CD GitOps argocd App-of-apps (platform) + per-app ApplicationSets (sandbox/production) ADR-0004
Tekton + Chains CI + provenance tekton-pipelines / tekton-chains tend-build-attest pipeline; SLSA v1 signing Evidence chain
Kyverno admission kyverno 10-policy baseline incl. verify-attestation and field defaulting ADR-0006; source in deploy/policies/kyverno/
Cilium CNI cluster-wide Default-deny + generated gateway-only perimeter ADR-0007; source in deploy/policies/cilium/
OpenCost + Prometheus cost telemetry opencost Per-namespace allocation feeding the cost reconciler Lifecycle
App workloads generated code tend-app-{name} (sandbox and production share the namespace; the environments differ by branch and overlay) The governed apps themselves — untrusted by design Governance model

Reading on