Merge d0713f248b into 220c4a3a7c
This commit is contained in:
commit
37af3e8ef8
|
|
@ -24,6 +24,7 @@ This document is auto-generated from the flag definitions in the VPA admission-c
|
|||
| `log-file` | string | | If non-empty, use this log file (no effect when -logtostderr=true) |
|
||||
| `log-file-max-size` | int | 1800 | uDefines the maximum size a log file can grow to (no effect when -logtostderr=true). Unit is megabytes. If the value is 0, the maximum file size is unlimited. |
|
||||
| `logtostderr` | | true | log to standard error instead of files |
|
||||
| `max-allowed-cpu-boost` | string | | Maximum amount of CPU that will be applied for a container with boost. |
|
||||
| `min-tls-version` | string | | The minimum TLS version to accept. Must be set to either tls1_2 or tls1_3. (default "tls1_2") |
|
||||
| `one-output` | severity | | If true, only write logs to their native level (vs also writing to each lower severity level; no effect when -logtostderr=true) |
|
||||
| `port` | int | 8000 | The port to listen on. |
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"k8s.io/client-go/informers"
|
||||
kube_client "k8s.io/client-go/kubernetes"
|
||||
typedadmregv1 "k8s.io/client-go/kubernetes/typed/admissionregistration/v1"
|
||||
|
|
@ -78,6 +79,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.")
|
||||
webhookLabels = flag.String("webhook-labels", "", "Comma separated list of labels to add to the webhook object. Format: key1:value1,key2:value2")
|
||||
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")
|
||||
maxAllowedCPUBoost = flag.String("max-allowed-cpu-boost", "", "Maximum amount of CPU that will be applied for a container with boost.")
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -93,6 +95,13 @@ func main() {
|
|||
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
|
||||
}
|
||||
|
||||
if *maxAllowedCPUBoost != "" {
|
||||
if _, err := resource.ParseQuantity(*maxAllowedCPUBoost); err != nil {
|
||||
klog.ErrorS(err, "Failed to parse maxAllowedCPUBoost")
|
||||
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
|
||||
}
|
||||
}
|
||||
|
||||
healthCheck := metrics.NewHealthCheck(time.Minute)
|
||||
metrics_admission.Register()
|
||||
server.Initialize(&commonFlags.EnableProfiling, healthCheck, address)
|
||||
|
|
@ -145,7 +154,7 @@ func main() {
|
|||
hostname,
|
||||
)
|
||||
|
||||
calculators := []patch.Calculator{patch.NewResourceUpdatesCalculator(recommendationProvider), patch.NewObservedContainersCalculator()}
|
||||
calculators := []patch.Calculator{patch.NewResourceUpdatesCalculator(recommendationProvider, *maxAllowedCPUBoost), patch.NewObservedContainersCalculator()}
|
||||
as := logic.NewAdmissionServer(podPreprocessor, vpaPreprocessor, limitRangeCalculator, vpaMatcher, calculators)
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
as.Serve(w, r)
|
||||
|
|
|
|||
|
|
@ -21,10 +21,14 @@ import (
|
|||
"strings"
|
||||
|
||||
core "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
resource_admission "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource"
|
||||
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/recommendation"
|
||||
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
|
||||
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/features"
|
||||
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/annotations"
|
||||
resourcehelpers "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/resources"
|
||||
vpa_api_util "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa"
|
||||
)
|
||||
|
|
@ -37,13 +41,19 @@ const (
|
|||
|
||||
type resourcesUpdatesPatchCalculator struct {
|
||||
recommendationProvider recommendation.Provider
|
||||
maxAllowedCPUBoost resource.Quantity
|
||||
}
|
||||
|
||||
// NewResourceUpdatesCalculator returns a calculator for
|
||||
// resource update patches.
|
||||
func NewResourceUpdatesCalculator(recommendationProvider recommendation.Provider) Calculator {
|
||||
func NewResourceUpdatesCalculator(recommendationProvider recommendation.Provider, maxAllowedCPUBoost string) Calculator {
|
||||
var maxAllowedCPUBoostQuantity resource.Quantity
|
||||
if maxAllowedCPUBoost != "" {
|
||||
maxAllowedCPUBoostQuantity = resource.MustParse(maxAllowedCPUBoost)
|
||||
}
|
||||
return &resourcesUpdatesPatchCalculator{
|
||||
recommendationProvider: recommendationProvider,
|
||||
maxAllowedCPUBoost: maxAllowedCPUBoostQuantity,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -52,11 +62,22 @@ func (*resourcesUpdatesPatchCalculator) PatchResourceTarget() PatchResourceTarge
|
|||
}
|
||||
|
||||
func (c *resourcesUpdatesPatchCalculator) CalculatePatches(pod *core.Pod, vpa *vpa_types.VerticalPodAutoscaler) ([]resource_admission.PatchRecord, error) {
|
||||
klog.Infof("Calculating patches for pod %s/%s with VPA %s", pod.Namespace, pod.Name, vpa.Name)
|
||||
result := []resource_admission.PatchRecord{}
|
||||
|
||||
containersResources, annotationsPerContainer, err := c.recommendationProvider.GetContainersResourcesForPod(pod, vpa)
|
||||
if err != nil {
|
||||
return []resource_admission.PatchRecord{}, fmt.Errorf("failed to calculate resource patch for pod %s/%s: %v", pod.Namespace, pod.Name, err)
|
||||
return nil, fmt.Errorf("failed to calculate resource patch for pod %s/%s: %v", pod.Namespace, pod.Name, err)
|
||||
}
|
||||
|
||||
if vpa_api_util.GetUpdateMode(vpa) == vpa_types.UpdateModeOff {
|
||||
// If update mode is "Off", we don't want to apply any recommendations,
|
||||
// but we still want to apply startup boost.
|
||||
for i := range containersResources {
|
||||
containersResources[i].Requests = nil
|
||||
containersResources[i].Limits = nil
|
||||
}
|
||||
annotationsPerContainer = vpa_api_util.ContainerToAnnotationsMap{}
|
||||
}
|
||||
|
||||
if annotationsPerContainer == nil {
|
||||
|
|
@ -65,10 +86,66 @@ func (c *resourcesUpdatesPatchCalculator) CalculatePatches(pod *core.Pod, vpa *v
|
|||
|
||||
updatesAnnotation := []string{}
|
||||
for i, containerResources := range containersResources {
|
||||
// Apply startup boost if configured
|
||||
if features.Enabled(features.CPUStartupBoost) {
|
||||
policy := vpa_api_util.GetContainerResourcePolicy(pod.Spec.Containers[i].Name, vpa.Spec.ResourcePolicy)
|
||||
if policy != nil && policy.Mode != nil && *policy.Mode == vpa_types.ContainerScalingModeOff {
|
||||
klog.V(4).InfoS("Not applying startup boost for container", "containerName", pod.Spec.Containers[i].Name, "reason", "scaling mode is Off")
|
||||
continue
|
||||
} else {
|
||||
startupBoostPolicy := getContainerStartupBoostPolicy(&pod.Spec.Containers[i], vpa)
|
||||
if startupBoostPolicy != nil {
|
||||
originalRequest := pod.Spec.Containers[i].Resources.Requests[core.ResourceCPU]
|
||||
boostedRequest, err := calculateBoostedCPU(originalRequest, startupBoostPolicy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !c.maxAllowedCPUBoost.IsZero() && boostedRequest.Cmp(c.maxAllowedCPUBoost) > 0 {
|
||||
boostedRequest = &c.maxAllowedCPUBoost
|
||||
}
|
||||
if containerResources.Requests == nil {
|
||||
containerResources.Requests = core.ResourceList{}
|
||||
}
|
||||
controlledValues := vpa_api_util.GetContainerControlledValues(pod.Spec.Containers[i].Name, vpa.Spec.ResourcePolicy)
|
||||
resourceList := core.ResourceList{core.ResourceCPU: *boostedRequest}
|
||||
if controlledValues == vpa_types.ContainerControlledValuesRequestsOnly {
|
||||
vpa_api_util.CapRecommendationToContainerLimit(resourceList, pod.Spec.Containers[i].Resources.Limits)
|
||||
}
|
||||
containerResources.Requests[core.ResourceCPU] = resourceList[core.ResourceCPU]
|
||||
|
||||
if controlledValues == vpa_types.ContainerControlledValuesRequestsAndLimits {
|
||||
if containerResources.Limits == nil {
|
||||
containerResources.Limits = core.ResourceList{}
|
||||
}
|
||||
originalLimit := pod.Spec.Containers[i].Resources.Limits[core.ResourceCPU]
|
||||
if originalLimit.IsZero() {
|
||||
originalLimit = pod.Spec.Containers[i].Resources.Requests[core.ResourceCPU]
|
||||
}
|
||||
boostedLimit, err := calculateBoostedCPU(originalLimit, startupBoostPolicy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !c.maxAllowedCPUBoost.IsZero() && boostedLimit.Cmp(c.maxAllowedCPUBoost) > 0 {
|
||||
boostedLimit = &c.maxAllowedCPUBoost
|
||||
}
|
||||
containerResources.Limits[core.ResourceCPU] = *boostedLimit
|
||||
}
|
||||
originalResources, err := annotations.GetOriginalResourcesAnnotationValue(&pod.Spec.Containers[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, GetAddAnnotationPatch(annotations.StartupCPUBoostAnnotation, originalResources))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newPatches, newUpdatesAnnotation := getContainerPatch(pod, i, annotationsPerContainer, containerResources)
|
||||
if len(newPatches) > 0 {
|
||||
result = append(result, newPatches...)
|
||||
updatesAnnotation = append(updatesAnnotation, newUpdatesAnnotation)
|
||||
}
|
||||
}
|
||||
|
||||
if len(updatesAnnotation) > 0 {
|
||||
vpaAnnotationValue := fmt.Sprintf("Pod resources updated by %s: %s", vpa.Name, strings.Join(updatesAnnotation, "; "))
|
||||
|
|
@ -77,6 +154,49 @@ func (c *resourcesUpdatesPatchCalculator) CalculatePatches(pod *core.Pod, vpa *v
|
|||
return result, nil
|
||||
}
|
||||
|
||||
func getContainerStartupBoostPolicy(container *core.Container, vpa *vpa_types.VerticalPodAutoscaler) *vpa_types.StartupBoost {
|
||||
policy := vpa_api_util.GetContainerResourcePolicy(container.Name, vpa.Spec.ResourcePolicy)
|
||||
startupBoost := vpa.Spec.StartupBoost
|
||||
if policy != nil && policy.StartupBoost != nil {
|
||||
startupBoost = policy.StartupBoost
|
||||
}
|
||||
return startupBoost
|
||||
}
|
||||
|
||||
func calculateBoostedCPU(baseCPU resource.Quantity, startupBoost *vpa_types.StartupBoost) (*resource.Quantity, error) {
|
||||
if startupBoost == nil {
|
||||
return &baseCPU, nil
|
||||
}
|
||||
|
||||
boostType := startupBoost.CPU.Type
|
||||
if boostType == "" {
|
||||
boostType = vpa_types.FactorStartupBoostType
|
||||
}
|
||||
|
||||
switch boostType {
|
||||
case vpa_types.FactorStartupBoostType:
|
||||
if startupBoost.CPU.Factor == nil {
|
||||
return nil, fmt.Errorf("startupBoost.CPU.Factor is required when Type is Factor or not specified")
|
||||
}
|
||||
factor := *startupBoost.CPU.Factor
|
||||
if factor < 1 {
|
||||
return nil, fmt.Errorf("boost factor must be >= 1")
|
||||
}
|
||||
boostedCPU := baseCPU.MilliValue()
|
||||
boostedCPU = int64(float64(boostedCPU) * float64(factor))
|
||||
return resource.NewMilliQuantity(boostedCPU, resource.DecimalSI), nil
|
||||
case vpa_types.QuantityStartupBoostType:
|
||||
if startupBoost.CPU.Quantity == nil {
|
||||
return nil, fmt.Errorf("startupBoost.CPU.Quantity is required when Type is Quantity")
|
||||
}
|
||||
quantity := *startupBoost.CPU.Quantity
|
||||
boostedCPU := baseCPU.MilliValue() + quantity.MilliValue()
|
||||
return resource.NewMilliQuantity(boostedCPU, resource.DecimalSI), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported startup boost type: %s", startupBoost.CPU.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func getContainerPatch(pod *core.Pod, i int, annotationsPerContainer vpa_api_util.ContainerToAnnotationsMap, containerResources vpa_api_util.ContainerResources) ([]resource_admission.PatchRecord, string) {
|
||||
var patches []resource_admission.PatchRecord
|
||||
// Add empty resources object if missing.
|
||||
|
|
|
|||
|
|
@ -24,9 +24,12 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
core "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
|
||||
resource_admission "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource"
|
||||
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
|
||||
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/features"
|
||||
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/annotations"
|
||||
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/test"
|
||||
vpa_api_util "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa"
|
||||
)
|
||||
|
|
@ -289,11 +292,22 @@ func TestCalculatePatches_ResourceUpdates(t *testing.T) {
|
|||
addAnnotationRequest([][]string{{cpu}}, limit),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no recommendation present",
|
||||
pod: test.Pod().
|
||||
AddContainer(core.Container{}).
|
||||
AddContainerStatus(test.ContainerStatus().
|
||||
WithCPULimit(resource.MustParse("0")).Get()).Get(),
|
||||
namespace: "default",
|
||||
recommendResources: make([]vpa_api_util.ContainerResources, 1),
|
||||
recommendAnnotations: vpa_api_util.ContainerToAnnotationsMap{},
|
||||
expectPatches: []resource_admission.PatchRecord{},
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
frp := fakeRecommendationProvider{tc.recommendResources, tc.recommendAnnotations, tc.recommendError}
|
||||
c := NewResourceUpdatesCalculator(&frp)
|
||||
c := NewResourceUpdatesCalculator(&frp, "")
|
||||
patches, err := c.CalculatePatches(tc.pod, test.VerticalPodAutoscaler().WithContainer("test").WithName("name").Get())
|
||||
if tc.expectError == nil {
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -335,7 +349,7 @@ func TestGetPatches_TwoReplacementResources(t *testing.T) {
|
|||
}
|
||||
recommendAnnotations := vpa_api_util.ContainerToAnnotationsMap{}
|
||||
frp := fakeRecommendationProvider{recommendResources, recommendAnnotations, nil}
|
||||
c := NewResourceUpdatesCalculator(&frp)
|
||||
c := NewResourceUpdatesCalculator(&frp, "")
|
||||
patches, err := c.CalculatePatches(pod, test.VerticalPodAutoscaler().WithName("name").WithContainer("test").Get())
|
||||
assert.NoError(t, err)
|
||||
// Order of updates for cpu and unobtanium depends on order of iterating a map, both possible results are valid.
|
||||
|
|
@ -350,3 +364,333 @@ func TestGetPatches_TwoReplacementResources(t *testing.T) {
|
|||
AssertPatchOneOf(t, patches[2], []resource_admission.PatchRecord{cpuFirstUnobtaniumSecond, unobtaniumFirstCpuSecond})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculatePatches_StartupBoost(t *testing.T) {
|
||||
factor := int32(2)
|
||||
quantity := resource.MustParse("500m")
|
||||
invalidFactor := int32(0)
|
||||
invalidQuantity := resource.MustParse("200m")
|
||||
factor3 := int32(3)
|
||||
tests := []struct {
|
||||
name string
|
||||
pod *core.Pod
|
||||
vpa *vpa_types.VerticalPodAutoscaler
|
||||
recommendResources []vpa_api_util.ContainerResources
|
||||
recommendAnnotations vpa_api_util.ContainerToAnnotationsMap
|
||||
recommendError error
|
||||
maxAllowedCpu string
|
||||
expectPatches []resource_admission.PatchRecord
|
||||
expectError error
|
||||
featureGateEnabled bool
|
||||
}{
|
||||
{
|
||||
name: "startup boost factor",
|
||||
pod: &core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
{
|
||||
Name: "container1",
|
||||
Resources: core.ResourceRequirements{
|
||||
Requests: core.ResourceList{
|
||||
cpu: resource.MustParse("100m"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
vpa: test.VerticalPodAutoscaler().WithName("name").WithContainer("container1").WithCPUStartupBoost(vpa_types.FactorStartupBoostType, &factor, nil, "10s").Get(),
|
||||
recommendResources: []vpa_api_util.ContainerResources{
|
||||
{
|
||||
Requests: core.ResourceList{
|
||||
cpu: resource.MustParse("100m"),
|
||||
},
|
||||
},
|
||||
},
|
||||
maxAllowedCpu: "",
|
||||
featureGateEnabled: true,
|
||||
expectPatches: []resource_admission.PatchRecord{
|
||||
GetAddAnnotationPatch(annotations.StartupCPUBoostAnnotation, "{\"requests\":{\"cpu\":\"100m\"},\"limits\":{}}"),
|
||||
addResourceRequestPatch(0, cpu, "200m"),
|
||||
addLimitsPatch(0),
|
||||
addResourceLimitPatch(0, cpu, "200m"),
|
||||
GetAddAnnotationPatch(ResourceUpdatesAnnotation, "Pod resources updated by name: container 0: cpu request, cpu limit"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "startup boost quantity",
|
||||
pod: &core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
{
|
||||
Name: "container1",
|
||||
Resources: core.ResourceRequirements{
|
||||
Requests: core.ResourceList{
|
||||
cpu: resource.MustParse("100m"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
vpa: test.VerticalPodAutoscaler().WithName("name").WithContainer("container1").WithCPUStartupBoost(vpa_types.QuantityStartupBoostType, nil, &quantity, "10s").Get(),
|
||||
recommendResources: []vpa_api_util.ContainerResources{
|
||||
{
|
||||
Requests: core.ResourceList{
|
||||
cpu: resource.MustParse("100m"),
|
||||
},
|
||||
},
|
||||
},
|
||||
maxAllowedCpu: "",
|
||||
featureGateEnabled: true,
|
||||
expectPatches: []resource_admission.PatchRecord{
|
||||
GetAddAnnotationPatch(annotations.StartupCPUBoostAnnotation, "{\"requests\":{\"cpu\":\"100m\"},\"limits\":{}}"),
|
||||
addResourceRequestPatch(0, cpu, "600m"),
|
||||
addLimitsPatch(0),
|
||||
addResourceLimitPatch(0, cpu, "600m"),
|
||||
GetAddAnnotationPatch(ResourceUpdatesAnnotation, "Pod resources updated by name: container 0: cpu request, cpu limit"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "feature gate disabled",
|
||||
pod: &core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
{
|
||||
Name: "container1",
|
||||
Resources: core.ResourceRequirements{
|
||||
Requests: core.ResourceList{
|
||||
cpu: resource.MustParse("100m"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
vpa: test.VerticalPodAutoscaler().WithName("name").WithContainer("container1").WithCPUStartupBoost(vpa_types.FactorStartupBoostType, &factor, nil, "10s").Get(),
|
||||
recommendResources: []vpa_api_util.ContainerResources{
|
||||
{
|
||||
Requests: core.ResourceList{
|
||||
cpu: resource.MustParse("100m"),
|
||||
},
|
||||
},
|
||||
},
|
||||
maxAllowedCpu: "",
|
||||
featureGateEnabled: false,
|
||||
expectPatches: []resource_admission.PatchRecord{
|
||||
addResourceRequestPatch(0, cpu, "100m"),
|
||||
addAnnotationRequest([][]string{{cpu}}, "request"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid factor",
|
||||
pod: &core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
{
|
||||
Name: "container1",
|
||||
Resources: core.ResourceRequirements{
|
||||
Requests: core.ResourceList{
|
||||
cpu: resource.MustParse("100m"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
vpa: test.VerticalPodAutoscaler().WithName("name").WithContainer("container1").WithCPUStartupBoost(vpa_types.FactorStartupBoostType, &invalidFactor, nil, "10s").Get(),
|
||||
recommendResources: []vpa_api_util.ContainerResources{
|
||||
{
|
||||
Requests: core.ResourceList{
|
||||
cpu: resource.MustParse("100m"),
|
||||
},
|
||||
},
|
||||
},
|
||||
maxAllowedCpu: "",
|
||||
featureGateEnabled: true,
|
||||
expectError: fmt.Errorf("boost factor must be >= 1"),
|
||||
},
|
||||
{
|
||||
name: "quantity less than request",
|
||||
pod: &core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
{
|
||||
Name: "container1",
|
||||
Resources: core.ResourceRequirements{
|
||||
Requests: core.ResourceList{
|
||||
cpu: resource.MustParse("500m"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
vpa: test.VerticalPodAutoscaler().WithName("name").WithContainer("container1").WithCPUStartupBoost(vpa_types.QuantityStartupBoostType, nil, &invalidQuantity, "10s").Get(),
|
||||
recommendResources: []vpa_api_util.ContainerResources{
|
||||
{
|
||||
Requests: core.ResourceList{
|
||||
cpu: resource.MustParse("500m"),
|
||||
},
|
||||
},
|
||||
},
|
||||
maxAllowedCpu: "",
|
||||
featureGateEnabled: true,
|
||||
expectPatches: []resource_admission.PatchRecord{
|
||||
GetAddAnnotationPatch(annotations.StartupCPUBoostAnnotation, "{\"requests\":{\"cpu\":\"500m\"},\"limits\":{}}"),
|
||||
addResourceRequestPatch(0, cpu, "700m"),
|
||||
addLimitsPatch(0),
|
||||
addResourceLimitPatch(0, cpu, "700m"),
|
||||
GetAddAnnotationPatch(ResourceUpdatesAnnotation, "Pod resources updated by name: container 0: cpu request, cpu limit"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "startup boost capped",
|
||||
pod: &core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
{
|
||||
Name: "container1",
|
||||
Resources: core.ResourceRequirements{
|
||||
Requests: core.ResourceList{
|
||||
cpu: resource.MustParse("1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
vpa: test.VerticalPodAutoscaler().WithName("name").WithContainer("container1").WithCPUStartupBoost(vpa_types.FactorStartupBoostType, &factor3, nil, "1s").Get(),
|
||||
recommendResources: []vpa_api_util.ContainerResources{
|
||||
{
|
||||
Requests: core.ResourceList{
|
||||
cpu: resource.MustParse("1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
maxAllowedCpu: "2",
|
||||
featureGateEnabled: true,
|
||||
expectPatches: []resource_admission.PatchRecord{
|
||||
GetAddAnnotationPatch(annotations.StartupCPUBoostAnnotation, "{\"requests\":{\"cpu\":\"1\"},\"limits\":{}}"),
|
||||
addResourceRequestPatch(0, cpu, "2"),
|
||||
addLimitsPatch(0),
|
||||
addResourceLimitPatch(0, cpu, "2"),
|
||||
GetAddAnnotationPatch(ResourceUpdatesAnnotation, "Pod resources updated by name: container 0: cpu request, cpu limit"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "startup boost with scaling mode off",
|
||||
pod: &core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
{
|
||||
Name: "container1",
|
||||
Resources: core.ResourceRequirements{
|
||||
Requests: core.ResourceList{
|
||||
cpu: resource.MustParse("100m"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
vpa: test.VerticalPodAutoscaler().WithName("name").WithContainer("container1").WithCPUStartupBoost(vpa_types.FactorStartupBoostType, &factor, nil, "10s").WithScalingMode("container1", vpa_types.ContainerScalingModeOff).Get(),
|
||||
recommendResources: []vpa_api_util.ContainerResources{
|
||||
{
|
||||
Requests: core.ResourceList{
|
||||
cpu: resource.MustParse("1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
maxAllowedCpu: "",
|
||||
featureGateEnabled: true,
|
||||
expectPatches: []resource_admission.PatchRecord{},
|
||||
},
|
||||
{
|
||||
name: "startup boost no recommendation",
|
||||
pod: &core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
{
|
||||
Name: "container1",
|
||||
Resources: core.ResourceRequirements{
|
||||
Requests: core.ResourceList{
|
||||
cpu: resource.MustParse("100m"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
vpa: test.VerticalPodAutoscaler().WithName("name").WithContainer("container1").WithCPUStartupBoost(vpa_types.FactorStartupBoostType, &factor, nil, "10s").Get(),
|
||||
recommendResources: make([]vpa_api_util.ContainerResources, 1),
|
||||
maxAllowedCpu: "",
|
||||
featureGateEnabled: true,
|
||||
expectPatches: []resource_admission.PatchRecord{
|
||||
GetAddAnnotationPatch(annotations.StartupCPUBoostAnnotation, "{\"requests\":{\"cpu\":\"100m\"},\"limits\":{}}"),
|
||||
addResourceRequestPatch(0, cpu, "200m"),
|
||||
addLimitsPatch(0),
|
||||
addResourceLimitPatch(0, cpu, "200m"),
|
||||
GetAddAnnotationPatch(ResourceUpdatesAnnotation, "Pod resources updated by name: container 0: cpu request, cpu limit"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "startup boost with ControlledValues=RequestsOnly",
|
||||
pod: &core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
{
|
||||
Name: "container1",
|
||||
Resources: core.ResourceRequirements{
|
||||
Requests: core.ResourceList{
|
||||
cpu: resource.MustParse("100m"),
|
||||
},
|
||||
Limits: core.ResourceList{
|
||||
cpu: resource.MustParse("200m"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
vpa: test.VerticalPodAutoscaler().WithName("name").WithContainer("container1").WithCPUStartupBoost(vpa_types.FactorStartupBoostType, &factor, nil, "10s").WithControlledValues("container1", vpa_types.ContainerControlledValuesRequestsOnly).Get(),
|
||||
recommendResources: []vpa_api_util.ContainerResources{
|
||||
{
|
||||
Requests: core.ResourceList{
|
||||
cpu: resource.MustParse("100m"),
|
||||
},
|
||||
},
|
||||
},
|
||||
maxAllowedCpu: "",
|
||||
featureGateEnabled: true,
|
||||
expectPatches: []resource_admission.PatchRecord{
|
||||
GetAddAnnotationPatch(annotations.StartupCPUBoostAnnotation, "{\"requests\":{\"cpu\":\"100m\"},\"limits\":{\"cpu\":\"200m\"}}"),
|
||||
addResourceRequestPatch(0, cpu, "200m"),
|
||||
GetAddAnnotationPatch(ResourceUpdatesAnnotation, "Pod resources updated by name: container 0: cpu request"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, features.MutableFeatureGate, features.CPUStartupBoost, tc.featureGateEnabled)
|
||||
|
||||
frp := fakeRecommendationProvider{tc.recommendResources, tc.recommendAnnotations, tc.recommendError}
|
||||
c := NewResourceUpdatesCalculator(&frp, tc.maxAllowedCpu)
|
||||
patches, err := c.CalculatePatches(tc.pod, tc.vpa)
|
||||
if tc.expectError == nil {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
if assert.Error(t, err) {
|
||||
assert.Equal(t, tc.expectError.Error(), err.Error())
|
||||
}
|
||||
}
|
||||
if assert.Len(t, patches, len(tc.expectPatches), fmt.Sprintf("got %+v, want %+v", patches, tc.expectPatches)) {
|
||||
for i, gotPatch := range patches {
|
||||
if !EqPatch(gotPatch, tc.expectPatches[i]) {
|
||||
t.Errorf("Expected patch at position %d to be %+v, got %+v", i, tc.expectPatches[i], gotPatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ func (m *matcher) GetMatchingVPA(ctx context.Context, pod *core.Pod) *vpa_types.
|
|||
|
||||
var controllingVpa *vpa_types.VerticalPodAutoscaler
|
||||
for _, vpaConfig := range configs {
|
||||
if vpa_api_util.GetUpdateMode(vpaConfig) == vpa_types.UpdateModeOff {
|
||||
if vpa_api_util.GetUpdateMode(vpaConfig) == vpa_types.UpdateModeOff && vpaConfig.Spec.StartupBoost == nil {
|
||||
continue
|
||||
}
|
||||
if vpaConfig.Spec.TargetRef == nil {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
Copyright 2025 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 annotations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
core "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
// StartupCPUBoostAnnotation is the annotation set on a pod when a CPU boost is applied.
|
||||
// The value of the annotation is the original resource specification of the container.
|
||||
StartupCPUBoostAnnotation = "startup-cpu-boost"
|
||||
)
|
||||
|
||||
// OriginalResources contains the original resources of a container.
|
||||
type OriginalResources struct {
|
||||
Requests core.ResourceList `json:"requests"`
|
||||
Limits core.ResourceList `json:"limits"`
|
||||
}
|
||||
|
||||
// GetOriginalResourcesAnnotationValue returns the annotation value for the original resources.
|
||||
func GetOriginalResourcesAnnotationValue(container *core.Container) (string, error) {
|
||||
original := OriginalResources{
|
||||
Requests: core.ResourceList{},
|
||||
Limits: core.ResourceList{},
|
||||
}
|
||||
if cpu, ok := container.Resources.Requests[core.ResourceCPU]; ok {
|
||||
original.Requests[core.ResourceCPU] = cpu
|
||||
}
|
||||
if mem, ok := container.Resources.Requests[core.ResourceMemory]; ok {
|
||||
original.Requests[core.ResourceMemory] = mem
|
||||
}
|
||||
if cpu, ok := container.Resources.Limits[core.ResourceCPU]; ok {
|
||||
original.Limits[core.ResourceCPU] = cpu
|
||||
}
|
||||
if mem, ok := container.Resources.Limits[core.ResourceMemory]; ok {
|
||||
original.Limits[core.ResourceMemory] = mem
|
||||
}
|
||||
b, err := json.Marshal(original)
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
// GetOriginalResourcesFromAnnotation returns the original resources from the annotation.
|
||||
func GetOriginalResourcesFromAnnotation(pod *core.Pod) (*OriginalResources, error) {
|
||||
val, ok := pod.Annotations[StartupCPUBoostAnnotation]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
var original OriginalResources
|
||||
err := json.Unmarshal([]byte(val), &original)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &original, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
Copyright 2025 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 annotations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
core "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestGetOriginalResourcesAnnotationValue(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
container *core.Container
|
||||
expected *OriginalResources
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "full resources",
|
||||
container: &core.Container{
|
||||
Resources: core.ResourceRequirements{
|
||||
Requests: core.ResourceList{
|
||||
core.ResourceCPU: resource.MustParse("1"),
|
||||
core.ResourceMemory: resource.MustParse("1Gi"),
|
||||
},
|
||||
Limits: core.ResourceList{
|
||||
core.ResourceCPU: resource.MustParse("2"),
|
||||
core.ResourceMemory: resource.MustParse("2Gi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: &OriginalResources{
|
||||
Requests: core.ResourceList{
|
||||
core.ResourceCPU: resource.MustParse("1"),
|
||||
core.ResourceMemory: resource.MustParse("1Gi"),
|
||||
},
|
||||
Limits: core.ResourceList{
|
||||
core.ResourceCPU: resource.MustParse("2"),
|
||||
core.ResourceMemory: resource.MustParse("2Gi"),
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "only requests",
|
||||
container: &core.Container{
|
||||
Resources: core.ResourceRequirements{
|
||||
Requests: core.ResourceList{
|
||||
core.ResourceCPU: resource.MustParse("1"),
|
||||
core.ResourceMemory: resource.MustParse("1Gi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: &OriginalResources{
|
||||
Requests: core.ResourceList{
|
||||
core.ResourceCPU: resource.MustParse("1"),
|
||||
core.ResourceMemory: resource.MustParse("1Gi"),
|
||||
},
|
||||
Limits: core.ResourceList{},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "no resources",
|
||||
container: &core.Container{
|
||||
Resources: core.ResourceRequirements{},
|
||||
},
|
||||
expected: &OriginalResources{
|
||||
Requests: core.ResourceList{},
|
||||
Limits: core.ResourceList{},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
val, err := GetOriginalResourcesAnnotationValue(tc.container)
|
||||
if tc.expectErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
var got OriginalResources
|
||||
err = json.Unmarshal([]byte(val), &got)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, tc.expected.Requests.Cpu().Equal(*got.Requests.Cpu()), "CPU requests do not match")
|
||||
assert.True(t, tc.expected.Requests.Memory().Equal(*got.Requests.Memory()), "Memory requests do not match")
|
||||
assert.True(t, tc.expected.Limits.Cpu().Equal(*got.Limits.Cpu()), "CPU limits do not match")
|
||||
assert.True(t, tc.expected.Limits.Memory().Equal(*got.Limits.Memory()), "Memory limits do not match")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOriginalResourcesFromAnnotation(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
pod *core.Pod
|
||||
expected *OriginalResources
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid annotation",
|
||||
pod: &core.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
StartupCPUBoostAnnotation: `{"requests":{"cpu":"1","memory":"1Gi"},"limits":{"cpu":"2","memory":"2Gi"}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: &OriginalResources{
|
||||
Requests: core.ResourceList{
|
||||
core.ResourceCPU: resource.MustParse("1"),
|
||||
core.ResourceMemory: resource.MustParse("1Gi"),
|
||||
},
|
||||
Limits: core.ResourceList{
|
||||
core.ResourceCPU: resource.MustParse("2"),
|
||||
core.ResourceMemory: resource.MustParse("2Gi"),
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "no annotation",
|
||||
pod: &core.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
expected: nil,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid json",
|
||||
pod: &core.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
StartupCPUBoostAnnotation: "invalid-json",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: nil,
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := GetOriginalResourcesFromAnnotation(tc.pod)
|
||||
if tc.expectErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
if tc.expected == nil {
|
||||
assert.Nil(t, got)
|
||||
} else {
|
||||
assert.NotNil(t, got)
|
||||
assert.True(t, tc.expected.Requests.Cpu().Equal(*got.Requests.Cpu()), "CPU requests do not match")
|
||||
assert.True(t, tc.expected.Requests.Memory().Equal(*got.Requests.Memory()), "Memory requests do not match")
|
||||
assert.True(t, tc.expected.Limits.Cpu().Equal(*got.Limits.Cpu()), "CPU limits do not match")
|
||||
assert.True(t, tc.expected.Limits.Memory().Equal(*got.Limits.Memory()), "Memory limits do not match")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import (
|
|||
|
||||
autoscaling "k8s.io/api/autoscaling/v1"
|
||||
core "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
|
||||
|
|
@ -47,6 +48,7 @@ type VerticalPodAutoscalerBuilder interface {
|
|||
WithGroupVersion(gv meta.GroupVersion) VerticalPodAutoscalerBuilder
|
||||
WithEvictionRequirements([]*vpa_types.EvictionRequirement) VerticalPodAutoscalerBuilder
|
||||
WithMinReplicas(minReplicas *int32) VerticalPodAutoscalerBuilder
|
||||
WithCPUStartupBoost(boostType vpa_types.StartupBoostType, factor *int32, quantity *resource.Quantity, duration string) VerticalPodAutoscalerBuilder
|
||||
AppendCondition(conditionType vpa_types.VerticalPodAutoscalerConditionType,
|
||||
status core.ConditionStatus, reason, message string, lastTransitionTime time.Time) VerticalPodAutoscalerBuilder
|
||||
AppendRecommendation(vpa_types.RecommendedContainerResources) VerticalPodAutoscalerBuilder
|
||||
|
|
@ -81,6 +83,7 @@ type verticalPodAutoscalerBuilder struct {
|
|||
maxAllowed map[string]core.ResourceList
|
||||
controlledValues map[string]*vpa_types.ContainerControlledValues
|
||||
scalingMode map[string]*vpa_types.ContainerScalingMode
|
||||
startupBoost *vpa_types.StartupBoost
|
||||
recommendation RecommendationBuilder
|
||||
conditions []vpa_types.VerticalPodAutoscalerCondition
|
||||
annotations map[string]string
|
||||
|
|
@ -232,6 +235,24 @@ func (b *verticalPodAutoscalerBuilder) AppendRecommendation(recommendation vpa_t
|
|||
return &c
|
||||
}
|
||||
|
||||
func (b *verticalPodAutoscalerBuilder) WithCPUStartupBoost(boostType vpa_types.StartupBoostType, factor *int32, quantity *resource.Quantity, duration string) VerticalPodAutoscalerBuilder {
|
||||
c := *b
|
||||
parsedDuration, _ := time.ParseDuration(duration)
|
||||
cpuStartupBoost := &vpa_types.GenericStartupBoost{
|
||||
Type: boostType,
|
||||
Duration: &meta.Duration{Duration: parsedDuration},
|
||||
}
|
||||
if factor != nil {
|
||||
cpuStartupBoost.Factor = factor
|
||||
} else {
|
||||
cpuStartupBoost.Quantity = quantity
|
||||
}
|
||||
c.startupBoost = &vpa_types.StartupBoost{
|
||||
CPU: cpuStartupBoost,
|
||||
}
|
||||
return &c
|
||||
}
|
||||
|
||||
func (b *verticalPodAutoscalerBuilder) Get() *vpa_types.VerticalPodAutoscaler {
|
||||
if len(b.containerNames) == 0 {
|
||||
panic("Must call WithContainer() before Get()")
|
||||
|
|
@ -280,6 +301,7 @@ func (b *verticalPodAutoscalerBuilder) Get() *vpa_types.VerticalPodAutoscaler {
|
|||
ResourcePolicy: &resourcePolicy,
|
||||
TargetRef: b.targetRef,
|
||||
Recommenders: recommenders,
|
||||
StartupBoost: b.startupBoost,
|
||||
},
|
||||
Status: vpa_types.VerticalPodAutoscalerStatus{
|
||||
Recommendation: recommendation,
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ func getCappedRecommendationForContainer(
|
|||
}
|
||||
// TODO: If limits and policy are conflicting, set some condition on the VPA.
|
||||
if containerControlledValues == vpa_types.ContainerControlledValuesRequestsOnly {
|
||||
annotations = capRecommendationToContainerLimit(recommendation, containerLimits)
|
||||
annotations = CapRecommendationToContainerLimit(recommendation, containerLimits)
|
||||
if genAnnotations {
|
||||
cappingAnnotations = append(cappingAnnotations, annotations...)
|
||||
}
|
||||
|
|
@ -150,9 +150,9 @@ func getCappedRecommendationForContainer(
|
|||
return cappedRecommendations, cappingAnnotations, nil
|
||||
}
|
||||
|
||||
// capRecommendationToContainerLimit makes sure recommendation is not above current limit for the container.
|
||||
// CapRecommendationToContainerLimit makes sure recommendation is not above current limit for the container.
|
||||
// If this function makes adjustments appropriate annotations are returned.
|
||||
func capRecommendationToContainerLimit(recommendation apiv1.ResourceList, containerLimits apiv1.ResourceList) []string {
|
||||
func CapRecommendationToContainerLimit(recommendation apiv1.ResourceList, containerLimits apiv1.ResourceList) []string {
|
||||
annotations := make([]string, 0)
|
||||
// Iterate over limits set in the container. Unset means Infinite limit.
|
||||
for resourceName, limit := range containerLimits {
|
||||
|
|
|
|||
Loading…
Reference in New Issue