Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/Skidbladnir

.nornir/tier0-common-infra-design.md

tier0 common-infra — design

Class: design · Function axis: f6c (product / ComponentSpec) + f8 (orchestrate waves) + the openbao feature. States only the shape we ship — the app-agnostic platform bring-up (rebirth-ladder Tier 0.5). The why this substrate and the rejected all-Helm approach are in the reasoning notes; the beginner walk-through is tier0-common-infra-for-idiots.md.

What it is

The common-infra ComponentSpec set — the shared cluster substrate every lake app sits on, brought up before any app, in dependency order. Four app-agnostic components, each a [ComponentSpec] DESCRIBED (not linked) exactly like holger/tunnr, so they render to k8s manifests through the ONE k8s renderer and order through the ONE wave engine — no new machinery, just the two shipped limbs composed.

The set (product.rs)

ComponentSpec::ingress_nginx() · cert_manager() · holger_registry() · openbao(), collected by common_infra() -> Vec<ComponentSpec> (deliberately SEPARATE from registry(), the lake app products):

component namespace image shape after
ingress-nginx ingress-nginx registry.k8s.io/ingress-nginx/controller:v1.11.3 stateless, 80/443 — (root)
cert-manager cert-manager quay.io/jetstack/cert-manager-controller:v1.16.2 stateless, 9402 — (root)
holger-registry holger git.x14.se/nordisk/holger:latest StatefulSet + PVC 50Gi ingress-nginx, cert-manager
openbao openbao ghcr.io/openbao/openbao:2.0.3 StatefulSet + PVC 10Gi cert-manager

Wave ordering (common_infra_waves)

common_infra_waves() -> Result<Vec<Vec<String>>> maps each spec through as_product() (its after → depends_on) and hands the set to the SHARED crate::orchestrate::start_waves — the ONE Kahn topo sort (sources first; stable within a wave). There is no second ordering to drift (L5). For the shipped set the waves are exactly:

wave 0: [cert-manager, ingress-nginx]   # the two roots (no deps)
wave 1: [holger-registry, openbao]      # each after its wave-0 deps

A caller brings up wave 0 in parallel, waits for readiness, then wave 1. A mis-declared after that never converges is a hard cycle error, not a silent hang.

The renderer half (pure) vs the live back half (deferred)

Everything above is pure and laptop-testable: the waves are computed offline; each spec renders to manifest DATA via renderer_for(Backend::Kubernetes) (a StatefulSet/Deployment + Service + PVC, in the component's own namespace, with the pinned image). The deferred back half is the live cluster apply (k8s_apply.rs, kubernetes feature — server-side apply / scale / rollout-wait).

Production-shaped object sets (infra.rs) — beyond the generic workload

The generic k8s renderer projects a ComponentSpec onto ONE workload (Deployment/StatefulSet + Service + PVC) — right for a lake app, but an infra component needs its whole real object graph (RBAC, an IngressClass, a LoadBalancer Service — all outside the workload projection). So infra::ingress_nginx_objects() renders the complete production set for the ingress controller — 18 objects (9 core + 9 admission-webhook) — reusing ComponentSpec::ingress_nginx() for the identity constants (name/namespace/image/ports — ONE source, L5). The core nine, in apply order:

  1. ServiceAccount ingress-nginx — the identity the controller runs as.

  2. ConfigMap ingress-nginx-controller — controller tunables (the Deployment's --configmap).

  3. ClusterRole ingress-nginx — cluster-wide watch of Ingresses/IngressClasses/EndpointSlices + ingresses/status update.

  4. ClusterRoleBinding — SA → ClusterRole.

  5. Role ingress-nginx — namespace leader-election (coordination.k8s.io leases) + local config.

  6. RoleBinding — SA → Role.

  7. Deployment ingress-nginx-controller — runs as the SA, wired to its ConfigMap + IngressClass

    • election id, ports 80/443/8443(webhook).
  8. Service ingress-nginx-controller — a LoadBalancer publishing 80/443.

  9. IngressClass nginx — controller k8s.io/ingress-nginx.

RBAC is rendered coherent: both bindings reference their role AND the one ServiceAccount the Deployment runs as, so the grants actually reach the pod (asserted).

The admission webhook (9 more objects) makes ingress-nginx fully production-shaped — the apiserver validates every Ingress on CREATE/UPDATE before it is admitted: a ValidatingWebhookConfiguration (validate.nginx.ingress.kubernetes.io, clientConfig → the admission Service), an admission Service (ingress-nginx-controller-admission, routing to the controller's webhook port), a dedicated admission ServiceAccount + ClusterRole/Binding (update the webhook config's caBundle) + Role/Binding (create the serving-cert Secret), and the two upstream kube-webhook-certgen Jobs — a create Job (mint the self-signed serving cert) + a patch Job (inject its CA into the webhook config's caBundle), both running AS the admission SA. This self-provisions the serving cert via the Jobs rather than coupling to cert-manager, so ingress-nginx stays a wave-0 root (no new dependency) and matches the shipped ingress-nginx manifests. The webhook clientConfig→Service link and the cert-Job→SA→RBAC coherence are asserted.

cert-manager — infra::cert_manager_objects() (the symmetric TLS-issuer graph)

cert-manager is THREE components, so its production set is deeper — 28 objects rendered from ComponentSpec::cert_manager() (the webhook/cainjector images are the controller image with the component swapped, so the version pin lives in ONE place, L5):

CRD schemas are DEFERRED (the flagged CRD-schema-heavy leg): each CRD's v1 uses openAPIV3Schema: {type: object, x-kubernetes-preserve-unknown-fields: true} + an explicit # SCHEMA DEFERRED marker — an "accept anything" schema, NOT the hand-authored OpenAPI validation (a separate slice). Honest: the registration is applyable; strict validation is not modelled.

Per-component RBAC is asserted coherent (each ClusterRoleBinding binds its ClusterRole to the SA its Deployment runs as). Live apply of either set is the deferred back half.

OpenBao structural-populate hook (openbao.rs, openbao feature)

Once the openbao component's wave is up, its structure is populated (never secret values — those are tillsynia-admin's job). This is expressed as a pure, ordered plan:

The plan is DATA, so a bring-up asserts the exact structure it will create — ordering, per-env scoping (one env's policy never grants another's path), and step count — entirely offline.

Tests (RED-when-broken)