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

nordisk/Skidbladnir

src/infra.rs

//! TIER-0.5 common-infra — **production-shaped** manifest sets for the platform
//! substrate components.
//!
//! The generic k8s renderer (`crate::k8s`) projects a `ComponentSpec` onto a single
//! workload (Deployment/StatefulSet + Service + PVC) — right for a lake *app*, but an
//! infra component like the ingress controller needs its whole real object graph:
//! a ServiceAccount, cluster- and namespace-scoped RBAC, a ConfigMap, the workload,
//! a LoadBalancer Service, and an IngressClass. Those objects (RBAC rules, an
//! `IngressClass`, a `LoadBalancer` Service) are outside the generic workload
//! projection, so this module renders the **complete set** — reusing the
//! `ComponentSpec::ingress_nginx()` spec for the identity constants (name / namespace
//! / image / ports) so those live in ONE place (L5), never re-typed here.
//!
//! Pure: no root, no cluster, no IO — every object is rendered as manifest DATA
//! (`RenderedFile`), asserted offline; the live apply (server-side apply of the set)
//! is the deferred back half (the `kubernetes` feature's applier).

use crate::product::ComponentSpec;
use crate::service::RenderedFile;

/// The full production object set for **ingress-nginx** (the L7 edge): the nine
/// objects a real bring-up applies, in apply order —
/// ServiceAccount · ConfigMap · ClusterRole · ClusterRoleBinding · Role · RoleBinding
/// · Deployment · Service (LoadBalancer) · IngressClass — rendered from the
/// `ComponentSpec::ingress_nginx()` identity values. Each is a `RenderedFile` at
/// `<ns>/ingress-nginx-<kind>.yaml`.
///
/// The admission webhook (`ValidatingWebhookConfiguration` + its Service + the
/// cert-patch Job/RBAC) is a documented follow-up — this set is the controller's own
/// production graph, enough to run and be RBAC-correct.
pub fn ingress_nginx_objects() -> Vec<RenderedFile> {
    let spec = ComponentSpec::ingress_nginx();
    let ns = spec
        .namespace
        .clone()
        .unwrap_or_else(|| "ingress-nginx".to_string());
    let sa = "ingress-nginx"; // the ServiceAccount every object binds to
    let controller = "ingress-nginx-controller";
    let image = spec
        .image
        .clone()
        .unwrap_or_else(|| "registry.k8s.io/ingress-nginx/controller:latest".to_string());
    let cm = "ingress-nginx-controller"; // the controller ConfigMap

    // Shared recommended labels (so Service selectors + tests can key off them).
    let labels =
        "    app.kubernetes.io/name: ingress-nginx\n    app.kubernetes.io/part-of: ingress-nginx\n";
    let sel = "app.kubernetes.io/name: ingress-nginx";

    let mut out = Vec::new();
    let mut push = |kind_slug: &str, body: String| {
        out.push(RenderedFile {
            path: format!("{ns}/ingress-nginx-{kind_slug}.yaml"),
            contents: body,
            mode: 0o644,
        });
    };

    // 1. ServiceAccount — the identity the controller pod runs as; every binding
    //    below grants THIS account its permissions.
    push(
        "serviceaccount",
        format!(
            "apiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: {sa}\n  namespace: {ns}\n  labels:\n{labels}"
        ),
    );

    // 2. ConfigMap — the controller's tunables (the Deployment points `--configmap` here).
    push(
        "configmap",
        format!(
            "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: {cm}\n  namespace: {ns}\n  labels:\n{labels}\
             data:\n  allow-snippet-annotations: \"false\"\n"
        ),
    );

    // 3. ClusterRole — cluster-wide read the controller needs to watch Ingresses and
    //    their backends across all namespaces, plus IngressClass + endpointslices.
    push(
        "clusterrole",
        format!(
            "apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n  name: {sa}\n  labels:\n{labels}\
             rules:\n\
             - apiGroups: [\"\"]\n    resources: [\"configmaps\", \"endpoints\", \"nodes\", \"pods\", \"secrets\", \"namespaces\"]\n    verbs: [\"list\", \"watch\"]\n\
             - apiGroups: [\"\"]\n    resources: [\"services\"]\n    verbs: [\"get\", \"list\", \"watch\"]\n\
             - apiGroups: [\"discovery.k8s.io\"]\n    resources: [\"endpointslices\"]\n    verbs: [\"list\", \"watch\", \"get\"]\n\
             - apiGroups: [\"networking.k8s.io\"]\n    resources: [\"ingresses\"]\n    verbs: [\"get\", \"list\", \"watch\"]\n\
             - apiGroups: [\"networking.k8s.io\"]\n    resources: [\"ingresses/status\"]\n    verbs: [\"update\"]\n\
             - apiGroups: [\"networking.k8s.io\"]\n    resources: [\"ingressclasses\"]\n    verbs: [\"get\", \"list\", \"watch\"]\n\
             - apiGroups: [\"\"]\n    resources: [\"events\"]\n    verbs: [\"create\", \"patch\"]\n"
        ),
    );

    // 4. ClusterRoleBinding — bind the ServiceAccount to the ClusterRole.
    push(
        "clusterrolebinding",
        format!(
            "apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n  name: {sa}\n  labels:\n{labels}\
             roleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: ClusterRole\n  name: {sa}\n\
             subjects:\n- kind: ServiceAccount\n  name: {sa}\n  namespace: {ns}\n"
        ),
    );

    // 5. Role — namespace-scoped leader election (leases) + the controller's own
    //    config/secret access in its namespace.
    push(
        "role",
        format!(
            "apiVersion: rbac.authorization.k8s.io/v1\nkind: Role\nmetadata:\n  name: {sa}\n  namespace: {ns}\n  labels:\n{labels}\
             rules:\n\
             - apiGroups: [\"\"]\n    resources: [\"configmaps\", \"pods\", \"secrets\", \"endpoints\"]\n    verbs: [\"get\", \"list\", \"watch\"]\n\
             - apiGroups: [\"coordination.k8s.io\"]\n    resources: [\"leases\"]\n    verbs: [\"get\", \"create\", \"update\"]\n\
             - apiGroups: [\"\"]\n    resources: [\"events\"]\n    verbs: [\"create\", \"patch\"]\n"
        ),
    );

    // 6. RoleBinding — bind the ServiceAccount to the namespace Role.
    push(
        "rolebinding",
        format!(
            "apiVersion: rbac.authorization.k8s.io/v1\nkind: RoleBinding\nmetadata:\n  name: {sa}\n  namespace: {ns}\n  labels:\n{labels}\
             roleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: Role\n  name: {sa}\n\
             subjects:\n- kind: ServiceAccount\n  name: {sa}\n  namespace: {ns}\n"
        ),
    );

    // 7. Deployment — the controller, running AS the ServiceAccount, wired to its
    //    ConfigMap + IngressClass + the leader-election id.
    let http = spec.ports.first().copied().unwrap_or(80);
    let https = spec.ports.get(1).copied().unwrap_or(443);
    push(
        "deployment",
        format!(
            "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {controller}\n  namespace: {ns}\n  labels:\n{labels}\
             spec:\n  replicas: 1\n  selector:\n    matchLabels:\n      {sel}\n\
             template:\n    metadata:\n      labels:\n        {sel}\n\
             spec:\n      serviceAccountName: {sa}\n      containers:\n\
             - name: controller\n        image: {image}\n        args:\n\
             - /nginx-ingress-controller\n\
             - --publish-service=$(POD_NAMESPACE)/{controller}\n\
             - --election-id=ingress-nginx-leader\n\
             - --controller-class=k8s.io/ingress-nginx\n\
             - --ingress-class=nginx\n\
             - --configmap=$(POD_NAMESPACE)/{cm}\n\
             - --validating-webhook=:8443\n\
             env:\n\
             - name: POD_NAMESPACE\n          valueFrom:\n            fieldRef:\n              fieldPath: metadata.namespace\n\
             ports:\n\
             - name: http\n          containerPort: {http}\n        - name: https\n          containerPort: {https}\n        - name: webhook\n          containerPort: 8443\n"
        ),
    );

    // 8. Service — a LoadBalancer publishing 80/443 to the controller pods.
    push(
        "service",
        format!(
            "apiVersion: v1\nkind: Service\nmetadata:\n  name: {controller}\n  namespace: {ns}\n  labels:\n{labels}\
             spec:\n  type: LoadBalancer\n  selector:\n    {sel}\n\
             ports:\n\
             - name: http\n    port: {http}\n    targetPort: http\n    protocol: TCP\n\
             - name: https\n    port: {https}\n    targetPort: https\n    protocol: TCP\n"
        ),
    );

    // 9. IngressClass — makes `nginx` the class this controller serves.
    push(
        "ingressclass",
        format!(
            "apiVersion: networking.k8s.io/v1\nkind: IngressClass\nmetadata:\n  name: nginx\n  labels:\n{labels}\
             spec:\n  controller: k8s.io/ingress-nginx\n"
        ),
    );

    // ── admission webhook (production-shaped admission control) ──
    // ingress-nginx validates incoming Ingress objects through an admission webhook.
    // The webhook needs a serving cert; rather than couple ingress-nginx to
    // cert-manager (which would make it depend on cert-manager's wave), it
    // self-provisions the cert via the two upstream `kube-webhook-certgen` Jobs — a
    // create Job (mint the self-signed cert Secret) + a patch Job (write its CA into
    // the ValidatingWebhookConfiguration's caBundle). This keeps ingress-nginx a
    // wave-0 root and matches the shipped ingress-nginx manifests.
    let admission = "ingress-nginx-admission";
    let admission_svc = "ingress-nginx-controller-admission";
    // The certgen sidecar image (pinned; a different image from the controller).
    let certgen = "registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.4.4";

    // 10. admission ServiceAccount — the identity the cert-patch Jobs run as.
    push(
        "admission-serviceaccount",
        format!(
            "apiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: {admission}\n  namespace: {ns}\n  labels:\n{labels}"
        ),
    );
    // 11. admission ClusterRole — the patch Job updates the ValidatingWebhookConfiguration's caBundle.
    push(
        "admission-clusterrole",
        format!(
            "apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n  name: {admission}\n  labels:\n{labels}\
             rules:\n- apiGroups: [\"admissionregistration.k8s.io\"]\n    resources: [\"validatingwebhookconfigurations\"]\n    verbs: [\"get\", \"update\"]\n"
        ),
    );
    // 12. admission ClusterRoleBinding — bind the admission SA to that ClusterRole.
    push(
        "admission-clusterrolebinding",
        format!(
            "apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n  name: {admission}\n  labels:\n{labels}\
             roleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: ClusterRole\n  name: {admission}\n\
             subjects:\n- kind: ServiceAccount\n  name: {admission}\n  namespace: {ns}\n"
        ),
    );
    // 13. admission Role — the create Job mints + reads the serving-cert Secret.
    push(
        "admission-role",
        format!(
            "apiVersion: rbac.authorization.k8s.io/v1\nkind: Role\nmetadata:\n  name: {admission}\n  namespace: {ns}\n  labels:\n{labels}\
             rules:\n- apiGroups: [\"\"]\n    resources: [\"secrets\"]\n    verbs: [\"get\", \"create\"]\n"
        ),
    );
    // 14. admission RoleBinding — bind the admission SA to that Role.
    push(
        "admission-rolebinding",
        format!(
            "apiVersion: rbac.authorization.k8s.io/v1\nkind: RoleBinding\nmetadata:\n  name: {admission}\n  namespace: {ns}\n  labels:\n{labels}\
             roleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: Role\n  name: {admission}\n\
             subjects:\n- kind: ServiceAccount\n  name: {admission}\n  namespace: {ns}\n"
        ),
    );
    // 15. admission Service — routes the apiserver's admission calls to the controller's
    //     webhook port (8443). Selects the SAME controller pods.
    push(
        "admission-service",
        format!(
            "apiVersion: v1\nkind: Service\nmetadata:\n  name: {admission_svc}\n  namespace: {ns}\n  labels:\n{labels}\
             spec:\n  type: ClusterIP\n  selector:\n    {sel}\n\
             ports:\n  - name: https-webhook\n    port: 443\n    targetPort: webhook\n    protocol: TCP\n"
        ),
    );
    // 16. ValidatingWebhookConfiguration — the apiserver calls this on Ingress
    //     CREATE/UPDATE; clientConfig points at the admission Service. The patch Job
    //     fills its caBundle at install time.
    push(
        "admission-validatingwebhookconfiguration",
        format!(
            "apiVersion: admissionregistration.k8s.io/v1\nkind: ValidatingWebhookConfiguration\nmetadata:\n  name: {admission}\n  labels:\n{labels}\
             webhooks:\n- name: validate.nginx.ingress.kubernetes.io\n  admissionReviewVersions: [\"v1\"]\n  sideEffects: None\n  failurePolicy: Fail\n\
             clientConfig:\n    service:\n      name: {admission_svc}\n      namespace: {ns}\n      path: /networking/v1/ingresses\n\
             rules:\n  - apiGroups: [\"networking.k8s.io\"]\n    apiVersions: [\"v1\"]\n    operations: [\"CREATE\", \"UPDATE\"]\n    resources: [\"ingresses\"]\n"
        ),
    );
    // 17-18. cert-patch Jobs — create the serving-cert Secret, then patch the
    //     ValidatingWebhookConfiguration's caBundle. Both run AS the admission SA.
    let cert_job = |phase: &str, args: &str| {
        format!(
            "apiVersion: batch/v1\nkind: Job\nmetadata:\n  name: {admission}-{phase}\n  namespace: {ns}\n  labels:\n{labels}\
             spec:\n  template:\n    metadata:\n      labels:\n        app.kubernetes.io/name: ingress-nginx\n\
             spec:\n      serviceAccountName: {admission}\n      restartPolicy: OnFailure\n      containers:\n\
             - name: {phase}\n        image: {certgen}\n        args:\n{args}"
        )
    };
    push(
        "admission-create-job",
        cert_job(
            "create",
            &format!(
                "        - create\n        - --host=ingress-nginx-controller-admission,ingress-nginx-controller-admission.{ns}.svc\n        - --namespace=$(POD_NAMESPACE)\n        - --secret-name=ingress-nginx-admission\n\
                 env:\n        - name: POD_NAMESPACE\n          valueFrom:\n            fieldRef:\n              fieldPath: metadata.namespace\n"
            ),
        ),
    );
    push(
        "admission-patch-job",
        cert_job(
            "patch",
            "        - patch\n        - --webhook-name=ingress-nginx-admission\n        - --namespace=$(POD_NAMESPACE)\n        - --patch-mutating=false\n        - --secret-name=ingress-nginx-admission\n        - --patch-failure-policy=Fail\n\
                 env:\n        - name: POD_NAMESPACE\n          valueFrom:\n            fieldRef:\n              fieldPath: metadata.namespace\n",
        ),
    );

    out
}

