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 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