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}"))
);
}
}