// ─────────────────────── shared manifest builders (cert-manager) ───────────────────────
// Compact YAML builders so the three-component cert-manager graph stays DRY. Each
// takes the essentials and emits one object; the `app` label lets Service selectors +
// tests key off the component.

fn cm_labels(app: &str) -> String {
    format!(
        "  labels:\n    app.kubernetes.io/name: {app}\n    app.kubernetes.io/instance: cert-manager\n"
    )
}

fn cm_sa(name: &str, ns: &str, app: &str) -> String {
    format!(
        "apiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: {name}\n  namespace: {ns}\n{}",
        cm_labels(app)
    )
}

fn cm_cluster_role(name: &str, app: &str, rules: &str) -> String {
    format!(
        "apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n  name: {name}\n{}rules:\n{rules}",
        cm_labels(app)
    )
}

fn cm_crb(name: &str, app: &str, cluster_role: &str, sa: &str, ns: &str) -> String {
    format!(
        "apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n  name: {name}\n{}\
         roleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: ClusterRole\n  name: {cluster_role}\n\
         subjects:\n- kind: ServiceAccount\n  name: {sa}\n  namespace: {ns}\n",
        cm_labels(app)
    )
}

fn cm_role(name: &str, ns: &str, app: &str, rules: &str) -> String {
    format!(
        "apiVersion: rbac.authorization.k8s.io/v1\nkind: Role\nmetadata:\n  name: {name}\n  namespace: {ns}\n{}rules:\n{rules}",
        cm_labels(app)
    )
}

