diff --git a/vertical-pod-autoscaler/pkg/admission-controller/main.go b/vertical-pod-autoscaler/pkg/admission-controller/main.go index d8904643601d..0b7be7b7b129 100644 --- a/vertical-pod-autoscaler/pkg/admission-controller/main.go +++ b/vertical-pod-autoscaler/pkg/admission-controller/main.go @@ -21,6 +21,7 @@ import ( "fmt" "net/http" "os" + "strings" "time" apiv1 "k8s.io/api/core/v1" @@ -69,6 +70,7 @@ var ( registerWebhook = flag.Bool("register-webhook", true, "If set to true, admission webhook object will be created on start up to register with the API server.") registerByURL = flag.Bool("register-by-url", false, "If set to true, admission webhook will be registered by URL (webhookAddress:webhookPort) instead of by service name") vpaObjectNamespace = flag.String("vpa-object-namespace", apiv1.NamespaceAll, "Namespace to search for VPA objects. Empty means all namespaces will be used.") + allowedResources = flag.String("allowed-resources", strings.Join([]string{string(apiv1.ResourceCPU), string(apiv1.ResourceMemory)}, ","), "Comma-separated list of resources that can be applied from a VPA.") ) func main() { @@ -96,7 +98,14 @@ func main() { klog.Errorf("Failed to create limitRangeCalculator, falling back to not checking limits. Error message: %s", err) limitRangeCalculator = limitrange.NewNoopLimitsCalculator() } - recommendationProvider := recommendation.NewProvider(limitRangeCalculator, vpa_api_util.NewCappingRecommendationProcessor(limitRangeCalculator)) + + var allowedResourcesNames []apiv1.ResourceName + for _, resourceName := range strings.Split(*allowedResources, ",") { + if resourceName != "" { + allowedResourcesNames = append(allowedResourcesNames, apiv1.ResourceName(resourceName)) + } + } + recommendationProvider := recommendation.NewProvider(limitRangeCalculator, vpa_api_util.NewCappingRecommendationProcessor(limitRangeCalculator), allowedResourcesNames) vpaMatcher := vpa.NewMatcher(vpaLister, targetSelectorFetcher) hostname, err := os.Hostname() diff --git a/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/recommendation/recommendation_provider.go b/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/recommendation/recommendation_provider.go index 3c93338f6716..1109968e1b5f 100644 --- a/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/recommendation/recommendation_provider.go +++ b/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/recommendation/recommendation_provider.go @@ -18,7 +18,6 @@ package recommendation import ( "fmt" - core "k8s.io/api/core/v1" "k8s.io/klog/v2" @@ -35,24 +34,27 @@ type Provider interface { type recommendationProvider struct { limitsRangeCalculator limitrange.LimitRangeCalculator recommendationProcessor vpa_api_util.RecommendationProcessor + allowedResources []core.ResourceName } // NewProvider constructs the recommendation provider that can be used to determine recommendations for pods. func NewProvider(calculator limitrange.LimitRangeCalculator, - recommendationProcessor vpa_api_util.RecommendationProcessor) Provider { + recommendationProcessor vpa_api_util.RecommendationProcessor, + allowedResources []core.ResourceName) Provider { return &recommendationProvider{ limitsRangeCalculator: calculator, recommendationProcessor: recommendationProcessor, + allowedResources: allowedResources, } } // GetContainersResources returns the recommended resources for each container in the given pod in the same order they are specified in the pod.Spec. // If addAll is set to true, containers w/o a recommendation are also added to the list, otherwise they're skipped (default behaviour). func GetContainersResources(pod *core.Pod, vpaResourcePolicy *vpa_types.PodResourcePolicy, podRecommendation vpa_types.RecommendedPodResources, limitRange *core.LimitRangeItem, - addAll bool, annotations vpa_api_util.ContainerToAnnotationsMap) []vpa_api_util.ContainerResources { + addAll bool, annotations vpa_api_util.ContainerToAnnotationsMap, allowedResources []core.ResourceName) []vpa_api_util.ContainerResources { resources := make([]vpa_api_util.ContainerResources, len(pod.Spec.Containers)) for i, container := range pod.Spec.Containers { - recommendation := vpa_api_util.GetRecommendationForContainer(container.Name, &podRecommendation) + recommendation := filterAllowedContainerResources(vpa_api_util.GetRecommendationForContainer(container.Name, &podRecommendation), allowedResources) if recommendation == nil { if !addAll { klog.V(2).Infof("no matching recommendation found for container %s, skipping", container.Name) @@ -109,7 +111,7 @@ func (p *recommendationProvider) GetContainersResourcesForPod(pod *core.Pod, vpa if vpa.Spec.UpdatePolicy == nil || vpa.Spec.UpdatePolicy.UpdateMode == nil || *vpa.Spec.UpdatePolicy.UpdateMode != vpa_types.UpdateModeOff { resourcePolicy = vpa.Spec.ResourcePolicy } - containerResources := GetContainersResources(pod, resourcePolicy, *recommendedPodResources, containerLimitRange, false, annotations) + containerResources := GetContainersResources(pod, resourcePolicy, *recommendedPodResources, containerLimitRange, false, annotations, p.allowedResources) // Ensure that we are not propagating empty resource key if any. for _, resource := range containerResources { @@ -120,3 +122,35 @@ func (p *recommendationProvider) GetContainersResourcesForPod(pod *core.Pod, vpa return containerResources, annotations, nil } + +func filterAllowedContainerResources(recommendation *vpa_types.RecommendedContainerResources, allowedResources []core.ResourceName) *vpa_types.RecommendedContainerResources { + if recommendation != nil { + recommendation.Target = filterResourceList(recommendation.Target, allowedResources) + recommendation.LowerBound = filterResourceList(recommendation.LowerBound, allowedResources) + recommendation.UpperBound = filterResourceList(recommendation.UpperBound, allowedResources) + recommendation.UncappedTarget = filterResourceList(recommendation.UncappedTarget, allowedResources) + } + return recommendation +} + +func filterResourceList(resourceList core.ResourceList, allowedResources []core.ResourceName) core.ResourceList { + if len(allowedResources) == 0 { + return resourceList + } + filteredResourceList := core.ResourceList{} + for resourceName, resourceValue := range resourceList { + if containsResource(allowedResources, resourceName) { + filteredResourceList[resourceName] = resourceValue + } + } + return filteredResourceList +} + +func containsResource(resourceNames []core.ResourceName, target core.ResourceName) bool { + for _, resourceName := range resourceNames { + if target == resourceName { + return true + } + } + return false +} diff --git a/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/recommendation/recommendation_provider_test.go b/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/recommendation/recommendation_provider_test.go index f58e98f1a88e..52b6f38ab2be 100644 --- a/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/recommendation/recommendation_provider_test.go +++ b/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/recommendation/recommendation_provider_test.go @@ -116,6 +116,7 @@ func TestUpdateResourceRequests(t *testing.T) { name string pod *apiv1.Pod vpa *vpa_types.VerticalPodAutoscaler + allowedResources []apiv1.ResourceName expectedAction bool expectedError error expectedMem resource.Quantity @@ -127,71 +128,79 @@ func TestUpdateResourceRequests(t *testing.T) { annotations vpa_api_util.ContainerToAnnotationsMap }{ { - name: "uninitialized pod", - pod: uninitialized, - vpa: vpa, - expectedAction: true, - expectedMem: resource.MustParse("200Mi"), - expectedCPU: resource.MustParse("2"), + name: "uninitialized pod", + pod: uninitialized, + vpa: vpa, + allowedResources: []apiv1.ResourceName{apiv1.ResourceCPU, apiv1.ResourceMemory}, + expectedAction: true, + expectedMem: resource.MustParse("200Mi"), + expectedCPU: resource.MustParse("2"), }, { - name: "target below min", - pod: uninitialized, - vpa: targetBelowMinVPA, - expectedAction: true, - expectedMem: resource.MustParse("300Mi"), // MinMemory is expected to be used - expectedCPU: resource.MustParse("4"), // MinCpu is expected to be used + name: "target below min", + pod: uninitialized, + vpa: targetBelowMinVPA, + allowedResources: []apiv1.ResourceName{apiv1.ResourceCPU, apiv1.ResourceMemory}, + expectedAction: true, + expectedMem: resource.MustParse("300Mi"), // MinMemory is expected to be used + expectedCPU: resource.MustParse("4"), // MinCpu is expected to be used annotations: vpa_api_util.ContainerToAnnotationsMap{ containerName: []string{"cpu capped to minAllowed", "memory capped to minAllowed"}, }, }, { - name: "target above max", - pod: uninitialized, - vpa: targetAboveMaxVPA, - expectedAction: true, - expectedMem: resource.MustParse("1Gi"), // MaxMemory is expected to be used - expectedCPU: resource.MustParse("5"), // MaxCpu is expected to be used + name: "target above max", + pod: uninitialized, + vpa: targetAboveMaxVPA, + allowedResources: []apiv1.ResourceName{apiv1.ResourceCPU, apiv1.ResourceMemory}, + expectedAction: true, + expectedMem: resource.MustParse("1Gi"), // MaxMemory is expected to be used + expectedCPU: resource.MustParse("5"), // MaxCpu is expected to be used annotations: vpa_api_util.ContainerToAnnotationsMap{ containerName: []string{"cpu capped to maxAllowed", "memory capped to maxAllowed"}, }, }, { - name: "initialized pod", - pod: initialized, - vpa: vpa, - expectedAction: true, - expectedMem: resource.MustParse("200Mi"), - expectedCPU: resource.MustParse("2"), + name: "initialized pod", + pod: initialized, + vpa: vpa, + allowedResources: []apiv1.ResourceName{apiv1.ResourceCPU, apiv1.ResourceMemory}, + expectedAction: true, + expectedMem: resource.MustParse("200Mi"), + expectedCPU: resource.MustParse("2"), }, { - name: "high memory", - pod: initialized, - vpa: vpaWithHighMemory, - expectedAction: true, - expectedMem: resource.MustParse("1000Mi"), - expectedCPU: resource.MustParse("2"), + name: "high memory", + pod: initialized, + vpa: vpaWithHighMemory, + allowedResources: []apiv1.ResourceName{apiv1.ResourceCPU, apiv1.ResourceMemory}, + expectedAction: true, + expectedMem: resource.MustParse("1000Mi"), + expectedCPU: resource.MustParse("2"), }, { - name: "empty recommendation", - pod: initialized, - vpa: vpaWithEmptyRecommendation, - expectedAction: true, - expectedMem: resource.MustParse("0"), - expectedCPU: resource.MustParse("0"), + name: "empty recommendation", + pod: initialized, + vpa: vpaWithEmptyRecommendation, + allowedResources: []apiv1.ResourceName{apiv1.ResourceCPU, apiv1.ResourceMemory}, + expectedAction: true, + expectedMem: resource.MustParse("0"), + expectedCPU: resource.MustParse("0"), }, { - name: "nil recommendation", - pod: initialized, - vpa: vpaWithNilRecommendation, - expectedAction: true, - expectedMem: resource.MustParse("0"), - expectedCPU: resource.MustParse("0"), + name: "nil recommendation", + pod: initialized, + vpa: vpaWithNilRecommendation, + allowedResources: []apiv1.ResourceName{apiv1.ResourceCPU, apiv1.ResourceMemory}, + expectedAction: true, + expectedMem: resource.MustParse("0"), + expectedCPU: resource.MustParse("0"), }, { name: "guaranteed resources", pod: limitsMatchRequestsPod, vpa: vpa, + allowedResources: []apiv1.ResourceName{apiv1.ResourceCPU, apiv1.ResourceMemory}, expectedAction: true, expectedMem: resource.MustParse("200Mi"), expectedCPU: resource.MustParse("2"), @@ -202,6 +211,7 @@ func TestUpdateResourceRequests(t *testing.T) { name: "guaranteed resources - no request", pod: limitsNoRequestsPod, vpa: vpa, + allowedResources: []apiv1.ResourceName{apiv1.ResourceCPU, apiv1.ResourceMemory}, expectedAction: true, expectedMem: resource.MustParse("200Mi"), expectedCPU: resource.MustParse("2"), @@ -212,6 +222,7 @@ func TestUpdateResourceRequests(t *testing.T) { name: "proportional limit - as default", pod: podWithDoubleLimit, vpa: vpa, + allowedResources: []apiv1.ResourceName{apiv1.ResourceCPU, apiv1.ResourceMemory}, expectedAction: true, expectedCPU: resource.MustParse("2"), expectedMem: resource.MustParse("200Mi"), @@ -222,6 +233,7 @@ func TestUpdateResourceRequests(t *testing.T) { name: "proportional limit - set explicit", pod: podWithDoubleLimit, vpa: resourceRequestsAndLimitsVPA, + allowedResources: []apiv1.ResourceName{apiv1.ResourceCPU, apiv1.ResourceMemory}, expectedAction: true, expectedCPU: resource.MustParse("2"), expectedMem: resource.MustParse("200Mi"), @@ -229,20 +241,22 @@ func TestUpdateResourceRequests(t *testing.T) { expectedMemLimit: mustParseResourcePointer("400Mi"), }, { - name: "disabled limit scaling", - pod: podWithDoubleLimit, - vpa: resourceRequestsOnlyVPA, - expectedAction: true, - expectedCPU: resource.MustParse("2"), - expectedMem: resource.MustParse("200Mi"), + name: "disabled limit scaling", + pod: podWithDoubleLimit, + vpa: resourceRequestsOnlyVPA, + allowedResources: []apiv1.ResourceName{apiv1.ResourceCPU, apiv1.ResourceMemory}, + expectedAction: true, + expectedCPU: resource.MustParse("2"), + expectedMem: resource.MustParse("200Mi"), }, { - name: "disabled limit scaling - requests capped at limit", - pod: podWithDoubleLimit, - vpa: resourceRequestsOnlyVPAHighTarget, - expectedAction: true, - expectedCPU: resource.MustParse("2"), - expectedMem: resource.MustParse("200Mi"), + name: "disabled limit scaling - requests capped at limit", + pod: podWithDoubleLimit, + vpa: resourceRequestsOnlyVPAHighTarget, + allowedResources: []apiv1.ResourceName{apiv1.ResourceCPU, apiv1.ResourceMemory}, + expectedAction: true, + expectedCPU: resource.MustParse("2"), + expectedMem: resource.MustParse("200Mi"), annotations: vpa_api_util.ContainerToAnnotationsMap{ containerName: []string{ "cpu capped to container limit", @@ -254,6 +268,7 @@ func TestUpdateResourceRequests(t *testing.T) { name: "limit over int64", pod: podWithTenfoldLimit, vpa: vpaWithExabyteRecommendation, + allowedResources: []apiv1.ResourceName{apiv1.ResourceCPU, apiv1.ResourceMemory}, expectedAction: true, expectedCPU: resource.MustParse("1Ei"), expectedMem: resource.MustParse("1Ei"), @@ -270,6 +285,7 @@ func TestUpdateResourceRequests(t *testing.T) { name: "limit range calculation error", pod: initialized, vpa: vpa, + allowedResources: []apiv1.ResourceName{apiv1.ResourceCPU, apiv1.ResourceMemory}, limitRangeCalcErr: fmt.Errorf("oh no"), expectedAction: false, expectedError: fmt.Errorf("error getting containerLimitRange: oh no"), @@ -278,6 +294,7 @@ func TestUpdateResourceRequests(t *testing.T) { name: "proportional limit from default", pod: initialized, vpa: vpa, + allowedResources: []apiv1.ResourceName{apiv1.ResourceCPU, apiv1.ResourceMemory}, expectedAction: true, expectedCPU: resource.MustParse("2"), expectedMem: resource.MustParse("200Mi"), @@ -291,6 +308,22 @@ func TestUpdateResourceRequests(t *testing.T) { }, }, }, + { + name: "memory recommendation filtered out", + pod: initialized, + vpa: vpa, + allowedResources: []apiv1.ResourceName{apiv1.ResourceCPU}, + expectedAction: true, + expectedCPU: resource.MustParse("2"), + }, + { + name: "cpu recommendation filtered out", + pod: initialized, + vpa: vpa, + allowedResources: []apiv1.ResourceName{apiv1.ResourceMemory}, + expectedAction: true, + expectedMem: resource.MustParse("200Mi"), + }, } for _, tc := range testCases { @@ -301,6 +334,7 @@ func TestUpdateResourceRequests(t *testing.T) { containerLimitRange: tc.limitRange, containerErr: tc.limitRangeCalcErr, }, + allowedResources: tc.allowedResources, } resources, annotations, err := recommendationProvider.GetContainersResourcesForPod(tc.pod, tc.vpa) @@ -314,11 +348,21 @@ func TestUpdateResourceRequests(t *testing.T) { _, foundEmpty := resources[0].Requests[""] assert.Equal(t, foundEmpty, false, "empty resourceKey have not been purged") - cpuRequest := resources[0].Requests[apiv1.ResourceCPU] - assert.Equal(t, tc.expectedCPU.Value(), cpuRequest.Value(), "cpu request doesn't match") + if containsResource(tc.allowedResources, apiv1.ResourceCPU) { + cpuRequest := resources[0].Requests[apiv1.ResourceCPU] + assert.Equal(t, tc.expectedCPU.Value(), cpuRequest.Value(), "cpu request doesn't match") + } else { + _, ok := resources[0].Requests[apiv1.ResourceCPU] + assert.False(t, ok) + } - memoryRequest := resources[0].Requests[apiv1.ResourceMemory] - assert.Equal(t, tc.expectedMem.Value(), memoryRequest.Value(), "memory request doesn't match") + if containsResource(tc.allowedResources, apiv1.ResourceMemory) { + memoryRequest := resources[0].Requests[apiv1.ResourceMemory] + assert.Equal(t, tc.expectedMem.Value(), memoryRequest.Value(), "memory request doesn't match") + } else { + _, ok := resources[0].Requests[apiv1.ResourceMemory] + assert.False(t, ok) + } cpuLimit, cpuLimitPresent := resources[0].Limits[apiv1.ResourceCPU] if tc.expectedCPULimit == nil {