diff --git a/pkg/api/copy_test.go b/pkg/api/copy_test.go index 2c4f11ca..38836907 100644 --- a/pkg/api/copy_test.go +++ b/pkg/api/copy_test.go @@ -224,6 +224,7 @@ func TestStackConfigCompose_Copy(t *testing.T) { "memory": "2Gi", }, "controlledResources": []interface{}{"cpu", "memory"}, + "controlledValues": "RequestsOnly", }, } cloudExtras := any(vpaConfig) diff --git a/pkg/clouds/k8s/kube_run.go b/pkg/clouds/k8s/kube_run.go index a7de93b2..84eb3b4c 100644 --- a/pkg/clouds/k8s/kube_run.go +++ b/pkg/clouds/k8s/kube_run.go @@ -117,8 +117,17 @@ type VPAConfig struct { MinAllowed *VPAResourceRequirements `json:"minAllowed" yaml:"minAllowed"` // MaxAllowed specifies maximum allowed resources MaxAllowed *VPAResourceRequirements `json:"maxAllowed" yaml:"maxAllowed"` - // ControlledResources specifies which resources VPA should control + // ControlledResources specifies which resources VPA should control. + // Per the VPA CRD this is a per-container field; SC places it inside each + // containerPolicy entry, not at resourcePolicy level. ControlledResources []string `json:"controlledResources" yaml:"controlledResources"` + // ControlledValues specifies which resource values VPA should control. + // One of "RequestsAndLimits" (default) or "RequestsOnly". Use "RequestsOnly" + // when the underlying deployment template's limits are sized for cold-start + // bursts (e.g. Django/gunicorn) and you don't want VPA to scale the limit + // proportionally with a lowered request — the proportional shrink causes + // CPU-throttle-induced startup probe failures. + ControlledValues *string `json:"controlledValues" yaml:"controlledValues"` } // VPAResourceRequirements defines resource requirements for VPA diff --git a/pkg/clouds/pulumi/kubernetes/simple_container.go b/pkg/clouds/pulumi/kubernetes/simple_container.go index 06aacfcc..d16b41d9 100644 --- a/pkg/clouds/pulumi/kubernetes/simple_container.go +++ b/pkg/clouds/pulumi/kubernetes/simple_container.go @@ -994,17 +994,24 @@ func createVPA(ctx *sdk.Context, args *SimpleContainerArgs, deploymentName strin } // Add resource policy if specified - if args.VPA.MinAllowed != nil || args.VPA.MaxAllowed != nil || len(args.VPA.ControlledResources) > 0 { + if args.VPA.MinAllowed != nil || args.VPA.MaxAllowed != nil || len(args.VPA.ControlledResources) > 0 || args.VPA.ControlledValues != nil { resourcePolicy := map[string]interface{}{} - // Add controlled resources + // Build the container policy. Per the VPA CRD, controlledResources and + // controlledValues are per-container fields and live inside the + // containerPolicy entry — not at the resourcePolicy level. Placing them + // at resourcePolicy level (the previous behavior) caused k8s to silently + // drop them. + containerPolicy := map[string]interface{}{ + "containerName": "*", + } + if len(args.VPA.ControlledResources) > 0 { - resourcePolicy["controlledResources"] = args.VPA.ControlledResources + containerPolicy["controlledResources"] = args.VPA.ControlledResources } - // Add container policies - containerPolicy := map[string]interface{}{ - "containerName": "*", + if args.VPA.ControlledValues != nil { + containerPolicy["controlledValues"] = lo.FromPtr(args.VPA.ControlledValues) } if args.VPA.MinAllowed != nil { diff --git a/pkg/clouds/pulumi/kubernetes/simple_container_test.go b/pkg/clouds/pulumi/kubernetes/simple_container_test.go index a2441e17..095ada2a 100644 --- a/pkg/clouds/pulumi/kubernetes/simple_container_test.go +++ b/pkg/clouds/pulumi/kubernetes/simple_container_test.go @@ -94,6 +94,29 @@ func createVPATestArgs() *SimpleContainerArgs { return args } +// createVPATestArgsWithControlledValues exercises the full VPA surface area: +// minAllowed, maxAllowed, controlledResources (which lives inside the +// containerPolicy per the VPA CRD), and the controlledValues knob that lets +// callers opt out of VPA scaling limits proportionally with requests. +func createVPATestArgsWithControlledValues() *SimpleContainerArgs { + args := createBasicTestArgs() + args.VPA = &k8s.VPAConfig{ + Enabled: true, + UpdateMode: lo.ToPtr("Auto"), + MinAllowed: &k8s.VPAResourceRequirements{ + CPU: lo.ToPtr("50m"), + Memory: lo.ToPtr("64Mi"), + }, + MaxAllowed: &k8s.VPAResourceRequirements{ + CPU: lo.ToPtr("2"), + Memory: lo.ToPtr("4Gi"), + }, + ControlledResources: []string{"cpu", "memory"}, + ControlledValues: lo.ToPtr("RequestsOnly"), + } + return args +} + // createComplexTestArgs creates SimpleContainerArgs with many features enabled func createComplexTestArgs() *SimpleContainerArgs { args := createBasicTestArgs() @@ -332,6 +355,28 @@ func TestNewSimpleContainer_WithHPA(t *testing.T) { Expect(err).ToNot(HaveOccurred(), "Test should complete without errors") } +// TestNewSimpleContainer_WithVPA_ControlledValues exercises the new +// ControlledValues + ControlledResources fields on VPAConfig. Asserts the +// resource creation succeeds; the actual CRD shape (controlledValues + +// controlledResources living inside containerPolicy, not at resourcePolicy +// level) is enforced by simple_container.go's createVPA implementation. +func TestNewSimpleContainer_WithVPA_ControlledValues(t *testing.T) { + RegisterTestingT(t) + + mocks := NewSimpleContainerMocks() + args := createVPATestArgsWithControlledValues() + + err := pulumi.RunErr(func(ctx *pulumi.Context) error { + sc, err := NewSimpleContainer(ctx, args) + Expect(err).ToNot(HaveOccurred(), "SimpleContainer with VPA controlledValues should be created successfully") + Expect(sc).ToNot(BeNil(), "SimpleContainer should not be nil") + Expect(sc.Deployment).ToNot(BeNil(), "Deployment should not be nil") + return nil + }, pulumi.WithMocks("project", "stack", mocks)) + + Expect(err).ToNot(HaveOccurred(), "Test should complete without errors") +} + func TestNewSimpleContainer_WithVPA(t *testing.T) { RegisterTestingT(t)