fn cm_rb(name: &str, ns: &str, app: &str, role: &str, sa: &str) -> String {
    format!(
        "apiVersion: rbac.authorization.k8s.io/v1\nkind: RoleBinding\nmetadata:\n  name: {name}\n  namespace: {ns}\n{}\
         roleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: Role\n  name: {role}\n\
         subjects:\n- kind: ServiceAccount\n  name: {sa}\n  namespace: {ns}\n",
        cm_labels(app)
    )
}

fn cm_deployment(
    name: &str,
    ns: &str,
    app: &str,
    sa: &str,
    image: &str,
    args: &str,
    port: u16,
) -> String {
    format!(
        "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {name}\n  namespace: {ns}\n{}\
         spec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app.kubernetes.io/name: {app}\n\
         template:\n    metadata:\n      labels:\n        app.kubernetes.io/name: {app}\n\
         spec:\n      serviceAccountName: {sa}\n      containers:\n\
         - name: {app}\n        image: {image}\n        args:\n{args}\
         env:\n\
         - name: POD_NAMESPACE\n          valueFrom:\n            fieldRef:\n              fieldPath: metadata.namespace\n\
         ports:\n        - containerPort: {port}\n",
        cm_labels(app)
    )
}

fn cm_service(name: &str, ns: &str, app: &str, port: u16, target: &str) -> String {
    format!(
        "apiVersion: v1\nkind: Service\nmetadata:\n  name: {name}\n  namespace: {ns}\n{}\
         spec:\n  type: ClusterIP\n  selector:\n    app.kubernetes.io/name: {app}\n\
         ports:\n  - port: {port}\n    targetPort: {target}\n    protocol: TCP\n",
        cm_labels(app)
    )
}

