diff --git a/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/features/inplacepodresize/nonsidecar_initcontainers_resize.go b/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/features/inplacepodresize/nonsidecar_initcontainers_resize.go new file mode 100644 index 00000000000..1bfbd074288 --- /dev/null +++ b/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/features/inplacepodresize/nonsidecar_initcontainers_resize.go @@ -0,0 +1,72 @@ +/* +Copyright 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 inplacepodresize + +import ( + "reflect" + + core "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/version" + "k8s.io/component-helpers/nodedeclaredfeatures/types" +) + +// Ensure the feature struct implements the unified Feature interface. +var _ types.Feature = &podLevelResourcesResizeFeature{} + +// IPPRInitContainersFeatureGate is the feature gate for IPPRPodLevelResourcesVerticalScaling. +const IPPRInitContainersFeatureGate = "InPlacePodVerticalScalingInitContainers" + +// Feature is the implementation of the `NonSidecarInitContainerResize` feature. +var NonSidecarInitContainerResizeFeature = &nonSidecarInitContainerResizeFeature{} + +type nonSidecarInitContainerResizeFeature struct{} + +func (f *nonSidecarInitContainerResizeFeature) Name() string { + return IPPRInitContainersFeatureGate +} + +func (f *nonSidecarInitContainerResizeFeature) Discover(cfg *types.NodeConfiguration) bool { + return cfg.FeatureGates.Enabled(IPPRInitContainersFeatureGate) +} + +func (f *nonSidecarInitContainerResizeFeature) Requirements() *types.FeatureRequirements { + return &types.FeatureRequirements{ + EnabledFeatureGates: []string{IPPRInitContainersFeatureGate}, + } +} + +func (f *nonSidecarInitContainerResizeFeature) InferForScheduling(podInfo *types.PodInfo) bool { + // This feature is only relevant for pod updates. + return false +} + +func (f *nonSidecarInitContainerResizeFeature) InferForUpdate(oldPodInfo, newPodInfo *types.PodInfo) bool { + for i, ctr := range oldPodInfo.Spec.InitContainers { + if !isSidecar(ctr) && !reflect.DeepEqual(ctr.Resources, newPodInfo.Spec.InitContainers[i].Resources) { + return true + } + } + return false +} + +func (f *nonSidecarInitContainerResizeFeature) MaxVersion() *version.Version { + return nil +} + +func isSidecar(initContainer core.Container) bool { + return initContainer.RestartPolicy != nil && *initContainer.RestartPolicy == core.ContainerRestartPolicyAlways +} diff --git a/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/features/inplacepodresize/nonsidecar_initcontainers_resize_test.go b/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/features/inplacepodresize/nonsidecar_initcontainers_resize_test.go new file mode 100644 index 00000000000..c540f5d71c9 --- /dev/null +++ b/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/features/inplacepodresize/nonsidecar_initcontainers_resize_test.go @@ -0,0 +1,248 @@ +/* +Copyright 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 inplacepodresize + +import ( + "testing" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/component-helpers/nodedeclaredfeatures/types" +) + +func TestInitContainerResizeFeatureDiscover(t *testing.T) { + tests := []struct { + name string + featureGate bool + expected bool + }{ + { + name: "FeatureGateEnabled", + featureGate: true, + expected: true, + }, + { + name: "FeatureGateDisabled", + featureGate: false, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := &types.NodeConfiguration{FeatureGates: types.FeatureGateMap{IPPRInitContainersFeatureGate: tt.featureGate}} + enabled := NonSidecarInitContainerResizeFeature.Discover(cfg) + if want, got := tt.expected, enabled; want != got { + t.Fatalf("want=%v,got=%v", want, got) + } + }) + } +} + +func TestNonSidecarInitContainerResizeFeatureInferForScheduling(t *testing.T) { + f := &nonSidecarInitContainerResizeFeature{} + podInfo := &types.PodInfo{Spec: &v1.PodSpec{}} + if f.InferForScheduling(podInfo) { + t.Fatalf("InferForScheduling should always be false") + } +} + +func TestNonSidecarInitContainerResizeFeatureInferForUpdate(t *testing.T) { + restartAlways := v1.ContainerRestartPolicyAlways + + tests := []struct { + name string + oldPodInfo *types.PodInfo + newPodInfo *types.PodInfo + expected bool + }{ + { + name: "NoChanges", + oldPodInfo: &types.PodInfo{ + Spec: &v1.PodSpec{ + InitContainers: []v1.Container{ + { + Name: "init-1", + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("100m")}, + }, + }, + }, + }, + }, + newPodInfo: &types.PodInfo{ + Spec: &v1.PodSpec{ + InitContainers: []v1.Container{ + { + Name: "init-1", + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("100m")}, + }, + }, + }, + }, + }, + expected: false, + }, + { + name: "InitContainerResourceChanged", + oldPodInfo: &types.PodInfo{ + Spec: &v1.PodSpec{ + InitContainers: []v1.Container{ + { + Name: "init-1", + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("100m")}, + }, + }, + }, + }, + }, + newPodInfo: &types.PodInfo{ + Spec: &v1.PodSpec{ + InitContainers: []v1.Container{ + { + Name: "init-1", + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("200m")}, + }, + }, + }, + }, + }, + expected: true, + }, + { + name: "SidecarContainerResourceChanged_Ignored", + oldPodInfo: &types.PodInfo{ + Spec: &v1.PodSpec{ + InitContainers: []v1.Container{ + { + Name: "sidecar", + RestartPolicy: &restartAlways, + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("100m")}, + }, + }, + }, + }, + }, + newPodInfo: &types.PodInfo{ + Spec: &v1.PodSpec{ + InitContainers: []v1.Container{ + { + Name: "sidecar", + RestartPolicy: &restartAlways, + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("200m")}, + }, + }, + }, + }, + }, + expected: false, + }, + { + name: "MixedContainers_OnlyInitChanged", + oldPodInfo: &types.PodInfo{ + Spec: &v1.PodSpec{ + InitContainers: []v1.Container{ + { + Name: "sidecar", + RestartPolicy: &restartAlways, + }, + { + Name: "regular-init", + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceMemory: resource.MustParse("128Mi")}, + }, + }, + }, + }, + }, + newPodInfo: &types.PodInfo{ + Spec: &v1.PodSpec{ + InitContainers: []v1.Container{ + { + Name: "sidecar", + RestartPolicy: &restartAlways, + }, + { + Name: "regular-init", + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceMemory: resource.MustParse("256Mi")}, + }, + }, + }, + }, + }, + expected: true, + }, + { + name: "MixedContainers_OnlySidecarChanged_Ignored", + oldPodInfo: &types.PodInfo{ + Spec: &v1.PodSpec{ + InitContainers: []v1.Container{ + { + Name: "sidecar", + RestartPolicy: &restartAlways, + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("100m")}, + }, + }, + { + Name: "regular-init", + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("100m")}, + }, + }, + }, + }, + }, + newPodInfo: &types.PodInfo{ + Spec: &v1.PodSpec{ + InitContainers: []v1.Container{ + { + Name: "sidecar", + RestartPolicy: &restartAlways, + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("200m")}, + }, + }, + { + Name: "regular-init", + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("100m")}, + }, + }, + }, + }, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := &nonSidecarInitContainerResizeFeature{} + got := f.InferForUpdate(tt.oldPodInfo, tt.newPodInfo) + if got != tt.expected { + t.Errorf("InferForUpdate() = %v, want %v", got, tt.expected) + } + }) + } +} diff --git a/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/features/registry.go b/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/features/registry.go index 57256b53dec..82546f198e2 100644 --- a/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/features/registry.go +++ b/staging/src/k8s.io/component-helpers/nodedeclaredfeatures/features/registry.go @@ -30,4 +30,5 @@ var AllFeatures = []types.Feature{ restartallcontainers.Feature, inplacepodresize.PodLevelResourcesResizeFeature, extendwebsocketstokubelet.Feature, + inplacepodresize.NonSidecarInitContainerResizeFeature, }