From 7e3280aa266494c58e4f4e9782d5f1ad98ceb4a9 Mon Sep 17 00:00:00 2001 From: pro-777 Date: Mon, 4 May 2026 14:27:30 +0300 Subject: [PATCH] =?UTF-8?q?feat(slice=202B.3):=20chart=20Restore=20half=20?= =?UTF-8?q?=E2=80=94=20injectedWildcards=20conditional=20(0.5.7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the chart-side machinery that lets Tower bypass the cert-manager Certificate path on Reconnect by injecting a Vault-stashed wildcard cert directly as a kubernetes.io/tls Secret. values.yaml: certManager.injectedWildcards: [] Each entry: { root, primary, crt, key }. Empty list = legacy ACME-only. templates/tenants-wildcard-cert.yaml: Build $injectedRoots index from injectedWildcards[]; per-domain Certificate is skipped when its root has an injected entry. templates/tenants-wildcard-secret.yaml (NEW): Per injected entry, render kubernetes.io/tls Secret using the same name the cert path would have produced (tenants-wildcard-tls primary, tenants-wildcard--tls non-primary). Sync-wave 2 to match the cert path's timing. Label odoosky.io/wildcard-source= vault-injected so harvester can skip them. Verified via helm template + self-signed dummy cert: - Pure injection: 0 Certificate, 1 Secret (correct name + base64) - Pure ACME: 1 Certificate, 0 Secret (status quo) - Mixed (2 domains, 1 injected): 1 Certificate + 1 Secret Inert without Tower wiring — existing clusters render identically to 0.5.6 because injectedWildcards defaults to []. Pushed first as the foundation layer for the upcoming Tower restore + harvester slices. Co-Authored-By: Claude Opus 4.7 (1M context) --- Chart.yaml | 4 +- templates/tenants-wildcard-cert.yaml | 13 +++++- templates/tenants-wildcard-secret.yaml | 62 ++++++++++++++++++++++++++ values.yaml | 21 +++++++++ 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 templates/tenants-wildcard-secret.yaml diff --git a/Chart.yaml b/Chart.yaml index 43d4cdd..55f72ed 100644 --- a/Chart.yaml +++ b/Chart.yaml @@ -23,8 +23,8 @@ description: | Git). type: application -version: 0.5.6 -appVersion: "0.5.6" +version: 0.5.7 +appVersion: "0.5.7" dependencies: - name: cert-manager diff --git a/templates/tenants-wildcard-cert.yaml b/templates/tenants-wildcard-cert.yaml index 1d7e22d..09d19b2 100644 --- a/templates/tenants-wildcard-cert.yaml +++ b/templates/tenants-wildcard-cert.yaml @@ -46,8 +46,19 @@ "primary" true "verified" true) }} {{- end }} +{{/* Slice 2B.3 — index of roots that have a Vault-stashed cert +injected via certManager.injectedWildcards[]. We skip the +Certificate resource entirely for those; the sibling +tenants-wildcard-secret.yaml renders the kubernetes.io/tls +Secret directly so no ACME order is placed. */}} +{{- $injectedRoots := dict }} +{{- range .Values.certManager.injectedWildcards | default (list) }} +{{- if and .root .crt .key }} +{{- $_ := set $injectedRoots .root true }} +{{- end }} +{{- end }} {{- range $i, $d := $domains }} -{{- if and $d.verified $d.wildcardHost }} +{{- if and $d.verified $d.wildcardHost (not (hasKey $injectedRoots $d.root)) }} {{- $suffix := "" }} {{- if not $d.primary }} {{- $suffix = printf "-%s" (replace "." "-" $d.root) }} diff --git a/templates/tenants-wildcard-secret.yaml b/templates/tenants-wildcard-secret.yaml new file mode 100644 index 0000000..33685b3 --- /dev/null +++ b/templates/tenants-wildcard-secret.yaml @@ -0,0 +1,62 @@ +# tenants-wildcard injected Secret(s) — Slice 2B.3 (2026-05-04). +# +# This template is the "Restore" half of the Vault Stash/Restore +# flow. Tower harvests successfully-issued wildcard cert Secrets +# into per-tenant Vault paths (`v3/tenants//certificates/`). +# On Reconnect for the same tenant + root, Tower reads the stash back +# and passes it as helm values: +# +# certManager.injectedWildcards: +# - root: "acme.com" +# primary: true # mirrors tenant.domains[i].primary +# crt: "" +# key: "" +# +# When an entry is present, this file emits a kubernetes.io/tls +# Secret with the SAME name the cert-manager Certificate path would +# have produced — so existing IngressRoutes and per-instance +# references don't have to change to use the injected variant. +# +# tenants-wildcard-cert.yaml's matching skip-condition keeps the +# Certificate resource from rendering for the same root, so no +# ACME order is placed and the Let's Encrypt rate-limit budget is +# preserved across reconnect churn. +# +# Naming contract (must mirror tenants-wildcard-cert.yaml exactly): +# primary → `tenants-wildcard-tls` +# non-primary → `tenants-wildcard--tls` +# +# Empty / missing fields on an entry → silently skip that entry. +# Tower is responsible for populating crt + key + root before passing +# the entry through; a half-formed entry shouldn't render a broken +# Secret that would mask the real ACME path. +{{- range .Values.certManager.injectedWildcards | default (list) }} +{{- if and .root .crt .key }} +{{- $suffix := "" }} +{{- if not .primary }} +{{- $suffix = printf "-%s" (replace "." "-" .root) }} +{{- end }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ printf "tenants-wildcard%s-tls" $suffix | quote }} + namespace: tenants + labels: + app.kubernetes.io/managed-by: cluster-platform-v3 + odoosky.io/domain-root: {{ .root | quote }} + odoosky.io/wildcard-source: vault-injected + {{- if .primary }} + odoosky.io/domain-primary: "true" + {{- end }} + annotations: + # Wave 2 to land in the same step as the (skipped) Certificate + # resource would have. Keeps the substrate-Ready timing model + # identical between ACME-issued and injected paths. + argocd.argoproj.io/sync-wave: "2" +type: kubernetes.io/tls +data: + tls.crt: {{ .crt | b64enc | quote }} + tls.key: {{ .key | b64enc | quote }} +{{- end }} +{{- end }} diff --git a/values.yaml b/values.yaml index 7eaae0a..b750519 100644 --- a/values.yaml +++ b/values.yaml @@ -53,6 +53,27 @@ acme: # the actual subchart values live below under the dep name `cert-manager`. certManager: enabled: true + # injectedWildcards — Slice 2B.3 (2026-05-04). Tower's per-tenant + # Vault-stash flow harvests successfully-issued wildcard cert + # Secrets and re-injects them on Reconnect to bypass Let's Encrypts + # 5-cert/identifier/168h rate limit. When an entry is present + # for a tenant.domains[].root, the chart: + # - SKIPS the cert-manager Certificate resource for that root + # (so no ACME order is placed) + # - Renders a kubernetes.io/tls Secret with the injected crt/key + # under the SAME name the cert-manager path would have used + # (`tenants-wildcard-tls` for primary, `tenants-wildcard--tls` otherwise) so existing + # IngressRoutes don't need to change. + # Empty list = legacy ACME-only path. Per-domain — a tenant can + # mix injected + ACME-issued certs across multiple roots. + # + # Each entry shape: + # - root: "acme.com" + # - primary: true # mirrors tenant.domains[i].primary + # - crt: "" + # - key: "" + injectedWildcards: [] # cert-manager — values passed THROUGH to the upstream jetstack subchart # (Chart.yaml dependency name = "cert-manager"). Subchart values must