/// The **cert-manager** core production object set (the TLS issuer) — the same
/// object-graph depth as [`ingress_nginx_objects`], for cert-manager's THREE
/// components (controller · webhook · cainjector). Reuses
/// `ComponentSpec::cert_manager()` for the namespace + the pinned image version (the
/// webhook/cainjector images are the controller image with the component swapped, so
/// the version pin lives in ONE place, L5).
///
/// Rendered: per component a ServiceAccount + a ClusterRole + ClusterRoleBinding + a
/// leader-election (or webhook-serving) Role + RoleBinding + a Deployment (running AS
/// its ServiceAccount); the controller + webhook Services; the webhook's Validating +
/// Mutating `WebhookConfiguration`s; and the six CRD **registrations**
/// (Certificate/CertificateRequest/Issuer/ClusterIssuer/Order/Challenge).
///
/// **CRD schemas are DEFERRED** (the flagged CRD-schema-heavy leg): each CRD is a real,
/// named `CustomResourceDefinition` with its group/scope/names and one served+stored
/// `v1` whose `openAPIV3Schema` is `x-kubernetes-preserve-unknown-fields: true` — a
/// deliberate "accept anything" schema, NOT the hand-authored OpenAPI validation
/// (that is a separate slice). Honest: the registration is applyable; the strict
/// validation is explicitly not modelled here.
pub fn cert_manager_objects() -> Vec<RenderedFile> {
    let spec = ComponentSpec::cert_manager();
    let ns = spec
        .namespace
        .clone()
        .unwrap_or_else(|| "cert-manager".to_string());
    let ctrl_image = spec
        .image
        .clone()
        .unwrap_or_else(|| "quay.io/jetstack/cert-manager-controller:latest".to_string());
    // Sibling images share the version pin — swap the component in the repo path.
    let webhook_image = ctrl_image.replace("controller", "webhook");
    let cainjector_image = ctrl_image.replace("controller", "cainjector");
    let metrics = spec.ports.first().copied().unwrap_or(9402);

    let mut out = Vec::new();
    let mut push = |slug: &str, body: String| {
        out.push(RenderedFile {
            path: format!("{ns}/cert-manager-{slug}.yaml"),
            contents: body,
            mode: 0o644,
        });
    };

    // ── controller ──
    push(
        "controller-serviceaccount",
        cm_sa("cert-manager", &ns, "cert-manager"),
    );
    push(
        "controller-clusterrole",
        cm_cluster_role(
            "cert-manager-controller",
            "cert-manager",
            "- apiGroups: [\"cert-manager.io\"]\n  resources: [\"certificates\", \"certificaterequests\", \"issuers\", \"clusterissuers\"]\n  verbs: [\"get\", \"list\", \"watch\", \"create\", \"update\", \"patch\", \"delete\"]\n\
             - apiGroups: [\"cert-manager.io\"]\n  resources: [\"certificates/status\", \"certificaterequests/status\", \"issuers/status\", \"clusterissuers/status\"]\n  verbs: [\"update\", \"patch\"]\n\
             - apiGroups: [\"acme.cert-manager.io\"]\n  resources: [\"orders\", \"challenges\"]\n  verbs: [\"get\", \"list\", \"watch\", \"create\", \"update\", \"patch\", \"delete\"]\n\
             - apiGroups: [\"\"]\n  resources: [\"secrets\"]\n  verbs: [\"get\", \"list\", \"watch\", \"create\", \"update\", \"delete\"]\n\
             - apiGroups: [\"\"]\n  resources: [\"events\"]\n  verbs: [\"create\", \"patch\"]\n\
             - apiGroups: [\"networking.k8s.io\"]\n  resources: [\"ingresses\"]\n  verbs: [\"get\", \"list\", \"watch\", \"create\", \"delete\"]\n",
        ),
    );
    push(
        "controller-clusterrolebinding",
        cm_crb(
            "cert-manager-controller",
            "cert-manager",
            "cert-manager-controller",
            "cert-manager",
            &ns,
        ),
    );
    push(
        "controller-role",
        cm_role(
            "cert-manager:leaderelection",
            "kube-system",
            "cert-manager",
            "- apiGroups: [\"coordination.k8s.io\"]\n  resources: [\"leases\"]\n  verbs: [\"get\", \"create\", \"update\", \"watch\", \"list\"]\n",
        ),
    );
    push(
        "controller-rolebinding",
        cm_rb(
            "cert-manager:leaderelection",
            "kube-system",
            "cert-manager",
            "cert-manager:leaderelection",
            "cert-manager",
        ),
    );
    push(
        "controller-deployment",
        cm_deployment(
            "cert-manager",
            &ns,
            "cert-manager",
            "cert-manager",
            &ctrl_image,
            "        - --v=2\n        - --cluster-resource-namespace=$(POD_NAMESPACE)\n        - --leader-election-namespace=kube-system\n",
            metrics,
        ),
    );
    push(
        "controller-service",
        cm_service("cert-manager", &ns, "cert-manager", metrics, "http-metrics"),
    );

    // ── webhook ──
    push(
        "webhook-serviceaccount",
        cm_sa("cert-manager-webhook", &ns, "cert-manager-webhook"),
    );
    push(
        "webhook-clusterrole",
        cm_cluster_role(
            "cert-manager-webhook",
            "cert-manager-webhook",
            "- apiGroups: [\"authorization.k8s.io\"]\n  resources: [\"subjectaccessreviews\"]\n  verbs: [\"create\"]\n\
             - apiGroups: [\"admission.cert-manager.io\"]\n  resources: [\"certificates\", \"issuers\", \"clusterissuers\"]\n  verbs: [\"create\"]\n",
        ),
    );
    push(
        "webhook-clusterrolebinding",
        cm_crb(
            "cert-manager-webhook",
            "cert-manager-webhook",
            "cert-manager-webhook",
            "cert-manager-webhook",
            &ns,
        ),
    );
    push(
        "webhook-role",
        cm_role(
            "cert-manager-webhook:dynamic-serving",
            &ns,
            "cert-manager-webhook",
            "- apiGroups: [\"\"]\n  resources: [\"secrets\"]\n  resourceNames: [\"cert-manager-webhook-ca\"]\n  verbs: [\"get\", \"list\", \"watch\", \"update\"]\n\
             - apiGroups: [\"\"]\n  resources: [\"secrets\"]\n  verbs: [\"create\"]\n",
        ),
    );
    push(
        "webhook-rolebinding",
        cm_rb(
            "cert-manager-webhook:dynamic-serving",
            &ns,
            "cert-manager-webhook",
            "cert-manager-webhook:dynamic-serving",
            "cert-manager-webhook",
        ),
    );
    push(
        "webhook-deployment",
        cm_deployment(
            "cert-manager-webhook",
            &ns,
            "cert-manager-webhook",
            "cert-manager-webhook",
            &webhook_image,
            "        - --v=2\n        - --secure-port=10250\n        - --dynamic-serving-ca-secret-namespace=$(POD_NAMESPACE)\n        - --dynamic-serving-ca-secret-name=cert-manager-webhook-ca\n        - --dynamic-serving-dns-names=cert-manager-webhook,cert-manager-webhook.$(POD_NAMESPACE),cert-manager-webhook.$(POD_NAMESPACE).svc\n",
            10250,
        ),
    );
    push(
        "webhook-service",
        cm_service(
            "cert-manager-webhook",
            &ns,
            "cert-manager-webhook",
            443,
            "https",
        ),
    );
    // The admission webhook wiring — Validating + Mutating configurations pointing at
    // the webhook Service (cert-manager injects the CA bundle via the cainjector).
    let webhook_cfg = |kind: &str| {
        format!(
            "apiVersion: admissionregistration.k8s.io/v1\nkind: {kind}\nmetadata:\n  name: cert-manager-webhook\n{}\
             webhooks:\n- name: webhook.cert-manager.io\n  admissionReviewVersions: [\"v1\"]\n  sideEffects: None\n  failurePolicy: Fail\n\
             clientConfig:\n    service:\n      name: cert-manager-webhook\n      namespace: {ns}\n      path: /{path}\n\
             rules:\n  - apiGroups: [\"cert-manager.io\", \"acme.cert-manager.io\"]\n    apiVersions: [\"v1\"]\n    operations: [\"CREATE\", \"UPDATE\"]\n    resources: [\"*/*\"]\n",
            cm_labels("cert-manager-webhook"),
            path = if kind == "ValidatingWebhookConfiguration" {
                "validate"
            } else {
                "mutate"
            }
        )
    };
    push(
        "webhook-validatingwebhookconfiguration",
        webhook_cfg("ValidatingWebhookConfiguration"),
    );
    push(
        "webhook-mutatingwebhookconfiguration",
        webhook_cfg("MutatingWebhookConfiguration"),
    );

    // ── cainjector ──
    push(
        "cainjector-serviceaccount",
        cm_sa("cert-manager-cainjector", &ns, "cert-manager-cainjector"),
    );
    push(
        "cainjector-clusterrole",
        cm_cluster_role(
            "cert-manager-cainjector",
            "cert-manager-cainjector",
            "- apiGroups: [\"cert-manager.io\"]\n  resources: [\"certificates\"]\n  verbs: [\"get\", \"list\", \"watch\"]\n\
             - apiGroups: [\"\"]\n  resources: [\"secrets\"]\n  verbs: [\"get\", \"list\", \"watch\"]\n\
             - apiGroups: [\"admissionregistration.k8s.io\"]\n  resources: [\"validatingwebhookconfigurations\", \"mutatingwebhookconfigurations\"]\n  verbs: [\"get\", \"list\", \"watch\", \"update\", \"patch\"]\n",
        ),
    );
    push(
        "cainjector-clusterrolebinding",
        cm_crb(
            "cert-manager-cainjector",
            "cert-manager-cainjector",
            "cert-manager-cainjector",
            "cert-manager-cainjector",
            &ns,
        ),
    );
    push(
        "cainjector-role",
        cm_role(
            "cert-manager-cainjector:leaderelection",
            "kube-system",
            "cert-manager-cainjector",
            "- apiGroups: [\"coordination.k8s.io\"]\n  resources: [\"leases\"]\n  verbs: [\"get\", \"create\", \"update\", \"watch\", \"list\"]\n",
        ),
    );
    push(
        "cainjector-rolebinding",
        cm_rb(
            "cert-manager-cainjector:leaderelection",
            "kube-system",
            "cert-manager-cainjector",
            "cert-manager-cainjector:leaderelection",
            "cert-manager-cainjector",
        ),
    );
    push(
        "cainjector-deployment",
        cm_deployment(
            "cert-manager-cainjector",
            &ns,
            "cert-manager-cainjector",
            "cert-manager-cainjector",
            &cainjector_image,
            "        - --v=2\n        - --leader-election-namespace=kube-system\n",
            9402,
        ),
    );

    // ── CRD registrations (schema DEFERRED — preserve-unknown-fields, not the
    //    hand-authored OpenAPI validation) ──
    for (plural, group, kind, scope) in [
        (
            "certificates",
            "cert-manager.io",
            "Certificate",
            "Namespaced",
        ),
        (
            "certificaterequests",
            "cert-manager.io",
            "CertificateRequest",
            "Namespaced",
        ),
        ("issuers", "cert-manager.io", "Issuer", "Namespaced"),
        (
            "clusterissuers",
            "cert-manager.io",
            "ClusterIssuer",
            "Cluster",
        ),
        ("orders", "acme.cert-manager.io", "Order", "Namespaced"),
        (
            "challenges",
            "acme.cert-manager.io",
            "Challenge",
            "Namespaced",
        ),
    ] {
        let singular = kind.to_lowercase();
        push(
            &format!("crd-{plural}"),
            format!(
                "apiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\nmetadata:\n  name: {plural}.{group}\n{}\
                 spec:\n  group: {group}\n  scope: {scope}\n  names:\n    kind: {kind}\n    plural: {plural}\n    singular: {singular}\n\
                 versions:\n  - name: v1\n    served: true\n    storage: true\n\
                 # SCHEMA DEFERRED: the hand-authored OpenAPI validation is a separate slice.\n\
                 schema:\n      openAPIV3Schema:\n        type: object\n        x-kubernetes-preserve-unknown-fields: true\n",
                cm_labels("cert-manager")
            ),
        );
    }

    out
}

