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` | 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. |
|
| `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 |
|
| `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") |
|
| `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) |
|
| `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. |
|
| `port` | int | 8000 | The port to listen on. |
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
kube_client "k8s.io/client-go/kubernetes"
|
kube_client "k8s.io/client-go/kubernetes"
|
||||||
typedadmregv1 "k8s.io/client-go/kubernetes/typed/admissionregistration/v1"
|
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.")
|
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")
|
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")
|
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() {
|
func main() {
|
||||||
|
|
@ -93,6 +95,13 @@ func main() {
|
||||||
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
|
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)
|
healthCheck := metrics.NewHealthCheck(time.Minute)
|
||||||
metrics_admission.Register()
|
metrics_admission.Register()
|
||||||
server.Initialize(&commonFlags.EnableProfiling, healthCheck, address)
|
server.Initialize(&commonFlags.EnableProfiling, healthCheck, address)
|
||||||
|
|
@ -145,7 +154,7 @@ func main() {
|
||||||
hostname,
|
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)
|
as := logic.NewAdmissionServer(podPreprocessor, vpaPreprocessor, limitRangeCalculator, vpaMatcher, calculators)
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
as.Serve(w, r)
|
as.Serve(w, r)
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,14 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
core "k8s.io/api/core/v1"
|
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"
|
resource_admission "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource"
|
||||||
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/recommendation"
|
"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"
|
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"
|
resourcehelpers "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/resources"
|
||||||
vpa_api_util "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa"
|
vpa_api_util "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa"
|
||||||
)
|
)
|
||||||
|
|
@ -37,13 +41,19 @@ const (
|
||||||
|
|
||||||
type resourcesUpdatesPatchCalculator struct {
|
type resourcesUpdatesPatchCalculator struct {
|
||||||
recommendationProvider recommendation.Provider
|
recommendationProvider recommendation.Provider
|
||||||
|
maxAllowedCPUBoost resource.Quantity
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewResourceUpdatesCalculator returns a calculator for
|
// NewResourceUpdatesCalculator returns a calculator for
|
||||||
// resource update patches.
|
// 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{
|
return &resourcesUpdatesPatchCalculator{
|
||||||
recommendationProvider: recommendationProvider,
|
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) {
|
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{}
|
result := []resource_admission.PatchRecord{}
|
||||||
|
|
||||||
containersResources, annotationsPerContainer, err := c.recommendationProvider.GetContainersResourcesForPod(pod, vpa)
|
containersResources, annotationsPerContainer, err := c.recommendationProvider.GetContainersResourcesForPod(pod, vpa)
|
||||||
if err != nil {
|
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 {
|
if annotationsPerContainer == nil {
|
||||||
|
|
@ -65,9 +86,65 @@ func (c *resourcesUpdatesPatchCalculator) CalculatePatches(pod *core.Pod, vpa *v
|
||||||
|
|
||||||
updatesAnnotation := []string{}
|
updatesAnnotation := []string{}
|
||||||
for i, containerResources := range containersResources {
|
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)
|
newPatches, newUpdatesAnnotation := getContainerPatch(pod, i, annotationsPerContainer, containerResources)
|
||||||
result = append(result, newPatches...)
|
if len(newPatches) > 0 {
|
||||||
updatesAnnotation = append(updatesAnnotation, newUpdatesAnnotation)
|
result = append(result, newPatches...)
|
||||||
|
updatesAnnotation = append(updatesAnnotation, newUpdatesAnnotation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(updatesAnnotation) > 0 {
|
if len(updatesAnnotation) > 0 {
|
||||||
|
|
@ -77,6 +154,49 @@ func (c *resourcesUpdatesPatchCalculator) CalculatePatches(pod *core.Pod, vpa *v
|
||||||
return result, nil
|
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) {
|
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
|
var patches []resource_admission.PatchRecord
|
||||||
// Add empty resources object if missing.
|
// Add empty resources object if missing.
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,12 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
core "k8s.io/api/core/v1"
|
core "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/api/resource"
|
"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"
|
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"
|
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"
|
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/test"
|
||||||
vpa_api_util "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa"
|
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),
|
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 {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
frp := fakeRecommendationProvider{tc.recommendResources, tc.recommendAnnotations, tc.recommendError}
|
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())
|
patches, err := c.CalculatePatches(tc.pod, test.VerticalPodAutoscaler().WithContainer("test").WithName("name").Get())
|
||||||
if tc.expectError == nil {
|
if tc.expectError == nil {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
@ -335,7 +349,7 @@ func TestGetPatches_TwoReplacementResources(t *testing.T) {
|
||||||
}
|
}
|
||||||
recommendAnnotations := vpa_api_util.ContainerToAnnotationsMap{}
|
recommendAnnotations := vpa_api_util.ContainerToAnnotationsMap{}
|
||||||
frp := fakeRecommendationProvider{recommendResources, recommendAnnotations, nil}
|
frp := fakeRecommendationProvider{recommendResources, recommendAnnotations, nil}
|
||||||
c := NewResourceUpdatesCalculator(&frp)
|
c := NewResourceUpdatesCalculator(&frp, "")
|
||||||
patches, err := c.CalculatePatches(pod, test.VerticalPodAutoscaler().WithName("name").WithContainer("test").Get())
|
patches, err := c.CalculatePatches(pod, test.VerticalPodAutoscaler().WithName("name").WithContainer("test").Get())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
// Order of updates for cpu and unobtanium depends on order of iterating a map, both possible results are valid.
|
// 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})
|
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
|
var controllingVpa *vpa_types.VerticalPodAutoscaler
|
||||||
for _, vpaConfig := range configs {
|
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
|
continue
|
||||||
}
|
}
|
||||||
if vpaConfig.Spec.TargetRef == nil {
|
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"
|
autoscaling "k8s.io/api/autoscaling/v1"
|
||||||
core "k8s.io/api/core/v1"
|
core "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/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
|
WithGroupVersion(gv meta.GroupVersion) VerticalPodAutoscalerBuilder
|
||||||
WithEvictionRequirements([]*vpa_types.EvictionRequirement) VerticalPodAutoscalerBuilder
|
WithEvictionRequirements([]*vpa_types.EvictionRequirement) VerticalPodAutoscalerBuilder
|
||||||
WithMinReplicas(minReplicas *int32) VerticalPodAutoscalerBuilder
|
WithMinReplicas(minReplicas *int32) VerticalPodAutoscalerBuilder
|
||||||
|
WithCPUStartupBoost(boostType vpa_types.StartupBoostType, factor *int32, quantity *resource.Quantity, duration string) VerticalPodAutoscalerBuilder
|
||||||
AppendCondition(conditionType vpa_types.VerticalPodAutoscalerConditionType,
|
AppendCondition(conditionType vpa_types.VerticalPodAutoscalerConditionType,
|
||||||
status core.ConditionStatus, reason, message string, lastTransitionTime time.Time) VerticalPodAutoscalerBuilder
|
status core.ConditionStatus, reason, message string, lastTransitionTime time.Time) VerticalPodAutoscalerBuilder
|
||||||
AppendRecommendation(vpa_types.RecommendedContainerResources) VerticalPodAutoscalerBuilder
|
AppendRecommendation(vpa_types.RecommendedContainerResources) VerticalPodAutoscalerBuilder
|
||||||
|
|
@ -81,6 +83,7 @@ type verticalPodAutoscalerBuilder struct {
|
||||||
maxAllowed map[string]core.ResourceList
|
maxAllowed map[string]core.ResourceList
|
||||||
controlledValues map[string]*vpa_types.ContainerControlledValues
|
controlledValues map[string]*vpa_types.ContainerControlledValues
|
||||||
scalingMode map[string]*vpa_types.ContainerScalingMode
|
scalingMode map[string]*vpa_types.ContainerScalingMode
|
||||||
|
startupBoost *vpa_types.StartupBoost
|
||||||
recommendation RecommendationBuilder
|
recommendation RecommendationBuilder
|
||||||
conditions []vpa_types.VerticalPodAutoscalerCondition
|
conditions []vpa_types.VerticalPodAutoscalerCondition
|
||||||
annotations map[string]string
|
annotations map[string]string
|
||||||
|
|
@ -232,6 +235,24 @@ func (b *verticalPodAutoscalerBuilder) AppendRecommendation(recommendation vpa_t
|
||||||
return &c
|
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 {
|
func (b *verticalPodAutoscalerBuilder) Get() *vpa_types.VerticalPodAutoscaler {
|
||||||
if len(b.containerNames) == 0 {
|
if len(b.containerNames) == 0 {
|
||||||
panic("Must call WithContainer() before Get()")
|
panic("Must call WithContainer() before Get()")
|
||||||
|
|
@ -280,6 +301,7 @@ func (b *verticalPodAutoscalerBuilder) Get() *vpa_types.VerticalPodAutoscaler {
|
||||||
ResourcePolicy: &resourcePolicy,
|
ResourcePolicy: &resourcePolicy,
|
||||||
TargetRef: b.targetRef,
|
TargetRef: b.targetRef,
|
||||||
Recommenders: recommenders,
|
Recommenders: recommenders,
|
||||||
|
StartupBoost: b.startupBoost,
|
||||||
},
|
},
|
||||||
Status: vpa_types.VerticalPodAutoscalerStatus{
|
Status: vpa_types.VerticalPodAutoscalerStatus{
|
||||||
Recommendation: recommendation,
|
Recommendation: recommendation,
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ func getCappedRecommendationForContainer(
|
||||||
}
|
}
|
||||||
// TODO: If limits and policy are conflicting, set some condition on the VPA.
|
// TODO: If limits and policy are conflicting, set some condition on the VPA.
|
||||||
if containerControlledValues == vpa_types.ContainerControlledValuesRequestsOnly {
|
if containerControlledValues == vpa_types.ContainerControlledValuesRequestsOnly {
|
||||||
annotations = capRecommendationToContainerLimit(recommendation, containerLimits)
|
annotations = CapRecommendationToContainerLimit(recommendation, containerLimits)
|
||||||
if genAnnotations {
|
if genAnnotations {
|
||||||
cappingAnnotations = append(cappingAnnotations, annotations...)
|
cappingAnnotations = append(cappingAnnotations, annotations...)
|
||||||
}
|
}
|
||||||
|
|
@ -150,9 +150,9 @@ func getCappedRecommendationForContainer(
|
||||||
return cappedRecommendations, cappingAnnotations, nil
|
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.
|
// 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)
|
annotations := make([]string, 0)
|
||||||
// Iterate over limits set in the container. Unset means Infinite limit.
|
// Iterate over limits set in the container. Unset means Infinite limit.
|
||||||
for resourceName, limit := range containerLimits {
|
for resourceName, limit := range containerLimits {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue