diff --git a/vertical-pod-autoscaler/pkg/recommender/main.go b/vertical-pod-autoscaler/pkg/recommender/main.go index 92ec1325b8c5..d4cfc8334038 100644 --- a/vertical-pod-autoscaler/pkg/recommender/main.go +++ b/vertical-pod-autoscaler/pkg/recommender/main.go @@ -57,6 +57,9 @@ var ( ctrPodNameLabel = flag.String("container-pod-name-label", "pod_name", `Label name to look for container pod names`) ctrNameLabel = flag.String("container-name-label", "name", `Label name to look for container names`) vpaObjectNamespace = flag.String("vpa-object-namespace", apiv1.NamespaceAll, "Namespace to search for VPA objects and pod stats. Empty means all namespaces will be used.") + + // recommendation post processor flags + postProcessorCPUasInteger = flag.Bool("recommendation-post-processor-cpu-as-integer-enabled", false, `Enable recommendation post process to have CPU as integer if requested by user in VPA object`) ) // Aggregation configuration flags @@ -84,6 +87,10 @@ func main() { useCheckpoints := *storage != "prometheus" var postProcessorsNames []routines.KnownPostProcessors + if *postProcessorCPUasInteger { + postProcessorsNames = append(postProcessorsNames, routines.IntegerCPU) + } + // Capping should stay in the last position, and always enabled postProcessorsNames = append(postProcessorsNames, routines.Capping) postProcessorFactory := routines.RecommendationPostProcessorFactory{PostProcessorsNames: postProcessorsNames} diff --git a/vertical-pod-autoscaler/pkg/recommender/routines/cpu_integer_post_processor.go b/vertical-pod-autoscaler/pkg/recommender/routines/cpu_integer_post_processor.go new file mode 100644 index 000000000000..62a9155917ae --- /dev/null +++ b/vertical-pod-autoscaler/pkg/recommender/routines/cpu_integer_post_processor.go @@ -0,0 +1,89 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package routines + +import ( + apiv1 "k8s.io/api/core/v1" + vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/model" + "math" + "strings" +) + +// integerCPUPostProcessor ensure that the policy is applied to recommendation +// it applies policy for fields: cpuAsInteger +type integerCPUPostProcessor struct{} + +const ( + // The user interface for that post processor is an annotation with the following format: + // vpa-post-processor.kubernetes.io/{containerName}_integerCPU=true + + vpaPostProcessorPrefix = "vpa-post-processor.kubernetes.io/" + vpaPostProcessorIntegerCPUSuffix = "_integerCPU" + vpaPostProcessorIntegerCPUValue = "true" +) + +var _ RecommendationPostProcessor = &integerCPUPostProcessor{} + +// Process apply the capping post-processing to the recommendation. +func (c integerCPUPostProcessor) Process(vpa *model.Vpa, recommendation *vpa_types.RecommendedPodResources, policy *vpa_types.PodResourcePolicy) *vpa_types.RecommendedPodResources { + + amendedRecommendation := recommendation.DeepCopy() + + process := func(recommendation apiv1.ResourceList) { + for resourceName, recommended := range recommendation { + if resourceName != apiv1.ResourceCPU { + continue + } + v := float64(recommended.MilliValue()) / 1000 + r := int64(math.Round(v)) + recommended.Set(r) + recommendation[resourceName] = recommended + } + } + + for key, value := range vpa.Annotations { + containerName := extractContainerName(key, vpaPostProcessorPrefix, vpaPostProcessorIntegerCPUSuffix) + if containerName == "" || value != vpaPostProcessorIntegerCPUValue { + continue + } + + for _, r := range amendedRecommendation.ContainerRecommendations { + if r.ContainerName != containerName { + continue + } + process(r.Target) + process(r.LowerBound) + process(r.UpperBound) + process(r.UncappedTarget) + } + } + return amendedRecommendation +} + +// extractContainerName return the container name for the feature based on annotation key +// if the return value is empty that means that the key does not match +func extractContainerName(key, prefix, suffix string) string { + if !strings.HasPrefix(key, prefix) { + return "" + } + if !strings.HasSuffix(key, suffix) { + return "" + } + + return key[len(prefix) : len(key)-len(suffix)] +} diff --git a/vertical-pod-autoscaler/pkg/recommender/routines/cpu_integer_post_processor_test.go b/vertical-pod-autoscaler/pkg/recommender/routines/cpu_integer_post_processor_test.go new file mode 100644 index 000000000000..951493146293 --- /dev/null +++ b/vertical-pod-autoscaler/pkg/recommender/routines/cpu_integer_post_processor_test.go @@ -0,0 +1,123 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package routines + +import ( + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/model" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/test" + "testing" +) + +func Test_extractContainerName(t *testing.T) { + tests := []struct { + name string + key string + prefix string + suffix string + want string + }{ + { + name: "empty", + key: "", + prefix: "", + suffix: "", + want: "", + }, + { + name: "no match", + key: "abc", + prefix: "z", + suffix: "x", + want: "", + }, + { + name: "match", + key: "abc", + prefix: "a", + suffix: "c", + want: "b", + }, + { + name: "real", + key: vpaPostProcessorPrefix + "kafka" + vpaPostProcessorIntegerCPUSuffix, + prefix: vpaPostProcessorPrefix, + suffix: vpaPostProcessorIntegerCPUSuffix, + want: "kafka", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, extractContainerName(tt.key, tt.prefix, tt.suffix), "extractContainerName(%v, %v, %v)", tt.key, tt.prefix, tt.suffix) + }) + } +} + +func Test_integerCPUPostProcessor_Process(t *testing.T) { + tests := []struct { + name string + vpa *model.Vpa + recommendation *vpa_types.RecommendedPodResources + want *vpa_types.RecommendedPodResources + }{ + { + name: "2 containers", + vpa: &model.Vpa{Annotations: map[string]string{ + vpaPostProcessorPrefix + "container1" + vpaPostProcessorIntegerCPUSuffix: "true", + }}, + recommendation: &vpa_types.RecommendedPodResources{ + ContainerRecommendations: []vpa_types.RecommendedContainerResources{ + test.Recommendation().WithContainer("container1").WithTarget("8.6", "200").GetContainerResources(), + test.Recommendation().WithContainer("container2").WithTarget("8.2", "200").GetContainerResources(), + }, + }, + want: &vpa_types.RecommendedPodResources{ + ContainerRecommendations: []vpa_types.RecommendedContainerResources{ + test.Recommendation().WithContainer("container1").WithTarget("9", "200").GetContainerResources(), + test.Recommendation().WithContainer("container2").WithTarget("8.2", "200").GetContainerResources(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := integerCPUPostProcessor{} + got := c.Process(tt.vpa, tt.recommendation, nil) + assert.Equalf(t, reScale3(tt.want), reScale3(got), "Process(%v, %v, nil)", tt.vpa, tt.recommendation) + }) + } +} + +func reScale3(recommended *vpa_types.RecommendedPodResources) *vpa_types.RecommendedPodResources { + + scale3 := func(rl v1.ResourceList) { + for k, v := range rl { + v.SetMilli(v.MilliValue()) + rl[k] = v + } + } + + for _, r := range recommended.ContainerRecommendations { + scale3(r.LowerBound) + scale3(r.Target) + scale3(r.UncappedTarget) + scale3(r.UpperBound) + } + return recommended +} diff --git a/vertical-pod-autoscaler/pkg/recommender/routines/recommendation_post_processor.go b/vertical-pod-autoscaler/pkg/recommender/routines/recommendation_post_processor.go index 239ab5c55405..3cc02d681602 100644 --- a/vertical-pod-autoscaler/pkg/recommender/routines/recommendation_post_processor.go +++ b/vertical-pod-autoscaler/pkg/recommender/routines/recommendation_post_processor.go @@ -29,6 +29,8 @@ type KnownPostProcessors string const ( // Capping is post-processor name to ensure that recommendation stays within [MinAllowed-MaxAllowed] range Capping KnownPostProcessors = "capping" + // IntegerCPU is post-processor name to ensure that CPU is an integer. This allows application that are using static CPU allocation to use VPA + IntegerCPU KnownPostProcessors = "integerCPU" ) // RecommendationPostProcessor can amend the recommendation according to the defined policies @@ -57,6 +59,8 @@ func (f *RecommendationPostProcessorFactory) Build() ([]RecommendationPostProces switch name { case Capping: processors = append(processors, &cappingPostProcessor{}) + case IntegerCPU: + processors = append(processors, &integerCPUPostProcessor{}) default: return nil, fmt.Errorf("unknown Post Processor: %s", name) }