#[cfg(test)]
mod tests {
    use super::*;

    fn set() -> Vec<RenderedFile> {
        ingress_nginx_objects()
    }

    fn body(objs: &[RenderedFile], kind_slug: &str) -> String {
        objs.iter()
            .find(|f| f.path.ends_with(&format!("ingress-nginx-{kind_slug}.yaml")))
            .unwrap_or_else(|| panic!("missing object: {kind_slug}"))
            .contents
            .clone()
    }

    /// The bring-up renders the COMPLETE production object set — all nine kinds. A
    /// missing RBAC/Service/SA/IngressClass object fails here (RED-when-broken): the
    /// `body(..)` lookup panics for any absent kind, and the kind-count is pinned.
    #[test]
    fn ingress_nginx_renders_the_full_production_object_set() {
        let objs = set();
        // Every required object is present and declares the right `kind:`.
        for (slug, kind) in [
            ("serviceaccount", "ServiceAccount"),
            ("configmap", "ConfigMap"),
            ("clusterrole", "ClusterRole"),
            ("clusterrolebinding", "ClusterRoleBinding"),
            ("role", "Role"),
            ("rolebinding", "RoleBinding"),
            ("deployment", "Deployment"),
            ("service", "Service"),
            ("ingressclass", "IngressClass"),
        ] {
            let b = body(&objs, slug);
            assert!(
                b.contains(&format!("kind: {kind}\n")),
                "{slug}: wrong/absent kind:\n{b}"
            );
        }
        // …plus the admission-webhook set (9 more): its SA/RBAC, the admission Service,
        // the ValidatingWebhookConfiguration, and the two cert-patch Jobs.
        for (slug, kind) in [
            ("admission-serviceaccount", "ServiceAccount"),
            ("admission-clusterrole", "ClusterRole"),
            ("admission-clusterrolebinding", "ClusterRoleBinding"),
            ("admission-role", "Role"),
            ("admission-rolebinding", "RoleBinding"),
            ("admission-service", "Service"),
            (
                "admission-validatingwebhookconfiguration",
                "ValidatingWebhookConfiguration",
            ),
            ("admission-create-job", "Job"),
            ("admission-patch-job", "Job"),
        ] {
            let b = body(&objs, slug);
            assert!(
                b.contains(&format!("kind: {kind}\n")),
                "{slug}: wrong/absent kind:\n{b}"
            );
        }
        assert_eq!(
            objs.len(),
            18,
            "core (9) + admission webhook (9) = eighteen objects"
        );
    }

