Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pkg/api/copy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ func TestStackConfigCompose_Copy(t *testing.T) {
"memory": "2Gi",
},
"controlledResources": []interface{}{"cpu", "memory"},
"controlledValues": "RequestsOnly",
},
}
cloudExtras := any(vpaConfig)
Expand Down
11 changes: 10 additions & 1 deletion pkg/clouds/k8s/kube_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 13 additions & 6 deletions pkg/clouds/pulumi/kubernetes/simple_container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
45 changes: 45 additions & 0 deletions pkg/clouds/pulumi/kubernetes/simple_container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)

Expand Down
Loading