Repository: kubernetes/autoscaler (Go)

Issue:
VPA - Support of Static Policy for CPU Management
**Which component are you using?**:

Vertical-Pod-Autoscaler/Recommender

**Is your feature request designed to solve a problem? If so describe the problem this feature should solve.**:

Feature: as a user of the [Static Policy of CPU management](https://kubernetes.io/docs/tasks/administer-cluster/cpu-management-policies/#static-policy) I would like VPA to ensure that CPU value is set as an integer. As a user I should be able to specify if the recommendation should be round/ceil/floor to become an integer and respect static CPU allocation requirement, from the doc:
```
Only containers that are both part of a Guaranteed pod and have integer CPU requests are assigned exclusive CPUs.
```

**Describe the solution you'd like.**:

The user should be able to define which container should have its CPU recommendation amended to be an integer.
To start the API can simply be an annotation on the VPA object:

`vpa-post-processor.kubernetes.io/{containerName}_integerCPU={round|ceil|floor}`

`vpa-post-processor.kubernetes.io/kafka_integerCPU=“round”`
Later when we have more user feedback this annotation can become a structured field in the `vpa.Spec.ResourcePolicy`

Implementation:
Today the VPA recommendation is amended to stay within the `minAllowed/maxAllowed`
We should be able to introduce more post-processing capabilities in the recommender.

Example:
The recommender internally delivers a recommendation of 3.85 CPU for container `kafka`, the recommendation that is written in the VPA object should be 4 CPU if the user has specified `vpa-post-processor.kubernetes.io/kafka_integerCPU=“round”`

**Describe any alternative solutions you've considered.**:

Write an entire dedicated custom recommender. I started to do that and realised that I was duplicating 90% of the default recommender code. Plus it will really be an heavy task to operate 2 different recommenders for a small difference in behavior.

**Additional context.**:

The issue #5212 can be handled in the exact same way from an implementation point of view.



Discussion: If we implemented #5237 then you could set N=1 and you would get this behavior. Or is there a difference I missed?



Fix (patch):
diff --git a/vertical-pod-autoscaler/FAQ.md b/vertical-pod-autoscaler/FAQ.md
index a26654e0b1d9..9ab83ceb535a 100644
--- a/vertical-pod-autoscaler/FAQ.md
+++ b/vertical-pod-autoscaler/FAQ.md
@@ -183,6 +183,7 @@ Name | Type | Description | Default
 `memory-aggregation-interval-count` | Int64 | The number of consecutive memory-aggregation-intervals which make up the MemoryAggregationWindowLength which in turn is the period for memory usage aggregation by VPA. In other words, MemoryAggregationWindowLength = memory-aggregation-interval * memory-aggregation-interval-count. | model.DefaultMemoryAggregationIntervalCount
 `memory-histogram-decay-half-life` | Duration | The amount of time it takes a historical memory usage sample to lose half of its weight. In other words, a fresh usage sample is twice as 'important' as one with age equal to the half life period. | model.DefaultMemoryHistogramDecayHalfLife
 `cpu-histogram-decay-half-life` | Duration | The amount of time it takes a historical CPU usage sample to lose half of its weight. | model.DefaultCPUHistogramDecayHalfLife
+`cpu-integer-post-processor-enabled` | Bool | Enable the CPU integer recommendation post processor | false
 
 ### What are the parameters to VPA updater?
 
diff --git a/vertical-pod-autoscaler/README.md b/vertical-pod-autoscaler/README.md
index 1b94ad5ab069..fc6bc707befb 100644
--- a/vertical-pod-autoscaler/README.md
+++ b/vertical-pod-autoscaler/README.md
@@ -19,6 +19,7 @@
   - [Capping to Limit Range](#capping-to-limit-range)
   - [Resource Policy Overriding Limit Range](#resource-policy-overriding-limit-range)
   - [Starting multiple recommenders](#starting-multiple-recommenders)
+  - [Using CPU management with static policy](#using-cpu-management-with-static-policy)
 - [Known limitations](#known-limitations)
 - [Related links](#related-links)
 
@@ -294,6 +295,17 @@ Please note the usage of the following arguments to override default names and p
 
 You can then choose which recommender to use by setting `recommenders` inside the `VerticalPodAutoscaler` spec.
 
+### Using CPU management with static policy
+
+If you are using the [CPU management with static policy](https://kubernetes.io/docs/tasks/administer-cluster/cpu-management-policies/#static-policy) for some containers,
+you probably want the CPU recommendation to be an integer. A dedicated recommendation pre-processor can perform a round up on the CPU recommendation. Recommendation capping still applies after the round up.   
+To activate this feature, pass the flag `--cpu-integer-post-processor-enabled` when you start the recommender. 
+The pre-processor only acts on containers having a specific configuration. This configuration consists in an annotation on your VPA object for each impacted container.
+The annotation format is the following:
+```
+vpa-post-processor.kubernetes.io/{containerName}_integerCPU=true
+```
+ 
 # Known limitations
 
 * Whenever VPA updates the pod resources, the pod is recreated, which causes all
diff --git a/vertical-pod-autoscaler/pkg/recommender/main.go b/vertical-pod-autoscaler/pkg/recommender/main.go
index 80743fd7cf2d..8146e4074236 100644
--- a/vertical-pod-autoscaler/pkg/recommender/main.go
+++ b/vertical-pod-autoscaler/pkg/recommender/main.go
@@ -67,6 +67,12 @@ var (
 	cpuHistogramDecayHalfLife      = flag.Duration("cpu-histogram-decay-half-life", model.DefaultCPUHistogramDecayHalfLife, `The amount of time it takes a historical CPU usage sample to lose half of its weight.`)
 )
 
+// Post processors flags
+var (
+	// CPU as integer to benefit for CPU management Static Policy ( https://kubernetes.io/docs/tasks/administer-cluster/cpu-management-policies/#static-policy )
+	postProcessorCPUasInteger = flag.Bool("cpu-integer-post-processor-enabled", false, "Enable the cpu-integer recommendation post processor. The post processor will round up CPU recommendations to a whole CPU for pods which were opted in by setting an appropriate label on VPA object (experimental)")
+)
+
 func main() {
 	klog.InitFlags(nil)
 	kube_flag.InitFlags()
@@ -83,9 +89,13 @@ func main() {
 
 	useCheckpoints := *storage != "prometheus"
 
-	postProcessors := []routines.RecommendationPostProcessor{
-		&routines.CappingPostProcessor{},
+	var postProcessors []routines.RecommendationPostProcessor
+	if *postProcessorCPUasInteger {
+		postProcessors = append(postProcessors, &routines.IntegerCPUPostProcessor{})
 	}
+	// CappingPostProcessor, should always come in the last position for post-processing
+	postProcessors = append(postProcessors, &routines.CappingPostProcessor{})
+
 	recommender := routines.NewRecommender(config, *checkpointsGCInterval, useCheckpoints, *vpaObjectNamespace, *recommenderName, postProcessors)
 
 	promQueryTimeout, err := time.ParseDuration(*queryTimeout)
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..2c221269c79a
--- /dev/null
+++ b/vertical-pod-autoscaler/pkg/recommender/routines/cpu_integer_post_processor.go
@@ -0,0 +1,87 @@
+/*
+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"
+	"k8s.io/apimachinery/pkg/api/resource"
+	vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
+	"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/model"
+	"strings"
+)
+
+// IntegerCPUPostProcessor ensures that the recommendation delivers an integer value for CPU
+// This is need for users who want to use CPU Management with static policy: https://kubernetes.io/docs/tasks/administer-cluster/cpu-management-policies/#static-policy
+type IntegerCPUPostProcessor struct{}
+
+const (
+	// The user interface for that post processor is an annotation on the VPA object 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.
+// For this post processor the CPU value is rounded up to an integer
+func (p *IntegerCPUPostProcessor) Process(vpa *model.Vpa, recommendation *vpa_types.RecommendedPodResources, policy *vpa_types.PodResourcePolicy) *vpa_types.RecommendedPodResources {
+
+	amendedRecommendation := recommendation.DeepCopy()
+
+	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
+			}
+			setIntegerCPURecommendation(r.Target)
+			setIntegerCPURecommendation(r.LowerBound)
+			setIntegerCPURecommendation(r.UpperBound)
+			setIntegerCPURecommendation(r.UncappedTarget)
+		}
+	}
+	return amendedRecommendation
+}
+
+func setIntegerCPURecommendation(recommendation apiv1.ResourceList) {
+	for resourceName, recommended := range recommendation {
+		if resourceName != apiv1.ResourceCPU {
+			continue
+		}
+		recommended.RoundUp(resource.Scale(0))
+		recommendation[resourceName] = recommended
+	}
+}
+
+// extractContainerName return the container name for the feature based on annotation key
+// if the returned 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)]
+}