    /// The admission webhook is fully wired: the `ValidatingWebhookConfiguration`'s
    /// clientConfig points at the admission Service, the admission Service targets the
    /// controller's webhook port, and the cert-patch Jobs (which self-provision + inject
    /// the serving cert) run AS the admission ServiceAccount whose RBAC lets them
    /// create the Secret + update the webhook config. RED-when-broken on any missing
    /// link (a webhook with no cert wiring, or a clientConfig pointing nowhere).
    #[test]
    fn ingress_nginx_admission_webhook_points_at_admission_service_and_wires_cert() {
        let objs = set();
        // The webhook config → the admission Service (name + namespace), guarding Ingresses.
        let vwc = body(&objs, "admission-validatingwebhookconfiguration");
        assert!(
            vwc.contains(
                "name: ingress-nginx-controller-admission\n      namespace: ingress-nginx"
            ),
            "clientConfig → admission Service:\n{vwc}"
        );
        assert!(
            vwc.contains("resources: [\"ingresses\"]")
                && vwc.contains("operations: [\"CREATE\", \"UPDATE\"]"),
            "validates Ingress CREATE/UPDATE:\n{vwc}"
        );

        // The admission Service exists and routes to the controller's webhook port.
        let svc = body(&objs, "admission-service");
        assert!(
            svc.contains("targetPort: webhook"),
            "admission Service → the controller webhook port:\n{svc}"
        );
        assert!(
            svc.contains("app.kubernetes.io/name: ingress-nginx"),
            "selects the controller pods"
        );

        // Both cert Jobs run as the admission SA and self-provision/patch the cert.
        let create = body(&objs, "admission-create-job");
        let patch = body(&objs, "admission-patch-job");
        assert!(
            create.contains("serviceAccountName: ingress-nginx-admission"),
            "create Job runs as admission SA"
        );
        assert!(
            create.contains("- create") && create.contains("--secret-name=ingress-nginx-admission"),
            "create mints the serving-cert Secret:\n{create}"
        );
        assert!(
            patch.contains("serviceAccountName: ingress-nginx-admission"),
            "patch Job runs as admission SA"
        );
        assert!(
            patch.contains("- patch") && patch.contains("--webhook-name=ingress-nginx-admission"),
            "patch injects the caBundle into the webhook config:\n{patch}"
        );

        // Admission RBAC is COHERENT: bindings target the admission SA, and the grants
        // are exactly what the Jobs need (update the webhook config; create the Secret).
        let crb = body(&objs, "admission-clusterrolebinding");
        assert!(crb.contains("kind: ClusterRole\n  name: ingress-nginx-admission"));
        assert!(crb.contains("kind: ServiceAccount\n  name: ingress-nginx-admission"));
        assert!(
            body(&objs, "admission-clusterrole").contains("validatingwebhookconfigurations"),
            "CR grants webhook-config update"
        );
        let rb = body(&objs, "admission-rolebinding");
        assert!(
            rb.contains("kind: Role\n  name: ingress-nginx-admission")
                && rb.contains("kind: ServiceAccount\n  name: ingress-nginx-admission")
        );
        assert!(
            body(&objs, "admission-role").contains("secrets"),
            "Role grants Secret create for the serving cert"
        );
    }

