diff --git a/.github/workflows/helm-test.yml b/.github/workflows/helm-test.yml index 1e4b80e5b7..04ae27a9e6 100644 --- a/.github/workflows/helm-test.yml +++ b/.github/workflows/helm-test.yml @@ -179,6 +179,29 @@ jobs: fail() { echo "::error::$1"; exit 1; } + # Without a Namespace in the render, Argo's CreateNamespace=true creates + # one it never adopts: everything inside is deleted correctly and the + # namespace itself is left behind, one per PR, forever. + grep -q '^kind: Namespace' /tmp/preview.yaml \ + || fail "preview render has no Namespace — it would outlive the PR" + python3 - <<'NS' + import sys, yaml + ns = [d for d in yaml.safe_load_all(open("/tmp/preview.yaml")) + if d and d["kind"] == "Namespace"] + if len(ns) != 1: + print(f"::error::expected exactly one Namespace, found {len(ns)}"); sys.exit(1) + m = ns[0]["metadata"] + w = int((m.get("annotations") or {}).get("argocd.argoproj.io/sync-wave", 0)) + if w >= -30: + print(f"::error::Namespace is at wave {w}, but everything lives inside it " + "— it must come before the ExternalSecret at -30"); sys.exit(1) + if not any(k.endswith("/preview") for k in (m.get("labels") or {})): + print("::error::Namespace carries no preview label — the Gateway admits " + "routes by namespace label, so the HTTPRoute would attach to nothing") + sys.exit(1) + print(f"namespace rendered at wave {w} with the gateway selector label") + NS + grep -q 'kind: HTTPRoute' /tmp/preview.yaml \ || fail "preview render has no HTTPRoute — the preview would be unreachable" grep -q 'pr-42.pr.studio.decocms.com' /tmp/preview.yaml \ diff --git a/deploy/helm/studio-previews/templates/applicationset.yaml b/deploy/helm/studio-previews/templates/applicationset.yaml index 0db0c63053..6dfaf11a7f 100644 --- a/deploy/helm/studio-previews/templates/applicationset.yaml +++ b/deploy/helm/studio-previews/templates/applicationset.yaml @@ -64,6 +64,13 @@ spec: tag: '{{ include "studio-previews.imageTag" . }}' preview: prNumber: '{{ `{{ .number }}` }}' + # The chart renders the namespace itself, so the label the + # Gateway selects on travels with the manifest instead of + # depending on managedNamespaceMetadata below. Both are set: + # the metadata still labels the namespace on the first sync, + # before the chart's own object is applied. + namespaceLabels: + {{ .Values.gateway.namespaceSelectorLabel }}: "true" host: '{{ include "studio-previews.host" . }}' gateway: name: {{ .Values.gateway.name }} diff --git a/deploy/helm/studio/Chart.yaml b/deploy/helm/studio/Chart.yaml index bb5b01487e..2819c88d7e 100644 --- a/deploy/helm/studio/Chart.yaml +++ b/deploy/helm/studio/Chart.yaml @@ -2,6 +2,12 @@ apiVersion: v2 name: chart-deco-studio description: Helm chart for deco Studio — supports inline secrets, external secretName, and AWS Secrets Manager via ExternalSecret type: application +# 0.14.3: the preview renders its own Namespace (preview.manageNamespace). +# CreateNamespace=true creates one without adopting it, so a cascading delete +# removed everything inside and left the namespace behind — one per pull request +# that ever had a preview. It also carries the Gateway's selector label now, so +# route attachment no longer depends on managedNamespaceMetadata. +# # 0.14.2: optional hosted sandboxes for previews (preview.sandbox.enabled, # default false). Renders one RoleBinding when on, nothing when off. Read the # note in templates/preview-sandbox-rbac.yaml before enabling: the namespace it @@ -32,7 +38,7 @@ type: application # when disabled, so existing releases are unaffected. # 0.12.4: chart-managed API/worker dispatch roles now use # STUDIO_DISPATCH_ROLE; legacy overrides are rejected. -version: 0.14.2 +version: 0.14.3 appVersion: "latest" dependencies: diff --git a/deploy/helm/studio/templates/preview-namespace.yaml b/deploy/helm/studio/templates/preview-namespace.yaml new file mode 100644 index 0000000000..530f63bbbf --- /dev/null +++ b/deploy/helm/studio/templates/preview-namespace.yaml @@ -0,0 +1,31 @@ +{{- if and .Values.preview.enabled .Values.preview.manageNamespace }} +{{- /* +The preview's own namespace, rendered by the chart rather than conjured by +Argo's CreateNamespace=true. + +CreateNamespace=true creates a namespace but does not adopt it: the object gets +no tracking annotation and never joins the Application's resource tree, so a +cascading delete leaves it behind. Everything inside is removed correctly — pods, +Services, HTTPRoute, ConfigMaps, the Postgres and MinIO that hold the data — and +an empty namespace stays for every pull request that ever had a preview. + +Rendering it here puts it under the same ownership as everything else, so it is +deleted with them. It also carries its own labels, which means the Gateway's +namespace selector no longer depends on managedNamespaceMetadata being set +correctly by whatever creates the Application — the failure that produces a +silent 404 with a green Argo. + +Wave -40: before the ExternalSecret at -30, since everything else is inside it. +*/ -}} +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .Release.Namespace }} + labels: + {{- include "chart-deco-studio.labels" . | nindent 4 }} + {{- with .Values.preview.namespaceLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + annotations: + argocd.argoproj.io/sync-wave: "-40" +{{- end }} diff --git a/deploy/helm/studio/values-preview.yaml b/deploy/helm/studio/values-preview.yaml index be70e0497e..0e221df1b6 100644 --- a/deploy/helm/studio/values-preview.yaml +++ b/deploy/helm/studio/values-preview.yaml @@ -24,6 +24,8 @@ preview: enabled: true + # namespaceLabels is injected by the studio-previews ApplicationSet — it is + # the one that knows which label the Gateway selects on. # prNumber / host injected per PR by the ApplicationSet. gateway: name: studio-preview diff --git a/deploy/helm/studio/values.yaml b/deploy/helm/studio/values.yaml index 5cc3bb7af2..8f171023c0 100644 --- a/deploy/helm/studio/values.yaml +++ b/deploy/helm/studio/values.yaml @@ -147,6 +147,21 @@ preview: limits: memory: "768Mi" + # Render the preview's namespace as part of the release. + # + # Argo's CreateNamespace=true creates a namespace without adopting it: no + # tracking annotation, never in the Application's resource tree, so a + # cascading delete leaves it behind. Everything inside is removed correctly, + # and an empty namespace accumulates for every PR that ever had a preview. + # + # Set false when something else owns the namespace. + manageNamespace: true + # Labels the namespace carries. The Gateway admits routes by namespace label, + # so this is what makes the HTTPRoute attach — putting it here means it no + # longer depends on the Application being created with the matching + # managedNamespaceMetadata, which is the omission that yields a silent 404. + namespaceLabels: {} + # Hosted agent sandboxes for a preview. OFF by default, and it should stay off # unless someone is actively testing agent behaviour. #