From 1324e8fe43c132425aa0dfe4e268a7d6036c2be2 Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Thu, 7 May 2026 23:49:20 +0400 Subject: [PATCH 1/2] feat(caddy): add CaddyDeploymentNameForChild parent-aware helper Caddy is provisioned by the parent infra stack, so its deployment name is keyed on parentEnv (e.g. caddy-production). When a sub-env client stack (parentEnv != stackEnv) wants to patch Caddy to trigger a rolling restart, it must target caddy-, not the existing GenerateCaddyDeploymentName(stackEnv) which produces the non-existent caddy-. This commit introduces CaddyDeploymentNameForChild(stackEnv, parentEnv) that resolves to the parent-keyed name for custom stacks and falls back to stackEnv for single-env / self-reference cases. Asymmetric to the existing GenerateCaddyDeploymentName which is still used from the parent stack's own provisioning where Environment is the right input. Test cases cover: single-env, self-reference, sub-env (gl-pay/caddy-test case), preview env, and empty stackEnv with parentEnv set. Signed-off-by: Dmitrii Creed --- pkg/clouds/pulumi/kubernetes/naming.go | 18 ++++++++ pkg/clouds/pulumi/kubernetes/naming_test.go | 50 +++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/pkg/clouds/pulumi/kubernetes/naming.go b/pkg/clouds/pulumi/kubernetes/naming.go index 3947d66c..c57bead1 100644 --- a/pkg/clouds/pulumi/kubernetes/naming.go +++ b/pkg/clouds/pulumi/kubernetes/naming.go @@ -81,3 +81,21 @@ func GenerateCaddyDeploymentName(stackEnv string) string { } return "caddy" } + +// CaddyDeploymentNameForChild returns the Caddy deployment name a *child* (client) stack +// must target when patching annotations to trigger a Caddy rolling restart. +// +// Caddy is provisioned by the parent infra stack, so its deployment name is keyed on +// parentEnv (e.g. caddy-production). For sub-env client stacks where parentEnv differs +// from stackEnv (e.g. parentEnv=production, stackEnv=gl-pay), passing stackEnv would +// produce caddy-gl-pay — which doesn't exist — and the patch would fail silently. +// For single-env stacks (parentEnv empty or equal to stackEnv) this falls back to stackEnv. +// +// Note: this is the call site asymmetric to GenerateCaddyDeploymentName, which is used +// from the parent stack's own provisioning where Environment is the correct input. +func CaddyDeploymentNameForChild(stackEnv, parentEnv string) string { + if isCustomStack(stackEnv, parentEnv) { + return GenerateCaddyDeploymentName(parentEnv) + } + return GenerateCaddyDeploymentName(stackEnv) +} diff --git a/pkg/clouds/pulumi/kubernetes/naming_test.go b/pkg/clouds/pulumi/kubernetes/naming_test.go index c11a4277..108a984b 100644 --- a/pkg/clouds/pulumi/kubernetes/naming_test.go +++ b/pkg/clouds/pulumi/kubernetes/naming_test.go @@ -434,6 +434,56 @@ func TestNamespaceIsStackName(t *testing.T) { } } +func TestCaddyDeploymentNameForChild(t *testing.T) { + tests := []struct { + name string + stackEnv string + parentEnv string + expected string + }{ + { + name: "single-env stack falls back to stackEnv", + stackEnv: "production", + parentEnv: "", + expected: "caddy-production", + }, + { + name: "self-reference falls back to stackEnv", + stackEnv: "staging", + parentEnv: "staging", + expected: "caddy-staging", + }, + { + name: "sub-env stack targets parent's caddy", + stackEnv: "gl-pay", + parentEnv: "production", + expected: "caddy-production", + }, + { + name: "preview env targets parent staging caddy", + stackEnv: "staging-preview", + parentEnv: "staging", + expected: "caddy-staging", + }, + { + name: "empty stackEnv with parentEnv still resolves to parent", + stackEnv: "", + parentEnv: "production", + expected: "caddy-production", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := CaddyDeploymentNameForChild(tt.stackEnv, tt.parentEnv) + if result != tt.expected { + t.Errorf("CaddyDeploymentNameForChild(%q, %q) = %v, expected %v", + tt.stackEnv, tt.parentEnv, result, tt.expected) + } + }) + } +} + func TestIsCustomStack(t *testing.T) { tests := []struct { name string From f8885ad05c2482f8ec887bdff4e71b48dd45c745 Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Thu, 7 May 2026 23:49:37 +0400 Subject: [PATCH 2/2] fix(caddy): use parent-aware helper at both client patch sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For sub-env client stacks (parentEnv: production, env: gl-pay/payhey/...), the Caddy patch that triggers the rolling restart was computing the deployment name from the child stack's environment, producing caddy- — a name that doesn't exist (Caddy is owned by the parent infra stack and named caddy-). The patch failed silently with deployments.apps not found, the deploy reported success, but Caddy never rolled, the generate-caddyfile init container never re-ran, and the new domain served Caddy's default page. Symptom from a real PAY-SPACE deploy: ❌ PATCH ERROR: failed to patch deployment pod-template annotations caddy/caddy-caddy-test: deployments.apps "caddy-caddy-test" not found curl https://caddy-test.pay.space/ → 'Default page' There are two patch call sites, depending on which template handles the deploy (cloud-compose with K8s template vs. cloud-compose with GKE Autopilot parent). Both now route through the parent-aware helper: pkg/clouds/pulumi/kubernetes/kube_run.go:201 pkg/clouds/pulumi/gcp/gke_autopilot_stack.go:224 PAY-SPACE's wallet stacks deploy through gke_autopilot_stack.go, which was the bug actually hitting prod. End-to-end validated: a fresh caddy-test sub-env stack deployed with this fix produces '✅ Caddy deployment patched: caddy/caddy-production' and serves the backend at caddy-test.pay.space without manual kubectl rollout. For the kube_run.go site we read parentEnv from params.ParentStack (populated by deployStackProgram); for the gke_autopilot_stack site input.StackParams.ParentEnv is reliably populated by the custom-stack fix-up at the top of that function. Pattern matches existing usages in aws/compute_proc.go:51 and gcp/compute_proc.go:243-247. Signed-off-by: Dmitrii Creed --- pkg/clouds/pulumi/gcp/gke_autopilot_stack.go | 11 +++++++---- pkg/clouds/pulumi/kubernetes/kube_run.go | 12 ++++++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/pkg/clouds/pulumi/gcp/gke_autopilot_stack.go b/pkg/clouds/pulumi/gcp/gke_autopilot_stack.go index 91578624..34cf7070 100644 --- a/pkg/clouds/pulumi/gcp/gke_autopilot_stack.go +++ b/pkg/clouds/pulumi/gcp/gke_autopilot_stack.go @@ -218,10 +218,13 @@ func GkeAutopilotStack(ctx *sdk.Context, stack api.Stack, input api.ResourceInpu return nil, errors.Wrapf(err, "failed to unmarshal caddy config from parent stack: JSON was %q", caddyConfigJson) } - // Attempt to patch caddy deployment annotations (non-critical - skip if it fails) - // Use deployment name override if specified, otherwise generate using single-dash convention - // to match the actual Caddy deployment naming (e.g., "caddy-staging" not "caddy--staging") - defaultDeploymentName := kubernetes.GenerateCaddyDeploymentName(input.StackParams.Environment) + // Attempt to patch caddy deployment annotations (non-critical - skip if it fails). + // Use deployment name override if specified, otherwise resolve via the parent-aware helper + // so sub-env client stacks (parentEnv != stackEnv) target the parent's caddy- + // deployment instead of a non-existent caddy-. + // input.StackParams.ParentEnv is reliably populated for this code path by the + // custom-stack fix-up at the top of this function. + defaultDeploymentName := kubernetes.CaddyDeploymentNameForChild(input.StackParams.Environment, input.StackParams.ParentEnv) deploymentName := lo.If(caddyCfg.DeploymentName != nil, lo.FromPtr(caddyCfg.DeploymentName)).Else(defaultDeploymentName) namespace := lo.If(caddyCfg.Namespace != nil, lo.FromPtr(caddyCfg.Namespace)).Else("caddy") diff --git a/pkg/clouds/pulumi/kubernetes/kube_run.go b/pkg/clouds/pulumi/kubernetes/kube_run.go index 200989f1..cc9d971e 100644 --- a/pkg/clouds/pulumi/kubernetes/kube_run.go +++ b/pkg/clouds/pulumi/kubernetes/kube_run.go @@ -195,10 +195,14 @@ func KubeRun(ctx *sdk.Context, stack api.Stack, input api.ResourceInput, params } if caddyConfig != nil { - // Attempt to patch caddy deployment annotations (non-critical - skip if it fails) - // Use deployment name override if specified, otherwise generate using single-dash convention - // to match the actual Caddy deployment naming (e.g., "caddy-staging" not "caddy--staging") - defaultCaddyName := GenerateCaddyDeploymentName(input.StackParams.Environment) + // Attempt to patch caddy deployment annotations (non-critical - skip if it fails). + // Use deployment name override if specified, otherwise resolve via the parent-aware helper + // so sub-env client stacks (parentEnv != stackEnv) target the parent's caddy- + // deployment instead of a non-existent caddy-. + // parentEnv lives on params.ParentStack — input.StackParams.ParentEnv is empty for client + // stack deploys (matches the pattern in aws/compute_proc.go and gcp/compute_proc.go). + parentEnv := lo.FromPtr(params.ParentStack).ParentEnv + defaultCaddyName := CaddyDeploymentNameForChild(input.StackParams.Environment, parentEnv) caddyServiceName := lo.If(caddyConfig.DeploymentName != nil, lo.FromPtr(caddyConfig.DeploymentName)).Else(defaultCaddyName) // Cast params.Provider to Kubernetes provider for patch operations