From c30951bace3b2998bbd4ec75799027368337771e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Stefaniak?= Date: Mon, 4 Dec 2023 16:17:11 +0100 Subject: [PATCH] Introduced resource container policies (#13) * Introduced resource container policies --- README.md | 19 +- api/v1alpha1/startupcpuboost_types.go | 36 +- api/v1alpha1/zz_generated.deepcopy.go | 52 +++ ...autoscaling.x-k8s.io_startupcpuboosts.yaml | 35 +- go.mod | 2 +- go.sum | 2 +- internal/boost/boost_suite_test.go | 31 +- internal/boost/{policy => duration}/fixed.go | 6 +- .../boost/{policy => duration}/fixed_test.go | 18 +- .../{policy => duration}/podcondition.go | 4 +- .../{policy => duration}/podcondition_test.go | 8 +- internal/boost/{policy => duration}/policy.go | 6 +- .../{policy => duration}/policy_suite_test.go | 2 +- internal/boost/manager.go | 10 +- internal/boost/resource/percentage.go | 59 +++ internal/boost/resource/percentage_test.go | 73 ++++ internal/boost/resource/resource.go | 22 ++ .../boost/resource/resource_suite_test.go | 49 +++ internal/boost/startupcpuboost.go | 74 ++-- internal/boost/startupcpuboost_test.go | 30 +- internal/controller/boost_pod_handler.go | 2 +- internal/controller/boost_pod_handler_test.go | 7 +- internal/controller/controller_suite_test.go | 11 +- internal/mock/startupcpuboost.go | 37 +- internal/webhook/doc.go | 17 + internal/webhook/podcpuboost_webhook.go | 90 ++--- internal/webhook/podcpuboost_webhook_test.go | 361 ++++++++++++------ .../webhook/podcpuboost_webhook_test.go.old | 169 ++++++++ internal/webhook/webhook_suite_test.go | 90 +++++ 29 files changed, 1024 insertions(+), 298 deletions(-) rename internal/boost/{policy => duration}/fixed.go (91%) rename internal/boost/{policy => duration}/fixed_test.go (75%) rename internal/boost/{policy => duration}/podcondition.go (95%) rename internal/boost/{policy => duration}/podcondition_test.go (90%) rename internal/boost/{policy => duration}/policy.go (85%) rename internal/boost/{policy => duration}/policy_suite_test.go (97%) create mode 100644 internal/boost/resource/percentage.go create mode 100644 internal/boost/resource/percentage_test.go create mode 100644 internal/boost/resource/resource.go create mode 100644 internal/boost/resource/resource_suite_test.go create mode 100644 internal/webhook/doc.go create mode 100644 internal/webhook/podcpuboost_webhook_test.go.old create mode 100644 internal/webhook/webhook_suite_test.go diff --git a/README.md b/README.md index f961ce7..e9c2b9f 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ enabled.** To install the latest release of Kube Startup CPU Boost in your cluster, run the following command: ```sh -kubectl apply -f https://github.com/google/kube-startup-cpu-boost/releases/download/v0.0.2/manifests.yaml +kubectl apply -f https://github.com/google/kube-startup-cpu-boost/releases/download/v0.1.0/manifests.yaml ``` The Kube Startup CPU Boost components run in `kube-startup-cpu-boost-system` namespace. @@ -71,7 +71,7 @@ gcloud container clusters create poc \ 1. Create `StartupCPUBoost` object in your workload's namespace - ```sh + ```yaml apiVersion: autoscaling.x-k8s.io/v1alpha1 kind: StartupCPUBoost metadata: @@ -79,19 +79,24 @@ gcloud container clusters create poc \ namespace: demo selector: matchExpressions: - - key: app + - key: app.kubernetes.io/name operator: In - values: ["app-001", "app-002"] + values: ["spring-rest-jpa"] spec: - boostPercent: 50 + resourcePolicy: + containerPolicies: + - containerName: spring-rest-jpa + percentageIncrease: + value: 50 durationPolicy: podCondition: type: Ready status: "True" ``` - The above example will boost CPU requests and limits of all PODs with `app=app-001` and `app=app-002` - labels in `demo` namespace. The resources will be increased by 50% until the + The above example will boost CPU requests and limits of a container `spring-rest-jpa` in a + PODs with `app.kubernetes.io/name=spring-rest-jpa` label in `demo` namespace. + The resources will be increased by 50% until the [POD Condition](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-conditions) `Ready` becomes `True`. diff --git a/api/v1alpha1/startupcpuboost_types.go b/api/v1alpha1/startupcpuboost_types.go index c542968..480215d 100644 --- a/api/v1alpha1/startupcpuboost_types.go +++ b/api/v1alpha1/startupcpuboost_types.go @@ -60,16 +60,42 @@ type DurationPolicy struct { PodCondition *PodConditionDurationPolicy `json:"podCondition,omitempty"` } +// PercentagePolicy defines the policy used to determine the target +// resources for a container +type PercentageIncrease struct { + // Value specifies the percentage value + // +kubebuilder:validation:Required + // +kubebuilder:validation:Minimum:=1 + Value int64 `json:"value,omitempty"` +} + +// ContainerPolicy defines the policy used to determine the target +// resources for a container +type ContainerPolicy struct { + // ContainerName specifies the name of container for a given policy + // +kubebuilder:validation:Required + ContainerName string `json:"containerName,omitempty"` + // PercentageIncrease specifies the percentage increase policy for a container + // +kubebuilder:validation:Required + PercentageIncrease PercentageIncrease `json:"percentageIncrease,omitempty"` +} + +// ResourcePolicy defines the policy used to determine the target +// resources for a POD +type ResourcePolicy struct { + // ContainerPolicies specifies resource policies for the containers + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems:=1 + ContainerPolicies []ContainerPolicy `json:"containerPolicies,omitempty"` +} + // StartupCPUBoostSpec defines the desired state of StartupCPUBoost type StartupCPUBoostSpec struct { + // ResourcePolicy specifies policies for container resource increase + ResourcePolicy ResourcePolicy `json:"resourcePolicy,omitempty"` // DurationPolicy specifies policies for resource boost duration // +kubebuilder:validation:Required DurationPolicy DurationPolicy `json:"durationPolicy,omitempty"` - // BootPercent defines the percent of CPU request increase that POD will get - // during the CPU boost time period - // +kubebuilder:validation:Required - // +kubebuilder:validation:Minimum:=1 - BoostPercent int64 `json:"boostPercent,omitempty"` } // StartupCPUBoostStatus defines the observed state of StartupCPUBoost diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 32c73cd..200bef1 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,6 +22,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContainerPolicy) DeepCopyInto(out *ContainerPolicy) { + *out = *in + out.PercentageIncrease = in.PercentageIncrease +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerPolicy. +func (in *ContainerPolicy) DeepCopy() *ContainerPolicy { + if in == nil { + return nil + } + out := new(ContainerPolicy) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DurationPolicy) DeepCopyInto(out *DurationPolicy) { *out = *in @@ -62,6 +78,21 @@ func (in *FixedDurationPolicy) DeepCopy() *FixedDurationPolicy { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PercentageIncrease) DeepCopyInto(out *PercentageIncrease) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PercentageIncrease. +func (in *PercentageIncrease) DeepCopy() *PercentageIncrease { + if in == nil { + return nil + } + out := new(PercentageIncrease) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PodConditionDurationPolicy) DeepCopyInto(out *PodConditionDurationPolicy) { *out = *in @@ -77,6 +108,26 @@ func (in *PodConditionDurationPolicy) DeepCopy() *PodConditionDurationPolicy { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourcePolicy) DeepCopyInto(out *ResourcePolicy) { + *out = *in + if in.ContainerPolicies != nil { + in, out := &in.ContainerPolicies, &out.ContainerPolicies + *out = make([]ContainerPolicy, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourcePolicy. +func (in *ResourcePolicy) DeepCopy() *ResourcePolicy { + if in == nil { + return nil + } + out := new(ResourcePolicy) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StartupCPUBoost) DeepCopyInto(out *StartupCPUBoost) { *out = *in @@ -140,6 +191,7 @@ func (in *StartupCPUBoostList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StartupCPUBoostSpec) DeepCopyInto(out *StartupCPUBoostSpec) { *out = *in + in.ResourcePolicy.DeepCopyInto(&out.ResourcePolicy) in.DurationPolicy.DeepCopyInto(&out.DurationPolicy) } diff --git a/config/crd/bases/autoscaling.x-k8s.io_startupcpuboosts.yaml b/config/crd/bases/autoscaling.x-k8s.io_startupcpuboosts.yaml index 2fedafd..430e2e6 100644 --- a/config/crd/bases/autoscaling.x-k8s.io_startupcpuboosts.yaml +++ b/config/crd/bases/autoscaling.x-k8s.io_startupcpuboosts.yaml @@ -78,12 +78,6 @@ spec: spec: description: StartupCPUBoostSpec defines the desired state of StartupCPUBoost properties: - boostPercent: - description: BootPercent defines the percent of CPU request increase - that POD will get during the CPU boost time period - format: int64 - minimum: 1 - type: integer durationPolicy: description: DurationPolicy specifies policies for resource boost duration @@ -114,6 +108,35 @@ spec: type: string type: object type: object + resourcePolicy: + description: ResourcePolicy specifies policies for container resource + increase + properties: + containerPolicies: + description: ContainerPolicies specifies resource policies for + the containers + items: + description: ContainerPolicy defines the policy used to determine + the target resources for a container + properties: + containerName: + description: ContainerName specifies the name of container + for a given policy + type: string + percentageIncrease: + description: PercentageIncrease specifies the percentage + increase policy for a container + properties: + value: + description: Value specifies the percentage value + format: int64 + minimum: 1 + type: integer + type: object + type: object + minItems: 1 + type: array + type: object type: object status: description: StartupCPUBoostStatus defines the observed state of StartupCPUBoost diff --git a/go.mod b/go.mod index 88e258e..82b6f07 100644 --- a/go.mod +++ b/go.mod @@ -60,7 +60,7 @@ require ( golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.9.3 // indirect - gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.3.0 google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index aeafda4..ca9430d 100644 --- a/go.sum +++ b/go.sum @@ -163,7 +163,7 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/internal/boost/boost_suite_test.go b/internal/boost/boost_suite_test.go index f63ddc2..f7b2683 100644 --- a/internal/boost/boost_suite_test.go +++ b/internal/boost/boost_suite_test.go @@ -33,19 +33,42 @@ func TestBoost(t *testing.T) { } var ( - podTemplate *corev1.Pod - annotTemplate *bpod.BoostPodAnnotation - specTemplate *autoscaling.StartupCPUBoost + podTemplate *corev1.Pod + annotTemplate *bpod.BoostPodAnnotation + specTemplate *autoscaling.StartupCPUBoost + containerOneName string + containerTwoName string + containerOnePercValue int64 + containerTwoPercValue int64 ) var _ = BeforeSuite(func() { + containerOneName = "container-one" + containerTwoName = "container-two" + containerOnePercValue = 120 + containerTwoPercValue = 100 specTemplate = &autoscaling.StartupCPUBoost{ ObjectMeta: metav1.ObjectMeta{ Name: "boost-001", Namespace: "demo", }, Spec: autoscaling.StartupCPUBoostSpec{ - BoostPercent: 55, + ResourcePolicy: autoscaling.ResourcePolicy{ + ContainerPolicies: []autoscaling.ContainerPolicy{ + { + ContainerName: containerOneName, + PercentageIncrease: autoscaling.PercentageIncrease{ + Value: containerOnePercValue, + }, + }, + { + ContainerName: containerTwoName, + PercentageIncrease: autoscaling.PercentageIncrease{ + Value: containerTwoPercValue, + }, + }, + }, + }, }, } annotTemplate = &bpod.BoostPodAnnotation{ diff --git a/internal/boost/policy/fixed.go b/internal/boost/duration/fixed.go similarity index 91% rename from internal/boost/policy/fixed.go rename to internal/boost/duration/fixed.go index fd31fb7..1060174 100644 --- a/internal/boost/policy/fixed.go +++ b/internal/boost/duration/fixed.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package policy +package duration import ( "time" @@ -31,11 +31,11 @@ type FixedDurationPolicy struct { duration time.Duration } -func NewFixedDurationPolicy(duration time.Duration) DurationPolicy { +func NewFixedDurationPolicy(duration time.Duration) Policy { return NewFixedDurationPolicyWithTimeFunc(time.Now, duration) } -func NewFixedDurationPolicyWithTimeFunc(timeFunc TimeFunc, duration time.Duration) DurationPolicy { +func NewFixedDurationPolicyWithTimeFunc(timeFunc TimeFunc, duration time.Duration) Policy { return &FixedDurationPolicy{ timeFunc: timeFunc, duration: duration, diff --git a/internal/boost/policy/fixed_test.go b/internal/boost/duration/fixed_test.go similarity index 75% rename from internal/boost/policy/fixed_test.go rename to internal/boost/duration/fixed_test.go index ec49f23..eba7c10 100644 --- a/internal/boost/policy/fixed_test.go +++ b/internal/boost/duration/fixed_test.go @@ -12,43 +12,43 @@ // See the License for the specific language governing permissions and // limitations under the License. -package policy_test +package duration_test import ( "time" - bpolicy "github.com/google/kube-startup-cpu-boost/internal/boost/policy" + "github.com/google/kube-startup-cpu-boost/internal/boost/duration" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var _ = Describe("FixedDurationPolicy", func() { - var policy bpolicy.DurationPolicy + var policy duration.Policy var now time.Time - var duration time.Duration - var timeFunc bpolicy.TimeFunc + var timeDuration time.Duration + var timeFunc duration.TimeFunc BeforeEach(func() { now = time.Now() - duration = 5 * time.Second + timeDuration = 5 * time.Second timeFunc = func() time.Time { return now } - policy = bpolicy.NewFixedDurationPolicyWithTimeFunc(timeFunc, duration) + policy = duration.NewFixedDurationPolicyWithTimeFunc(timeFunc, timeDuration) }) Describe("Validates POD", func() { When("the life time of a POD exceeds the policy duration", func() { It("returns policy is not valid", func() { - creationTimesamp := now.Add(-1 * duration).Add(-1 * time.Minute) + creationTimesamp := now.Add(-1 * timeDuration).Add(-1 * time.Minute) pod.CreationTimestamp = metav1.NewTime(creationTimesamp) Expect(policy.Valid(pod)).To(BeFalse()) }) }) When("the life time of a POD is within policy duration", func() { It("returns policy is valid", func() { - creationTimesamp := now.Add(-1 * duration).Add(1 * time.Minute) + creationTimesamp := now.Add(-1 * timeDuration).Add(1 * time.Minute) pod.CreationTimestamp = metav1.NewTime(creationTimesamp) Expect(policy.Valid(pod)).To(BeTrue()) }) diff --git a/internal/boost/policy/podcondition.go b/internal/boost/duration/podcondition.go similarity index 95% rename from internal/boost/policy/podcondition.go rename to internal/boost/duration/podcondition.go index a95730e..77fce35 100644 --- a/internal/boost/policy/podcondition.go +++ b/internal/boost/duration/podcondition.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package policy +package duration import ( corev1 "k8s.io/api/core/v1" @@ -27,7 +27,7 @@ type PodConditionPolicy struct { status corev1.ConditionStatus } -func NewPodConditionPolicy(condition corev1.PodConditionType, status corev1.ConditionStatus) DurationPolicy { +func NewPodConditionPolicy(condition corev1.PodConditionType, status corev1.ConditionStatus) Policy { return &PodConditionPolicy{ condition: condition, status: status, diff --git a/internal/boost/policy/podcondition_test.go b/internal/boost/duration/podcondition_test.go similarity index 90% rename from internal/boost/policy/podcondition_test.go rename to internal/boost/duration/podcondition_test.go index 02933bd..2d0de44 100644 --- a/internal/boost/policy/podcondition_test.go +++ b/internal/boost/duration/podcondition_test.go @@ -12,24 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -package policy_test +package duration_test import ( - bpolicy "github.com/google/kube-startup-cpu-boost/internal/boost/policy" + "github.com/google/kube-startup-cpu-boost/internal/boost/duration" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" ) var _ = Describe("PodConditionPolicy", func() { - var policy bpolicy.DurationPolicy + var policy duration.Policy var condition corev1.PodConditionType var status corev1.ConditionStatus BeforeEach(func() { condition = corev1.PodReady status = corev1.ConditionTrue - policy = bpolicy.NewPodConditionPolicy(condition, status) + policy = duration.NewPodConditionPolicy(condition, status) }) Describe("Validates POD", func() { diff --git a/internal/boost/policy/policy.go b/internal/boost/duration/policy.go similarity index 85% rename from internal/boost/policy/policy.go rename to internal/boost/duration/policy.go index e598391..30e2d72 100644 --- a/internal/boost/policy/policy.go +++ b/internal/boost/duration/policy.go @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package policy contains implementation of resource boost duration policies -package policy +// Package duration contains implementation of resource boost duration policies +package duration import corev1 "k8s.io/api/core/v1" @@ -22,7 +22,7 @@ const ( PolicyTypePodCondition = "PodCondition" ) -type DurationPolicy interface { +type Policy interface { Valid(pod *corev1.Pod) bool Name() string } diff --git a/internal/boost/policy/policy_suite_test.go b/internal/boost/duration/policy_suite_test.go similarity index 97% rename from internal/boost/policy/policy_suite_test.go rename to internal/boost/duration/policy_suite_test.go index b81e9d0..d7e95b0 100644 --- a/internal/boost/policy/policy_suite_test.go +++ b/internal/boost/duration/policy_suite_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package policy_test +package duration_test import ( "testing" diff --git a/internal/boost/manager.go b/internal/boost/manager.go index 06ab6ed..be9d4d6 100644 --- a/internal/boost/manager.go +++ b/internal/boost/manager.go @@ -24,7 +24,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/go-logr/logr" - "github.com/google/kube-startup-cpu-boost/internal/boost/policy" + "github.com/google/kube-startup-cpu-boost/internal/boost/duration" ctrl "sigs.k8s.io/controller-runtime" ) @@ -33,9 +33,7 @@ var ( ) const ( - DefaultManagerCheckInterval = time.Duration(5 * time.Second) - StartupCPUBoostPodLabelKey = "autoscaling.x-k8s.io/startup-cpu-boost" - StartupCPUBoostPodAnnotationKey = "autoscaling.x-k8s.io/startup-cpu-boost" + DefaultManagerCheckInterval = time.Duration(5 * time.Second) ) type Manager interface { @@ -168,7 +166,7 @@ func (m *managerImpl) addStartupCPUBoost(boost StartupCPUBoost) { m.startupCPUBoosts[boost.Namespace()] = boosts } boosts[boost.Name()] = boost - if _, ok := boost.DurationPolicies()[policy.FixedDurationPolicyName]; ok { + if _, ok := boost.DurationPolicies()[duration.FixedDurationPolicyName]; ok { key := boostKey{name: boost.Name(), namespace: boost.Namespace()} m.timePolicyBoosts[key] = boost } @@ -187,7 +185,7 @@ func (m *managerImpl) updateTimePolicyBoosts(ctx context.Context) { defer m.RUnlock() log := m.loggerFromContext(ctx) for _, boost := range m.timePolicyBoosts { - for _, pod := range boost.ValidatePolicy(ctx, policy.FixedDurationPolicyName) { + for _, pod := range boost.ValidatePolicy(ctx, duration.FixedDurationPolicyName) { log = log.WithValues("boost", boost.Name(), "namespace", boost.Namespace(), "pod", pod.Name) log.V(5).Info("updating pod with initial resources") if err := boost.RevertResources(ctx, pod); err != nil { diff --git a/internal/boost/resource/percentage.go b/internal/boost/resource/percentage.go new file mode 100644 index 0000000..a81f1d0 --- /dev/null +++ b/internal/boost/resource/percentage.go @@ -0,0 +1,59 @@ +// Copyright 2023 Google LLC +// +// 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 +// +// https://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 resource contains implementation of resource boost duration policies +package resource + +import ( + "gopkg.in/inf.v0" + corev1 "k8s.io/api/core/v1" + apiResource "k8s.io/apimachinery/pkg/api/resource" +) + +type PercentageContainerPolicy struct { + percentage int64 +} + +func NewPercentageContainerPolicy(percentage int64) ContainerPolicy { + return &PercentageContainerPolicy{ + percentage: percentage, + } +} + +func (p *PercentageContainerPolicy) Percentage() int64 { + return p.percentage +} + +func (p *PercentageContainerPolicy) NewResources(container *corev1.Container) *corev1.ResourceRequirements { + result := container.Resources.DeepCopy() + p.increaseResource(corev1.ResourceCPU, result.Requests) + p.increaseResource(corev1.ResourceCPU, result.Limits) + return result +} + +func (p *PercentageContainerPolicy) increaseResource(resource corev1.ResourceName, resources corev1.ResourceList) { + if quantity, ok := resources[resource]; ok { + resources[resource] = *increaseQuantity(quantity, p.percentage) + } +} + +func increaseQuantity(quantity apiResource.Quantity, incPerc int64) *apiResource.Quantity { + quantityDec := quantity.AsDec() + decPerc := inf.NewDec(100+incPerc, 2) + decResult := &inf.Dec{} + decResult.Mul(quantityDec, decPerc) + decRoundedResult := inf.Dec{} + decRoundedResult.Round(decResult, 2, inf.RoundCeil) + return apiResource.NewDecimalQuantity(decRoundedResult, quantity.Format) +} diff --git a/internal/boost/resource/percentage_test.go b/internal/boost/resource/percentage_test.go new file mode 100644 index 0000000..4deadac --- /dev/null +++ b/internal/boost/resource/percentage_test.go @@ -0,0 +1,73 @@ +// Copyright 2023 Google LLC +// +// 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 +// +// https://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 resource_test + +import ( + "github.com/google/kube-startup-cpu-boost/internal/boost/resource" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + apiResource "k8s.io/apimachinery/pkg/api/resource" +) + +var _ = Describe("PercentageResourcePolicy", func() { + var ( + container *corev1.Container + policy resource.ContainerPolicy + percentage int64 + newResources *corev1.ResourceRequirements + ) + + BeforeEach(func() { + percentage = 20 + }) + JustBeforeEach(func() { + policy = resource.NewPercentageContainerPolicy(percentage) + newResources = policy.NewResources(container) + }) + When("There are resources and limits defined", func() { + BeforeEach(func() { + container = containerTemplate.DeepCopy() + cpuReq, err := apiResource.ParseQuantity("1") + Expect(err).NotTo(HaveOccurred()) + cpuLim, err := apiResource.ParseQuantity("2") + Expect(err).NotTo(HaveOccurred()) + container.Resources.Requests[corev1.ResourceCPU] = cpuReq + container.Resources.Limits[corev1.ResourceCPU] = cpuLim + }) + It("returns resources with a valid CPU requests", func() { + Expect(newResources.Requests).To(HaveKey(corev1.ResourceCPU)) + qty := newResources.Requests[corev1.ResourceCPU] + Expect(qty.String()).To(Equal("1200m")) + }) + It("returns resources with a valid CPU limits", func() { + Expect(newResources.Limits).To(HaveKey(corev1.ResourceCPU)) + qty := newResources.Limits[corev1.ResourceCPU] + Expect(qty.String()).To(Equal("2400m")) + }) + }) + When("There are no resources and limits defined", func() { + BeforeEach(func() { + container = containerTemplate.DeepCopy() + container.Resources.Requests = nil + container.Resources.Limits = nil + }) + It("returns empty new resources", func() { + Expect(newResources.Requests).To(HaveLen(0)) + Expect(newResources.Limits).To(HaveLen(0)) + }) + }) +}) diff --git a/internal/boost/resource/resource.go b/internal/boost/resource/resource.go new file mode 100644 index 0000000..86ba42f --- /dev/null +++ b/internal/boost/resource/resource.go @@ -0,0 +1,22 @@ +// Copyright 2023 Google LLC +// +// 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 +// +// https://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 resource contains implementation of resource boost duration policies +package resource + +import corev1 "k8s.io/api/core/v1" + +type ContainerPolicy interface { + NewResources(container *corev1.Container) *corev1.ResourceRequirements +} diff --git a/internal/boost/resource/resource_suite_test.go b/internal/boost/resource/resource_suite_test.go new file mode 100644 index 0000000..f5ef731 --- /dev/null +++ b/internal/boost/resource/resource_suite_test.go @@ -0,0 +1,49 @@ +// Copyright 2023 Google LLC +// +// 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 +// +// https://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 resource_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + apiResource "k8s.io/apimachinery/pkg/api/resource" +) + +var containerTemplate *corev1.Container + +func TestResource(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Resource Suite") +} + +var _ = BeforeSuite(func() { + cpuRequestsQty, err := apiResource.ParseQuantity("500m") + Expect(err).NotTo(HaveOccurred()) + cpuLimitsQty, err := apiResource.ParseQuantity("1") + Expect(err).NotTo(HaveOccurred()) + containerTemplate = &corev1.Container{ + Name: "container-one", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: cpuRequestsQty, + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: cpuLimitsQty, + }, + }, + } +}) diff --git a/internal/boost/startupcpuboost.go b/internal/boost/startupcpuboost.go index 7879c00..0726d3d 100644 --- a/internal/boost/startupcpuboost.go +++ b/internal/boost/startupcpuboost.go @@ -22,8 +22,9 @@ import ( "github.com/go-logr/logr" autoscaling "github.com/google/kube-startup-cpu-boost/api/v1alpha1" + "github.com/google/kube-startup-cpu-boost/internal/boost/duration" bpod "github.com/google/kube-startup-cpu-boost/internal/boost/pod" - "github.com/google/kube-startup-cpu-boost/internal/boost/policy" + "github.com/google/kube-startup-cpu-boost/internal/boost/resource" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/labels" ctrl "sigs.k8s.io/controller-runtime" @@ -36,8 +37,8 @@ import ( type StartupCPUBoost interface { Name() string Namespace() string - BoostPercent() int64 - DurationPolicies() map[string]policy.DurationPolicy + ResourcePolicy(containerName string) (resource.ContainerPolicy, bool) + DurationPolicies() map[string]duration.Policy Pod(name string) (*corev1.Pod, bool) UpsertPod(ctx context.Context, pod *corev1.Pod) error DeletePod(ctx context.Context, pod *corev1.Pod) error @@ -49,13 +50,13 @@ type StartupCPUBoost interface { // StartupCPUBoostImpl is an implementation of a StartupCPUBoost CRD type StartupCPUBoostImpl struct { sync.RWMutex - name string - namespace string - percent int64 - selector labels.Selector - policies map[string]policy.DurationPolicy - pods sync.Map - client client.Client + name string + namespace string + selector labels.Selector + durationPolicies map[string]duration.Policy + resourcePolicies map[string]resource.ContainerPolicy + pods sync.Map + client client.Client } // NewStartupCPUBoost constructs startup-cpu-boost implementation from a given API spec @@ -65,12 +66,12 @@ func NewStartupCPUBoost(client client.Client, boost *autoscaling.StartupCPUBoost return nil, err } return &StartupCPUBoostImpl{ - name: boost.Name, - namespace: boost.Namespace, - selector: selector, - percent: boost.Spec.BoostPercent, - policies: policiesFromSpec(boost.Spec.DurationPolicy), - client: client, + name: boost.Name, + namespace: boost.Namespace, + selector: selector, + durationPolicies: mapDurationPolicy(boost.Spec.DurationPolicy), + resourcePolicies: mapResourcePolicy(boost.Spec.ResourcePolicy), + client: client, }, nil } @@ -84,14 +85,15 @@ func (b *StartupCPUBoostImpl) Namespace() string { return b.namespace } -// BoostPercent returns startup-cpu-boost boost percentage -func (b *StartupCPUBoostImpl) BoostPercent() int64 { - return b.percent +// ResourcePolicy returns the resource policy for a given container +func (b *StartupCPUBoostImpl) ResourcePolicy(containerName string) (resource.ContainerPolicy, bool) { + policy, ok := b.resourcePolicies[containerName] + return policy, ok } // DurationPolicies returns configured duration policies -func (b *StartupCPUBoostImpl) DurationPolicies() map[string]policy.DurationPolicy { - return b.policies +func (b *StartupCPUBoostImpl) DurationPolicies() map[string]duration.Policy { + return b.durationPolicies } // Pod returns a POD if tracked by startup-cpu-boost. @@ -112,7 +114,7 @@ func (b *StartupCPUBoostImpl) UpsertPod(ctx context.Context, pod *corev1.Pod) er return nil } log.V(5).Info("updating existing pod") - condPolicy, ok := b.policies[policy.PodConditionPolicyName] + condPolicy, ok := b.durationPolicies[duration.PodConditionPolicyName] if !ok { log.V(5).Info("skipping pod update as podCondition policy is missing") return nil @@ -140,7 +142,7 @@ func (b *StartupCPUBoostImpl) DeletePod(ctx context.Context, pod *corev1.Pod) er // The function returns slice of PODs that violated the policy. func (b *StartupCPUBoostImpl) ValidatePolicy(ctx context.Context, name string) (violated []*corev1.Pod) { violated = make([]*corev1.Pod, 0) - policy, ok := b.policies[name] + policy, ok := b.durationPolicies[name] if !ok { return } @@ -185,7 +187,7 @@ func (b *StartupCPUBoostImpl) loggerFromContext(ctx context.Context) logr.Logger // validatePolicyOnPod validates given policy on a given POD. // The function returns true if policy is valid or false otherwise -func (b *StartupCPUBoostImpl) validatePolicyOnPod(ctx context.Context, p policy.DurationPolicy, pod *corev1.Pod) (valid bool) { +func (b *StartupCPUBoostImpl) validatePolicyOnPod(ctx context.Context, p duration.Policy, pod *corev1.Pod) (valid bool) { log := b.loggerFromContext(ctx).WithValues("pod", pod.Name) if valid = p.Valid(pod); !valid { log.WithValues("policy", p.Name()).V(5).Info("policy is not valid") @@ -193,16 +195,26 @@ func (b *StartupCPUBoostImpl) validatePolicyOnPod(ctx context.Context, p policy. return } -// policiesFromSpec maps the Duration Policies from the API spec to the map holding policy -// implementations under policy name keys -func policiesFromSpec(policiesSpec autoscaling.DurationPolicy) map[string]policy.DurationPolicy { - policies := make(map[string]policy.DurationPolicy) +// mapDurationPolicy maps the Duration Policy from the API spec to the map of policy +// implementations with policy name keys +func mapDurationPolicy(policiesSpec autoscaling.DurationPolicy) map[string]duration.Policy { + policies := make(map[string]duration.Policy) if fixedPolicy := policiesSpec.Fixed; fixedPolicy != nil { - duration := fixedPolicyToDuration(*fixedPolicy) - policies[policy.FixedDurationPolicyName] = policy.NewFixedDurationPolicy(duration) + d := fixedPolicyToDuration(*fixedPolicy) + policies[duration.FixedDurationPolicyName] = duration.NewFixedDurationPolicy(d) } if condPolicy := policiesSpec.PodCondition; condPolicy != nil { - policies[policy.PodConditionPolicyName] = policy.NewPodConditionPolicy(condPolicy.Type, condPolicy.Status) + policies[duration.PodConditionPolicyName] = duration.NewPodConditionPolicy(condPolicy.Type, condPolicy.Status) + } + return policies +} + +// mapResourcePolicy maps the Resource Policy from the API spec to the map of policy +// implementations with container name keys +func mapResourcePolicy(spec autoscaling.ResourcePolicy) map[string]resource.ContainerPolicy { + policies := make(map[string]resource.ContainerPolicy) + for _, policySpec := range spec.ContainerPolicies { + policies[policySpec.ContainerName] = resource.NewPercentageContainerPolicy(policySpec.PercentageIncrease.Value) } return policies } diff --git a/internal/boost/startupcpuboost_test.go b/internal/boost/startupcpuboost_test.go index f3ef262..f98b3e0 100644 --- a/internal/boost/startupcpuboost_test.go +++ b/internal/boost/startupcpuboost_test.go @@ -20,7 +20,8 @@ import ( autoscaling "github.com/google/kube-startup-cpu-boost/api/v1alpha1" cpuboost "github.com/google/kube-startup-cpu-boost/internal/boost" - "github.com/google/kube-startup-cpu-boost/internal/boost/policy" + "github.com/google/kube-startup-cpu-boost/internal/boost/duration" + "github.com/google/kube-startup-cpu-boost/internal/boost/resource" "github.com/google/kube-startup-cpu-boost/internal/mock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -51,8 +52,19 @@ var _ = Describe("StartupCPUBoost", func() { It("returns valid namespace", func() { Expect(boost.Namespace()).To(Equal(spec.Namespace)) }) - It("returns valid boost percent", func() { - Expect(boost.BoostPercent()).To(Equal(spec.Spec.BoostPercent)) + It("returns valid resource policy for container one", func() { + p, ok := boost.ResourcePolicy(containerOneName) + Expect(ok).To(BeTrue()) + Expect(p).To(BeAssignableToTypeOf(&resource.PercentageContainerPolicy{})) + percPolicy, _ := p.(*resource.PercentageContainerPolicy) + Expect(percPolicy.Percentage()).To(Equal(containerOnePercValue)) + }) + It("returns valid resource policy for container two", func() { + p, ok := boost.ResourcePolicy(containerTwoName) + Expect(ok).To(BeTrue()) + Expect(p).To(BeAssignableToTypeOf(&resource.PercentageContainerPolicy{})) + percPolicy, _ := p.(*resource.PercentageContainerPolicy) + Expect(percPolicy.Percentage()).To(Equal(containerTwoPercValue)) }) When("the spec has fixed duration policy", func() { BeforeEach(func() { @@ -62,11 +74,11 @@ var _ = Describe("StartupCPUBoost", func() { } }) It("returns fixed duration policy implementation", func() { - Expect(boost.DurationPolicies()).To(HaveKey(policy.FixedDurationPolicyName)) + Expect(boost.DurationPolicies()).To(HaveKey(duration.FixedDurationPolicyName)) }) It("returned fixed duration policy implementation is valid", func() { - p := boost.DurationPolicies()[policy.FixedDurationPolicyName] - fixedP, ok := p.(*policy.FixedDurationPolicy) + p := boost.DurationPolicies()[duration.FixedDurationPolicyName] + fixedP, ok := p.(*duration.FixedDurationPolicy) Expect(ok).To(BeTrue()) expDuration := time.Duration(spec.Spec.DurationPolicy.Fixed.Value) * time.Second Expect(fixedP.Duration()).To(Equal(expDuration)) @@ -84,11 +96,11 @@ var _ = Describe("StartupCPUBoost", func() { } }) It("returns pod condition duration policy implementation", func() { - Expect(boost.DurationPolicies()).To(HaveKey(policy.PodConditionPolicyName)) + Expect(boost.DurationPolicies()).To(HaveKey(duration.PodConditionPolicyName)) }) It("returned pod condition duration policy implementation is valid", func() { - p := boost.DurationPolicies()[policy.PodConditionPolicyName] - podCondP, ok := p.(*policy.PodConditionPolicy) + p := boost.DurationPolicies()[duration.PodConditionPolicyName] + podCondP, ok := p.(*duration.PodConditionPolicy) Expect(ok).To(BeTrue()) Expect(podCondP.Condition()).To(Equal(spec.Spec.DurationPolicy.PodCondition.Type)) Expect(podCondP.Status()).To(Equal(spec.Spec.DurationPolicy.PodCondition.Status)) diff --git a/internal/controller/boost_pod_handler.go b/internal/controller/boost_pod_handler.go index 940c5d2..4b2ebd3 100644 --- a/internal/controller/boost_pod_handler.go +++ b/internal/controller/boost_pod_handler.go @@ -112,7 +112,7 @@ func (h *boostPodHandler) GetPodLabelSelector() *metav1.LabelSelector { return &metav1.LabelSelector{ MatchExpressions: []metav1.LabelSelectorRequirement{ { - Key: boost.StartupCPUBoostPodLabelKey, + Key: bpod.BoostLabelKey, Operator: metav1.LabelSelectorOpExists, Values: []string{}, }, diff --git a/internal/controller/boost_pod_handler_test.go b/internal/controller/boost_pod_handler_test.go index 0b77dc5..a4f283c 100644 --- a/internal/controller/boost_pod_handler_test.go +++ b/internal/controller/boost_pod_handler_test.go @@ -18,7 +18,7 @@ import ( "context" "github.com/go-logr/logr" - "github.com/google/kube-startup-cpu-boost/internal/boost" + "github.com/google/kube-startup-cpu-boost/internal/boost/pod" "github.com/google/kube-startup-cpu-boost/internal/controller" "github.com/google/kube-startup-cpu-boost/internal/mock" . "github.com/onsi/ginkgo/v2" @@ -193,10 +193,7 @@ var _ = Describe("BoostPodHandler", func() { m = &selector.MatchExpressions[0] }) It("has a valid key", func() { - Expect(m.Key).To(Equal(boost.StartupCPUBoostPodLabelKey)) - }) - It("has a valid operator", func() { - Expect(m.Key).To(Equal(boost.StartupCPUBoostPodLabelKey)) + Expect(m.Key).To(Equal(pod.BoostLabelKey)) }) It("has empty values list", func() { Expect(m.Values).To(HaveLen(0)) diff --git a/internal/controller/controller_suite_test.go b/internal/controller/controller_suite_test.go index fe8d9ba..efe2850 100644 --- a/internal/controller/controller_suite_test.go +++ b/internal/controller/controller_suite_test.go @@ -45,7 +45,16 @@ var _ = BeforeSuite(func() { Namespace: "demo", }, Spec: autoscaling.StartupCPUBoostSpec{ - BoostPercent: 55, + ResourcePolicy: autoscaling.ResourcePolicy{ + ContainerPolicies: []autoscaling.ContainerPolicy{ + { + ContainerName: "demo", + PercentageIncrease: autoscaling.PercentageIncrease{ + Value: 120, + }, + }, + }, + }, }, } annotTemplate = &bpod.BoostPodAnnotation{ diff --git a/internal/mock/startupcpuboost.go b/internal/mock/startupcpuboost.go index fa44a7b..e8820fc 100644 --- a/internal/mock/startupcpuboost.go +++ b/internal/mock/startupcpuboost.go @@ -11,6 +11,7 @@ // 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. +// // Code generated by MockGen. DO NOT EDIT. // Source: github.com/google/kube-startup-cpu-boost/internal/boost (interfaces: StartupCPUBoost) @@ -26,7 +27,8 @@ import ( context "context" reflect "reflect" - policy "github.com/google/kube-startup-cpu-boost/internal/boost/policy" + duration "github.com/google/kube-startup-cpu-boost/internal/boost/duration" + resource "github.com/google/kube-startup-cpu-boost/internal/boost/resource" gomock "go.uber.org/mock/gomock" v1 "k8s.io/api/core/v1" ) @@ -54,20 +56,6 @@ func (m *MockStartupCPUBoost) EXPECT() *MockStartupCPUBoostMockRecorder { return m.recorder } -// BoostPercent mocks base method. -func (m *MockStartupCPUBoost) BoostPercent() int64 { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "BoostPercent") - ret0, _ := ret[0].(int64) - return ret0 -} - -// BoostPercent indicates an expected call of BoostPercent. -func (mr *MockStartupCPUBoostMockRecorder) BoostPercent() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BoostPercent", reflect.TypeOf((*MockStartupCPUBoost)(nil).BoostPercent)) -} - // DeletePod mocks base method. func (m *MockStartupCPUBoost) DeletePod(arg0 context.Context, arg1 *v1.Pod) error { m.ctrl.T.Helper() @@ -83,10 +71,10 @@ func (mr *MockStartupCPUBoostMockRecorder) DeletePod(arg0, arg1 any) *gomock.Cal } // DurationPolicies mocks base method. -func (m *MockStartupCPUBoost) DurationPolicies() map[string]policy.DurationPolicy { +func (m *MockStartupCPUBoost) DurationPolicies() map[string]duration.Policy { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DurationPolicies") - ret0, _ := ret[0].(map[string]policy.DurationPolicy) + ret0, _ := ret[0].(map[string]duration.Policy) return ret0 } @@ -153,6 +141,21 @@ func (mr *MockStartupCPUBoostMockRecorder) Pod(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pod", reflect.TypeOf((*MockStartupCPUBoost)(nil).Pod), arg0) } +// ResourcePolicy mocks base method. +func (m *MockStartupCPUBoost) ResourcePolicy(arg0 string) (resource.ContainerPolicy, bool) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ResourcePolicy", arg0) + ret0, _ := ret[0].(resource.ContainerPolicy) + ret1, _ := ret[1].(bool) + return ret0, ret1 +} + +// ResourcePolicy indicates an expected call of ResourcePolicy. +func (mr *MockStartupCPUBoostMockRecorder) ResourcePolicy(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResourcePolicy", reflect.TypeOf((*MockStartupCPUBoost)(nil).ResourcePolicy), arg0) +} + // RevertResources mocks base method. func (m *MockStartupCPUBoost) RevertResources(arg0 context.Context, arg1 *v1.Pod) error { m.ctrl.T.Helper() diff --git a/internal/webhook/doc.go b/internal/webhook/doc.go new file mode 100644 index 0000000..35c5185 --- /dev/null +++ b/internal/webhook/doc.go @@ -0,0 +1,17 @@ +// Copyright 2023 Google LLC +// +// 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 +// +// https://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 webhook contains validating and mutating webhooks +// that are used by the Startup CPU Boost +package webhook diff --git a/internal/webhook/podcpuboost_webhook.go b/internal/webhook/podcpuboost_webhook.go index 38e00a7..3aabfc2 100644 --- a/internal/webhook/podcpuboost_webhook.go +++ b/internal/webhook/podcpuboost_webhook.go @@ -22,9 +22,7 @@ import ( "github.com/go-logr/logr" "github.com/google/kube-startup-cpu-boost/internal/boost" bpod "github.com/google/kube-startup-cpu-boost/internal/boost/pod" - inf "gopkg.in/inf.v0" corev1 "k8s.io/api/core/v1" - apiResource "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/webhook" @@ -53,78 +51,60 @@ func (h *podCPUBoostHandler) Handle(ctx context.Context, req admission.Request) if err != nil { return admission.Errored(http.StatusBadRequest, err) } - log := ctrl.LoggerFrom(ctx).WithName("cpuboost-webhook").WithValues("pod.Name", pod.Name, "pod.Namespace", pod.Namespace) - log.V(5).Info("Handling Pod") + log := ctrl.LoggerFrom(ctx).WithName("cpuboost-webhook") + log.V(5).Info("handling Pod") boostImpl, ok := h.manager.StartupCPUBoostForPod(ctx, pod) if !ok { - log.V(5).Info("StartupCPUBoost was not found") + log.V(5).Info("no startupCPUBoost matched") return admission.Allowed("no StartupCPUBoost matched") } - containers, ok := h.boostContainersCPU(pod, boostImpl.BoostPercent(), log) - if !ok { - log.V(5).Info("no suitable CPU requests were found") - return admission.Allowed("no CPU request found") - } - pod.Spec.Containers = containers - pod.ObjectMeta.Labels[boost.StartupCPUBoostPodLabelKey] = boostImpl.Name() + log = log.WithValues("boost", boostImpl.Name()) + h.boostContainerResources(boostImpl, pod, log) marshaledPod, err := json.Marshal(pod) if err != nil { return admission.Errored(http.StatusInternalServerError, err) } - return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod) } -/* -func (h *podCPUBoostHandler) InjectDecoder(d *admission.Decoder) error { - h.decoder = d - return nil -} -*/ - -func (h *podCPUBoostHandler) boostContainersCPU(pod *corev1.Pod, boostPerc int64, log logr.Logger) (result []corev1.Container, boosted bool) { - result = pod.Spec.Containers - boostAnnot := bpod.NewBoostAnnotation() - for _, container := range pod.Spec.Containers { - log = log.WithValues("container.Name", container.Name) - if boostedReq, initReq, _ := increaseQuantityForResource(container.Resources.Requests, corev1.ResourceCPU, boostPerc, log.WithValues("resourceRequirement", "request")); boostedReq { - boosted = true - boostAnnot.InitCPURequests[container.Name] = initReq.String() - } - if boostedLimit, initLimit, _ := increaseQuantityForResource(container.Resources.Limits, corev1.ResourceCPU, boostPerc, log.WithValues("resourceRequirement", "limit")); boostedLimit { - boostAnnot.InitCPULimits[container.Name] = initLimit.String() +func (h *podCPUBoostHandler) boostContainerResources(b boost.StartupCPUBoost, pod *corev1.Pod, log logr.Logger) { + annotation := bpod.NewBoostAnnotation() + for i, container := range pod.Spec.Containers { + policy, found := b.ResourcePolicy(container.Name) + if !found { + continue } + log = log.WithValues("container", container.Name, + "CPURequests", container.Resources.Requests.Cpu().String(), + "CPULimits", container.Resources.Limits.Cpu().String(), + ) + updateBoostAnnotation(annotation, container.Name, container.Resources) + resources := policy.NewResources(&container) + log = log.WithValues( + "newCPURequests", resources.Requests.Cpu().String(), + "newCPULimits", resources.Limits.Cpu().String(), + ) + log.Info("increasing resources") + pod.Spec.Containers[i].Resources = *resources } - if boosted { + if len(annotation.InitCPULimits) > 0 || len(annotation.InitCPURequests) > 0 { if pod.Annotations == nil { pod.Annotations = make(map[string]string) } - pod.Annotations[boost.StartupCPUBoostPodAnnotationKey] = boostAnnot.ToJSON() + pod.Annotations[bpod.BoostAnnotationKey] = annotation.ToJSON() + if pod.Labels == nil { + pod.Labels = make(map[string]string) + } + pod.Labels[bpod.BoostLabelKey] = b.Name() } - return } -func increaseQuantityForResource(resources corev1.ResourceList, resName corev1.ResourceName, incPerc int64, log logr.Logger) (increased bool, init, new *apiResource.Quantity) { - if quantity, ok := resources[resName]; ok { - newQuantity := increaseQuantity(quantity, incPerc) - log = log.WithValues(resName.String(), quantity.String(), "incPercent", incPerc, - resName.String()+"New", newQuantity.String()) - log.V(2).Info("increasing container resource quantity") - resources[corev1.ResourceCPU] = *newQuantity - init = &quantity - new = newQuantity - increased = true +func updateBoostAnnotation(annot *bpod.BoostPodAnnotation, containerName string, resources corev1.ResourceRequirements) { + if cpuRequests, ok := resources.Requests[corev1.ResourceCPU]; ok { + annot.InitCPURequests[containerName] = cpuRequests.String() + } + if cpuLimits, ok := resources.Limits[corev1.ResourceCPU]; ok { + annot.InitCPULimits[containerName] = cpuLimits.String() } - return -} - -func increaseQuantity(quantity apiResource.Quantity, incPerc int64) *apiResource.Quantity { - quantityDec := quantity.AsDec() - decPerc := inf.NewDec(100+incPerc, 2) - decResult := &inf.Dec{} - decResult.Mul(quantityDec, decPerc) - decRoundedResult := inf.Dec{} - decRoundedResult.Round(decResult, 2, inf.RoundCeil) - return apiResource.NewDecimalQuantity(decRoundedResult, quantity.Format) } diff --git a/internal/webhook/podcpuboost_webhook_test.go b/internal/webhook/podcpuboost_webhook_test.go index a0c754a..808b00b 100644 --- a/internal/webhook/podcpuboost_webhook_test.go +++ b/internal/webhook/podcpuboost_webhook_test.go @@ -12,158 +12,265 @@ // See the License for the specific language governing permissions and // limitations under the License. -package webhook +package webhook_test import ( + "context" "encoding/json" - "testing" + "errors" + "fmt" + "strconv" - "github.com/google/kube-startup-cpu-boost/internal/boost" bpod "github.com/google/kube-startup-cpu-boost/internal/boost/pod" + "github.com/google/kube-startup-cpu-boost/internal/boost/resource" + "github.com/google/kube-startup-cpu-boost/internal/mock" + bwebhook "github.com/google/kube-startup-cpu-boost/internal/webhook" . "github.com/onsi/ginkgo/v2" - + . "github.com/onsi/gomega" + "go.uber.org/mock/gomock" + "gomodules.xyz/jsonpatch/v2" + admissionv1 "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" apiResource "k8s.io/apimachinery/pkg/api/resource" - "sigs.k8s.io/controller-runtime/pkg/log/zap" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) -func TestBoostContainersCPU(t *testing.T) { - reqQuantityStr := "1" - reqQuantity, _ := apiResource.ParseQuantity(reqQuantityStr) - limitQuantityStr := "2" - limitQuantity, _ := apiResource.ParseQuantity(limitQuantityStr) - pod := &corev1.Pod{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "container-one", - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceCPU: reqQuantity, - }, - Limits: corev1.ResourceList{ - corev1.ResourceCPU: limitQuantity, - }, - }, - }, - { - Name: "container-two", - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceCPU: reqQuantity, - }, - Limits: corev1.ResourceList{ - corev1.ResourceCPU: limitQuantity, - }, +var _ = Describe("Pod CPU Boost Webhook", func() { + Describe("Handles admission requests", func() { + var ( + mockCtrl *gomock.Controller + manager *mock.MockManager + managerCall *gomock.Call + pod *corev1.Pod + response webhook.AdmissionResponse + ) + BeforeEach(func() { + pod = podTemplate.DeepCopy() + mockCtrl = gomock.NewController(GinkgoT()) + manager = mock.NewMockManager(mockCtrl) + managerCall = manager.EXPECT().StartupCPUBoostForPod( + gomock.Any(), + gomock.Cond(func(x any) bool { + p, ok := x.(*corev1.Pod) + if !ok { + return false + } + return p.Name == pod.Name && p.Namespace == pod.Namespace + }), + ) + }) + JustBeforeEach(func() { + podJSON, err := json.Marshal(pod) + Expect(err).NotTo(HaveOccurred()) + admissionReq := admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Object: runtime.RawExtension{ + Raw: podJSON, }, }, - }, - }, - } - var boostPerc int64 = 20 - expReqQuantityStr := "1200m" - expLimitQuantityStr := "2400m" - log := zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)) + } + hook := bwebhook.NewPodCPUBoostWebHook(manager, scheme.Scheme) + response = hook.Handle(context.TODO(), admissionReq) + }) + When("there is no matching Startup CPU Boost", func() { + BeforeEach(func() { + managerCall.Return(nil, false) + }) + It("calls the Startup CPU Boost manager", func() { + managerCall.Times(1) + }) + It("allows the admission", func() { + Expect(response.Allowed).To(BeTrue()) + }) + It("returns zero patches", func() { + Expect(response.Patches).To(HaveLen(0)) + }) + }) + When("there is a matching Startup CPU Boost", func() { + When("there is no policy for any container", func() { + var ( + boost *mock.MockStartupCPUBoost + resPolicyCallOne *gomock.Call + resPolicyCallTwo *gomock.Call + ) + BeforeEach(func() { + boost = mock.NewMockStartupCPUBoost(mockCtrl) + boost.EXPECT().Name().AnyTimes().Return("boost-one") + resPolicyCallOne = boost.EXPECT().ResourcePolicy(gomock.Eq(containerOneName)).Return(nil, false) + resPolicyCallTwo = boost.EXPECT().ResourcePolicy(gomock.Eq(containerTwoName)).Return(nil, false) + managerCall.Return(boost, true) + }) + It("retrieves resource policy for containers", func() { + resPolicyCallOne.Times(1) + resPolicyCallTwo.Times(1) + }) + It("allows the admission", func() { + Expect(response.Allowed).To(BeTrue()) + }) + It("returns zero patches", func() { + Expect(response.Patches).To(HaveLen(0)) + }) + }) + When("there is a policy for one container", func() { + var ( + boostName string + boost *mock.MockStartupCPUBoost + resPolicy resource.ContainerPolicy + resPolicyCallOne *gomock.Call + resPolicyCallTwo *gomock.Call + ) + BeforeEach(func() { + boost = mock.NewMockStartupCPUBoost(mockCtrl) + boostName = "boost-one" + boost.EXPECT().Name().AnyTimes().Return(boostName) + resPolicy = resource.NewPercentageContainerPolicy(120) + resPolicyCallOne = boost.EXPECT().ResourcePolicy(gomock.Eq(containerOneName)).Return(resPolicy, true) + resPolicyCallTwo = boost.EXPECT().ResourcePolicy(gomock.Eq(containerTwoName)).Return(nil, false) + managerCall.Return(boost, true) + }) + It("retrieves resource policy for containers", func() { + resPolicyCallOne.Times(1) + resPolicyCallTwo.Times(1) + }) + It("allows the admission", func() { + Expect(response.Allowed).To(BeTrue()) + }) + It("returns admission with four patches", func() { + Expect(response.Patches).To(HaveLen(4)) + }) + It("returns admission with boost label patch", func() { + Expect(response.Patches).To(ContainElement(boostLabelPatch(boostName))) + }) + It("returns admission with boost annotation patch", func() { + annotPatch, found := boostAnnotationPatch(response.Patches) + Expect(found).To(BeTrue()) + annot, err := boostAnnotationFromPatch(annotPatch) + Expect(err).NotTo(HaveOccurred()) + Expect(annot.InitCPURequests).To(HaveKeyWithValue( + containerOneName, + pod.Spec.Containers[0].Resources.Requests.Cpu().String(), + )) + Expect(annot.InitCPULimits).To(HaveKeyWithValue( + containerOneName, + pod.Spec.Containers[0].Resources.Limits.Cpu().String(), + )) + }) + It("returns admission with container-one requests patch", func() { + patch := containerResourcePatch(pod, resPolicy, "requests", 0) + Expect(response.Patches).To(ContainElement(patch)) + }) + It("returns admission with container-one limits patch", func() { + patch := containerResourcePatch(pod, resPolicy, "limits", 0) + Expect(response.Patches).To(ContainElement(patch)) + }) + When("container has no request and no limits set", func() { + BeforeEach(func() { + pod.Spec.Containers[0].Resources.Requests = nil + pod.Spec.Containers[0].Resources.Limits = nil + }) + It("allows the admission", func() { + Expect(response.Allowed).To(BeTrue()) + }) + It("returns admission with zero patches", func() { + Expect(response.Patches).To(HaveLen(0)) + }) + }) + When("container has only requests set", func() { + BeforeEach(func() { + pod.Spec.Containers[0].Resources.Limits = nil + }) + It("allows the admission", func() { + Expect(response.Allowed).To(BeTrue()) + }) + It("returns admission with three patches", func() { + Expect(response.Patches).To(HaveLen(3)) + }) + }) + }) + When("there is a policy for two containers", func() { + var ( + resPolicyCallOne *gomock.Call + resPolicyCallTwo *gomock.Call + ) + BeforeEach(func() { + boost := mock.NewMockStartupCPUBoost(mockCtrl) + boost.EXPECT().Name().AnyTimes().Return("boost-one") + resPolicy := resource.NewPercentageContainerPolicy(120) + resPolicyCallOne = boost.EXPECT().ResourcePolicy(gomock.Eq(containerOneName)).Return(resPolicy, true) + resPolicyCallTwo = boost.EXPECT().ResourcePolicy(gomock.Eq(containerTwoName)).Return(resPolicy, true) + managerCall.Return(boost, true) + }) + It("retrieves resource policy for containers", func() { + resPolicyCallOne.Times(1) + resPolicyCallTwo.Times(1) + }) + It("allows the admission", func() { + Expect(response.Allowed).To(BeTrue()) + }) + It("returns admission with six patches", func() { + Expect(response.Patches).To(HaveLen(6)) + }) + }) + }) + }) +}) - handler := &podCPUBoostHandler{} - result, boosted := handler.boostContainersCPU(pod, boostPerc, log) - if !boosted { - t.Fatalf("boosted = %v; want %v", boosted, true) - } - if len(result) != len(pod.Spec.Containers) { - t.Errorf("len(result) = %v; want %v", len(result), len(pod.Spec.Containers)) - } - for i := range result { - cpuReq := result[i].Resources.Requests[corev1.ResourceCPU] - cpuLimit := result[i].Resources.Limits[corev1.ResourceCPU] - if cpuReq.String() != expReqQuantityStr { - t.Errorf("container %d: cpu requests = %v; want %v", i, cpuReq.String(), expReqQuantityStr) - } - if cpuLimit.String() != expLimitQuantityStr { - t.Errorf("container %d: cpu limits = %v; want %v", i, cpuLimit.String(), expLimitQuantityStr) - } - } - annotStr, ok := pod.Annotations[boost.StartupCPUBoostPodAnnotationKey] +func boostAnnotationFromPatch(patch jsonpatch.Operation) (*bpod.BoostPodAnnotation, error) { + valueMap, ok := patch.Value.(map[string]interface{}) if !ok { - t.Fatalf("POD is missing startup CPU boost annotation") + return nil, errors.New("patch value is not map[string]interface{}") } - annot := &bpod.BoostPodAnnotation{} - if err := json.Unmarshal([]byte(annotStr), annot); err != nil { - t.Fatalf("can't unmarshal boost annotation due to %s", err) + annotValue, ok := valueMap[bpod.BoostAnnotationKey] + if !ok { + return nil, errors.New("patch value map has no boost annotation key") } - if len(annot.InitCPURequests) != len(pod.Spec.Containers) { - t.Fatalf("CPU boost annotation: len(initCPURequests) = %v; want %v", len(annot.InitCPURequests), len(pod.Spec.Containers)) + annotStr, err := strconv.Unquote(fmt.Sprintf("`%s`", annotValue)) + if err != nil { + return nil, errors.New("cannot unquote boost annotation JSON") } - if len(annot.InitCPULimits) != len(pod.Spec.Containers) { - t.Fatalf("CPU boost annotation: len(initCPULimits) = %v; want %v", len(annot.InitCPULimits), len(pod.Spec.Containers)) + var annot bpod.BoostPodAnnotation + if err := json.Unmarshal([]byte(annotStr), &annot); err != nil { + return nil, err } - for _, container := range pod.Spec.Containers { - initReq := annot.InitCPURequests[container.Name] - initLimit := annot.InitCPULimits[container.Name] - if initReq != reqQuantityStr { - t.Errorf("CPU boost annotation: InitCPURequests[%v] = %v; want %v", container.Name, initReq, reqQuantityStr) - } - if initLimit != limitQuantityStr { - t.Errorf("CPU boost annotation: InitCPULimits[%v] = %v; want %v", container.Name, initLimit, limitQuantityStr) + return &annot, nil +} + +func boostAnnotationPatch(patches []jsonpatch.Operation) (jsonpatch.Operation, bool) { + for _, patch := range patches { + if patch.Path == "/metadata/annotations" && patch.Operation == "add" { + return patch, true } } + return jsonpatch.Operation{}, false } -func TestIncreaseQuantityForResource(t *testing.T) { - quantityStr := "250m" - boostPerc := 120 - expectedQuantityStr := "550m" - quantity, _ := apiResource.ParseQuantity(quantityStr) - log := zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)) - requests := corev1.ResourceList{ - corev1.ResourceCPU: quantity, - } - increased, init, new := increaseQuantityForResource(requests, corev1.ResourceCPU, int64(boostPerc), log) - result := requests[corev1.ResourceCPU] - if !increased { - t.Errorf("increased = %v; want %v", increased, true) - } - if init.String() != quantityStr { - t.Errorf("initial quantity = %v; want %v", init.String(), quantityStr) - } - if new.String() != expectedQuantityStr { - t.Errorf("new quantity = %v; want %v", new.String(), expectedQuantityStr) - } - if result.String() != expectedQuantityStr { - t.Errorf("quantity = %v; want %v", result, expectedQuantityStr) +func boostLabelPatch(boostName string) jsonpatch.Operation { + return jsonpatch.Operation{ + Operation: "add", + Path: "/metadata/labels", + Value: map[string]interface{}{ + bpod.BoostLabelKey: boostName, + }, } } -func TestIncreaseQuantity(t *testing.T) { - type input struct { - quantityStr string - boostPerc int64 - } - inputs := []input{ - {"100m", 20}, - {"1.3", 50}, - {"800m", 100}, - {"4", 80}, - {"101m", 325}, - {"1", 20}, - } - expected := []string{ - "120m", - "1950m", - "1600m", - "7200m", - "430m", - "1200m", +func containerResourcePatch(pod *corev1.Pod, policy resource.ContainerPolicy, requirement string, containerIdx int) jsonpatch.Operation { + path := fmt.Sprintf("/spec/containers/%d/resources/%s/cpu", containerIdx, requirement) + var newQuantity apiResource.Quantity + switch requirement { + case "requests": + newQuantity = policy.NewResources(&pod.Spec.Containers[containerIdx]).Requests[corev1.ResourceCPU] + case "limits": + newQuantity = policy.NewResources(&pod.Spec.Containers[containerIdx]).Limits[corev1.ResourceCPU] + default: + panic("unsupported resource requirement") } - - for i := range inputs { - quantity, err := apiResource.ParseQuantity(inputs[i].quantityStr) - if err != nil { - t.Fatalf("could not parse quantity due to %s", err) - } - result := increaseQuantity(quantity, inputs[i].boostPerc) - if result.String() != expected[i] { - t.Errorf("input %d, result = %v; want %v", i, result, expected[i]) - } + return jsonpatch.Operation{ + Operation: "replace", + Path: path, + Value: newQuantity.String(), } } diff --git a/internal/webhook/podcpuboost_webhook_test.go.old b/internal/webhook/podcpuboost_webhook_test.go.old new file mode 100644 index 0000000..a0c754a --- /dev/null +++ b/internal/webhook/podcpuboost_webhook_test.go.old @@ -0,0 +1,169 @@ +// Copyright 2023 Google LLC +// +// 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 +// +// https://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 webhook + +import ( + "encoding/json" + "testing" + + "github.com/google/kube-startup-cpu-boost/internal/boost" + bpod "github.com/google/kube-startup-cpu-boost/internal/boost/pod" + . "github.com/onsi/ginkgo/v2" + + corev1 "k8s.io/api/core/v1" + apiResource "k8s.io/apimachinery/pkg/api/resource" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +func TestBoostContainersCPU(t *testing.T) { + reqQuantityStr := "1" + reqQuantity, _ := apiResource.ParseQuantity(reqQuantityStr) + limitQuantityStr := "2" + limitQuantity, _ := apiResource.ParseQuantity(limitQuantityStr) + pod := &corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "container-one", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: reqQuantity, + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: limitQuantity, + }, + }, + }, + { + Name: "container-two", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: reqQuantity, + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: limitQuantity, + }, + }, + }, + }, + }, + } + var boostPerc int64 = 20 + expReqQuantityStr := "1200m" + expLimitQuantityStr := "2400m" + log := zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)) + + handler := &podCPUBoostHandler{} + result, boosted := handler.boostContainersCPU(pod, boostPerc, log) + if !boosted { + t.Fatalf("boosted = %v; want %v", boosted, true) + } + if len(result) != len(pod.Spec.Containers) { + t.Errorf("len(result) = %v; want %v", len(result), len(pod.Spec.Containers)) + } + for i := range result { + cpuReq := result[i].Resources.Requests[corev1.ResourceCPU] + cpuLimit := result[i].Resources.Limits[corev1.ResourceCPU] + if cpuReq.String() != expReqQuantityStr { + t.Errorf("container %d: cpu requests = %v; want %v", i, cpuReq.String(), expReqQuantityStr) + } + if cpuLimit.String() != expLimitQuantityStr { + t.Errorf("container %d: cpu limits = %v; want %v", i, cpuLimit.String(), expLimitQuantityStr) + } + } + annotStr, ok := pod.Annotations[boost.StartupCPUBoostPodAnnotationKey] + if !ok { + t.Fatalf("POD is missing startup CPU boost annotation") + } + annot := &bpod.BoostPodAnnotation{} + if err := json.Unmarshal([]byte(annotStr), annot); err != nil { + t.Fatalf("can't unmarshal boost annotation due to %s", err) + } + if len(annot.InitCPURequests) != len(pod.Spec.Containers) { + t.Fatalf("CPU boost annotation: len(initCPURequests) = %v; want %v", len(annot.InitCPURequests), len(pod.Spec.Containers)) + } + if len(annot.InitCPULimits) != len(pod.Spec.Containers) { + t.Fatalf("CPU boost annotation: len(initCPULimits) = %v; want %v", len(annot.InitCPULimits), len(pod.Spec.Containers)) + } + for _, container := range pod.Spec.Containers { + initReq := annot.InitCPURequests[container.Name] + initLimit := annot.InitCPULimits[container.Name] + if initReq != reqQuantityStr { + t.Errorf("CPU boost annotation: InitCPURequests[%v] = %v; want %v", container.Name, initReq, reqQuantityStr) + } + if initLimit != limitQuantityStr { + t.Errorf("CPU boost annotation: InitCPULimits[%v] = %v; want %v", container.Name, initLimit, limitQuantityStr) + } + } +} + +func TestIncreaseQuantityForResource(t *testing.T) { + quantityStr := "250m" + boostPerc := 120 + expectedQuantityStr := "550m" + quantity, _ := apiResource.ParseQuantity(quantityStr) + log := zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)) + requests := corev1.ResourceList{ + corev1.ResourceCPU: quantity, + } + increased, init, new := increaseQuantityForResource(requests, corev1.ResourceCPU, int64(boostPerc), log) + result := requests[corev1.ResourceCPU] + if !increased { + t.Errorf("increased = %v; want %v", increased, true) + } + if init.String() != quantityStr { + t.Errorf("initial quantity = %v; want %v", init.String(), quantityStr) + } + if new.String() != expectedQuantityStr { + t.Errorf("new quantity = %v; want %v", new.String(), expectedQuantityStr) + } + if result.String() != expectedQuantityStr { + t.Errorf("quantity = %v; want %v", result, expectedQuantityStr) + } +} + +func TestIncreaseQuantity(t *testing.T) { + type input struct { + quantityStr string + boostPerc int64 + } + inputs := []input{ + {"100m", 20}, + {"1.3", 50}, + {"800m", 100}, + {"4", 80}, + {"101m", 325}, + {"1", 20}, + } + expected := []string{ + "120m", + "1950m", + "1600m", + "7200m", + "430m", + "1200m", + } + + for i := range inputs { + quantity, err := apiResource.ParseQuantity(inputs[i].quantityStr) + if err != nil { + t.Fatalf("could not parse quantity due to %s", err) + } + result := increaseQuantity(quantity, inputs[i].boostPerc) + if result.String() != expected[i] { + t.Errorf("input %d, result = %v; want %v", i, result, expected[i]) + } + } +} diff --git a/internal/webhook/webhook_suite_test.go b/internal/webhook/webhook_suite_test.go new file mode 100644 index 0000000..09d2b80 --- /dev/null +++ b/internal/webhook/webhook_suite_test.go @@ -0,0 +1,90 @@ +// Copyright 2023 Google LLC +// +// 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 +// +// https://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 webhook_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + apiResource "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestWebhook(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Webhook Suite") +} + +var ( + podTemplate *corev1.Pod + containerOneName string + containerTwoName string + containerOneCPUReq string + containerOneCPULimit string + containerTwoCPUReq string + containerTwoCPULimit string +) + +var _ = BeforeSuite(func() { + containerOneName = "container-one" + containerOneCPUReq = "500m" + containerOneCPULimit = "1000m" + containerTwoName = "container-two" + containerTwoCPUReq = "1" + containerTwoCPULimit = "2" + + containerOneCPUReqObj, err := apiResource.ParseQuantity(containerOneCPUReq) + Expect(err).NotTo(HaveOccurred()) + containerOneCPULimitObj, err := apiResource.ParseQuantity(containerOneCPULimit) + Expect(err).NotTo(HaveOccurred()) + containerTwoCPUReqObj, err := apiResource.ParseQuantity(containerTwoCPUReq) + Expect(err).NotTo(HaveOccurred()) + containerTwoCPULimitObj, err := apiResource.ParseQuantity(containerTwoCPULimit) + Expect(err).NotTo(HaveOccurred()) + podTemplate = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "demo-1235", + Namespace: "demo", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: containerOneName, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: containerOneCPUReqObj, + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: containerOneCPULimitObj, + }, + }, + }, + { + Name: containerTwoName, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: containerTwoCPUReqObj, + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: containerTwoCPULimitObj, + }, + }, + }, + }, + }, + } +})