    /// RBAC is COHERENT: both bindings reference their role AND the one ServiceAccount
    /// the Deployment runs as — so the granted permissions actually reach the pod.
    /// RED-when-broken if a binding points at the wrong role/subject or the Deployment
    /// drops its serviceAccountName.
    #[test]
    fn ingress_nginx_rbac_binds_the_service_account_to_its_roles() {
        let objs = set();
        let crb = body(&objs, "clusterrolebinding");
        assert!(
            crb.contains("kind: ClusterRole\n  name: ingress-nginx"),
            "CRB → the ClusterRole:\n{crb}"
        );
        assert!(
            crb.contains("kind: ServiceAccount\n  name: ingress-nginx\n  namespace: ingress-nginx"),
            "CRB subject = the SA:\n{crb}"
        );

        let rb = body(&objs, "rolebinding");
        assert!(
            rb.contains("kind: Role\n  name: ingress-nginx"),
            "RB → the Role:\n{rb}"
        );
        assert!(
            rb.contains("kind: ServiceAccount\n  name: ingress-nginx"),
            "RB subject = the SA:\n{rb}"
        );

        // The Deployment actually RUNS as that ServiceAccount (else the RBAC is inert).
        let dep = body(&objs, "deployment");
        assert!(
            dep.contains("serviceAccountName: ingress-nginx"),
            "Deployment must run as the SA:\n{dep}"
        );

        // The ServiceAccount exists in the controller's namespace.
        let sa = body(&objs, "serviceaccount");
        assert!(sa.contains("name: ingress-nginx") && sa.contains("namespace: ingress-nginx"));
    }

    /// The RBAC rules grant the ingress-controller essentials — cluster-wide
    /// Ingress/IngressClass watch + status update, and namespace leader-election
    /// leases. RED-when-broken if a critical rule is dropped.
    #[test]
    fn ingress_nginx_rbac_rules_cover_the_controller_essentials() {
        let objs = set();
        let cr = body(&objs, "clusterrole");
        assert!(cr.contains("ingresses"), "must watch Ingresses:\n{cr}");
        assert!(
            cr.contains("ingressclasses"),
            "must read IngressClasses:\n{cr}"
        );
        assert!(
            cr.contains("ingresses/status"),
            "must update Ingress status (LB address):\n{cr}"
        );
        assert!(
            cr.contains("endpointslices"),
            "must read EndpointSlices for backends:\n{cr}"
        );

        let role = body(&objs, "role");
        assert!(
            role.contains("coordination.k8s.io") && role.contains("leases"),
            "leader election needs leases:\n{role}"
        );
    }

    /// The workload is production-wired: LoadBalancer Service on 80/443 selecting the
    /// controller, the IngressClass claims the `nginx` controller, the ConfigMap the
    /// Deployment references exists, and the pinned image flows from the ComponentSpec
    /// (single source of the identity constants — no twinning).
    #[test]
    fn ingress_nginx_service_ingressclass_and_image_are_production_wired() {
        let objs = set();
        let svc = body(&objs, "service");
        assert!(
            svc.contains("type: LoadBalancer"),
            "edge Service is a LoadBalancer:\n{svc}"
        );
        assert!(
            svc.contains("port: 80") && svc.contains("port: 443"),
            "publishes 80+443:\n{svc}"
        );
        assert!(
            svc.contains("app.kubernetes.io/name: ingress-nginx"),
            "selects the controller pods"
        );

        let ic = body(&objs, "ingressclass");
        assert!(ic.contains("name: nginx"), "the class is `nginx`:\n{ic}");
        assert!(
            ic.contains("controller: k8s.io/ingress-nginx"),
            "claims the ingress-nginx controller:\n{ic}"
        );

        // The Deployment's --configmap points at the ConfigMap object we render.
        let dep = body(&objs, "deployment");
        assert!(dep.contains("--configmap=$(POD_NAMESPACE)/ingress-nginx-controller"));
        // The image is the ComponentSpec's pinned value (single source of truth).
        let want = crate::product::ComponentSpec::ingress_nginx()
            .image
            .unwrap();
        assert!(
            dep.contains(&format!("image: {want}")),
            "image flows from the spec: {want}"
        );

        // Every namespaced object carries the controller namespace.
        for slug in [
            "serviceaccount",
            "configmap",
            "role",
            "rolebinding",
            "deployment",
            "service",
        ] {
            assert!(
                body(&objs, slug).contains("namespace: ingress-nginx"),
                "{slug} namespaced"
            );
        }
    }

    // ─────────────────────────── cert-manager ───────────────────────────

    fn cm_body(objs: &[RenderedFile], slug: &str) -> String {
        objs.iter()
            .find(|f| f.path.ends_with(&format!("cert-manager-{slug}.yaml")))
            .unwrap_or_else(|| panic!("missing cert-manager object: {slug}"))
            .contents
            .clone()
    }

    /// cert-manager renders the FULL three-component production graph (controller ·
    /// webhook · cainjector): each component has SA + ClusterRole +
    /// ClusterRoleBinding + Role + RoleBinding + Deployment, plus the two Services,
    /// both webhook configs,
    /// and the six CRD registrations — 28 objects. A missing object fails the lookup /
    /// the exact count (RED-when-broken).
    #[test]
    fn cert_manager_renders_all_three_components_full_graph() {
        let objs = cert_manager_objects();
        // Per-component core objects exist with the right kind.
        for comp in ["controller", "webhook", "cainjector"] {
            assert!(
                cm_body(&objs, &format!("{comp}-serviceaccount"))
                    .contains("kind: ServiceAccount\n")
            );
            assert!(cm_body(&objs, &format!("{comp}-clusterrole")).contains("kind: ClusterRole\n"));
            assert!(
                cm_body(&objs, &format!("{comp}-clusterrolebinding"))
                    .contains("kind: ClusterRoleBinding\n")
            );
            assert!(cm_body(&objs, &format!("{comp}-role")).contains("kind: Role\n"));
            assert!(cm_body(&objs, &format!("{comp}-rolebinding")).contains("kind: RoleBinding\n"));
            assert!(cm_body(&objs, &format!("{comp}-deployment")).contains("kind: Deployment\n"));
        }
        // Services (controller metrics + webhook), both webhook configs.
        assert!(cm_body(&objs, "controller-service").contains("kind: Service\n"));
        assert!(cm_body(&objs, "webhook-service").contains("kind: Service\n"));
        assert!(
            cm_body(&objs, "webhook-validatingwebhookconfiguration")
                .contains("kind: ValidatingWebhookConfiguration\n")
        );
        assert!(
            cm_body(&objs, "webhook-mutatingwebhookconfiguration")
                .contains("kind: MutatingWebhookConfiguration\n")
        );
        // The six CRDs.
        for plural in [
            "certificates",
            "certificaterequests",
            "issuers",
            "clusterissuers",
            "orders",
            "challenges",
        ] {
            assert!(
                cm_body(&objs, &format!("crd-{plural}"))
                    .contains("kind: CustomResourceDefinition\n"),
                "CRD {plural}"
            );
        }
        assert_eq!(
            objs.len(),
            28,
            "controller(7)+webhook(9)+cainjector(6)+CRDs(6)"
        );
    }

    /// RBAC is COHERENT per component: each ClusterRoleBinding binds its ClusterRole to
    /// the ServiceAccount, and each Deployment RUNS as that same ServiceAccount — so the
    /// grants reach the pod for all three components. RED-when-broken.
    #[test]
    fn cert_manager_rbac_binds_each_sa_to_its_deployment() {
        let objs = cert_manager_objects();
        for (comp, sa, cr) in [
            ("controller", "cert-manager", "cert-manager-controller"),
            ("webhook", "cert-manager-webhook", "cert-manager-webhook"),
            (
                "cainjector",
                "cert-manager-cainjector",
                "cert-manager-cainjector",
            ),
        ] {
            let crb = cm_body(&objs, &format!("{comp}-clusterrolebinding"));
            assert!(
                crb.contains(&format!("kind: ClusterRole\n  name: {cr}")),
                "{comp} CRB → its ClusterRole:\n{crb}"
            );
            assert!(
                crb.contains(&format!("kind: ServiceAccount\n  name: {sa}")),
                "{comp} CRB subject = its SA:\n{crb}"
            );
            let dep = cm_body(&objs, &format!("{comp}-deployment"));
            assert!(
                dep.contains(&format!("serviceAccountName: {sa}")),
                "{comp} Deployment runs as {sa}:\n{dep}"
            );
        }
        // Leader-election Role grants leases (controller + cainjector).
        assert!(
            cm_body(&objs, "controller-role").contains("leases"),
            "controller leader-election needs leases"
        );
        assert!(
            cm_body(&objs, "cainjector-role").contains("leases"),
            "cainjector leader-election needs leases"
        );
    }

    /// The six CRDs are REGISTERED (name/group/scope/names) but their schemas are
    /// DEFERRED: each carries `x-kubernetes-preserve-unknown-fields: true`, NOT a
    /// hand-authored OpenAPI validation. RED-when-broken if a CRD is dropped or if a
    /// schema is silently authored (the deferral marker vanishes).
    #[test]
    fn cert_manager_crds_are_registered_with_schema_deferred() {
        let objs = cert_manager_objects();
        let clusterissuer = cm_body(&objs, "crd-clusterissuers");
        assert!(clusterissuer.contains("name: clusterissuers.cert-manager.io"));
        assert!(
            clusterissuer.contains("scope: Cluster"),
            "ClusterIssuer is cluster-scoped:\n{clusterissuer}"
        );
        let order = cm_body(&objs, "crd-orders");
        assert!(
            order.contains("group: acme.cert-manager.io"),
            "orders are in the acme group:\n{order}"
        );
        // EVERY CRD defers its schema (preserve-unknown-fields) and none hand-authors one.
        for plural in [
            "certificates",
            "certificaterequests",
            "issuers",
            "clusterissuers",
            "orders",
            "challenges",
        ] {
            let crd = cm_body(&objs, &format!("crd-{plural}"));
            assert!(
                crd.contains("x-kubernetes-preserve-unknown-fields: true"),
                "{plural}: schema must be deferred:\n{crd}"
            );
            assert!(
                crd.contains("SCHEMA DEFERRED"),
                "{plural}: the deferral is explicit"
            );
        }
    }

    /// The webhook wiring is production-shaped: both admission configs point their
    /// clientConfig at the cert-manager-webhook Service in the namespace, and the
    /// webhook Deployment/Service use the webhook image (version pinned from the spec).
    #[test]
    fn cert_manager_webhook_wiring_and_image_pin() {
        let objs = cert_manager_objects();
        for kind in [
            "validatingwebhookconfiguration",
            "mutatingwebhookconfiguration",
        ] {
            let cfg = cm_body(&objs, &format!("webhook-{kind}"));
            assert!(
                cfg.contains("name: cert-manager-webhook\n      namespace: cert-manager"),
                "{kind} → the webhook Service:\n{cfg}"
            );
            assert!(
                cfg.contains("apiGroups: [\"cert-manager.io\", \"acme.cert-manager.io\"]"),
                "{kind} guards the cert-manager groups"
            );
        }
        // The webhook image is the controller image with the component swapped (one
        // version pin from the spec).
        let ctrl = crate::product::ComponentSpec::cert_manager().image.unwrap();
        let webhook_img = ctrl.replace("controller", "webhook");
        assert!(
            cm_body(&objs, "webhook-deployment").contains(&format!("image: {webhook_img}")),
            "webhook image pinned: {webhook_img}"
        );
        assert!(
            webhook_img.contains("cert-manager-webhook"),
            "the swap produced the webhook image"
        );
        // cainjector likewise.
        let cainjector_img = ctrl.replace("controller", "cainjector");
        assert!(
            cm_body(&objs, "cainjector-deployment").contains(&format!("image: {cainjector_img}"))
        );
    }
}