/* Copyright 2017 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 pod import ( "fmt" "reflect" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "k8s.io/component-base/featuregate" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/version" utilfeature "k8s.io/apiserver/pkg/util/feature" featuregatetesting "k8s.io/component-base/featuregate/testing" api "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/features" "k8s.io/utils/ptr" ) func TestVisitContainers(t *testing.T) { setAllFeatureEnabledContainersDuringTest := ContainerType(0) testCases := []struct { desc string spec *api.PodSpec wantContainers []string mask ContainerType }{ { desc: "empty podspec", spec: &api.PodSpec{}, wantContainers: []string{}, mask: AllContainers, }, { desc: "regular containers", spec: &api.PodSpec{ Containers: []api.Container{ {Name: "c1"}, {Name: "c2"}, }, InitContainers: []api.Container{ {Name: "i1"}, {Name: "i2"}, }, EphemeralContainers: []api.EphemeralContainer{ {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e1"}}, {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e2"}}, }, }, wantContainers: []string{"c1", "c2"}, mask: Containers, }, { desc: "init containers", spec: &api.PodSpec{ Containers: []api.Container{ {Name: "c1"}, {Name: "c2"}, }, InitContainers: []api.Container{ {Name: "i1"}, {Name: "i2"}, }, EphemeralContainers: []api.EphemeralContainer{ {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e1"}}, {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e2"}}, }, }, wantContainers: []string{"i1", "i2"}, mask: InitContainers, }, { desc: "ephemeral containers", spec: &api.PodSpec{ Containers: []api.Container{ {Name: "c1"}, {Name: "c2"}, }, InitContainers: []api.Container{ {Name: "i1"}, {Name: "i2"}, }, EphemeralContainers: []api.EphemeralContainer{ {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e1"}}, {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e2"}}, }, }, wantContainers: []string{"e1", "e2"}, mask: EphemeralContainers, }, { desc: "all container types", spec: &api.PodSpec{ Containers: []api.Container{ {Name: "c1"}, {Name: "c2"}, }, InitContainers: []api.Container{ {Name: "i1"}, {Name: "i2"}, }, EphemeralContainers: []api.EphemeralContainer{ {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e1"}}, {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e2"}}, }, }, wantContainers: []string{"i1", "i2", "c1", "c2", "e1", "e2"}, mask: AllContainers, }, { desc: "all feature enabled container types with ephemeral containers enabled", spec: &api.PodSpec{ Containers: []api.Container{ {Name: "c1"}, {Name: "c2", SecurityContext: &api.SecurityContext{}}, }, InitContainers: []api.Container{ {Name: "i1"}, {Name: "i2", SecurityContext: &api.SecurityContext{}}, }, EphemeralContainers: []api.EphemeralContainer{ {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e1"}}, {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e2"}}, }, }, wantContainers: []string{"i1", "i2", "c1", "c2", "e1", "e2"}, mask: setAllFeatureEnabledContainersDuringTest, }, { desc: "dropping fields", spec: &api.PodSpec{ Containers: []api.Container{ {Name: "c1"}, {Name: "c2", SecurityContext: &api.SecurityContext{}}, }, InitContainers: []api.Container{ {Name: "i1"}, {Name: "i2", SecurityContext: &api.SecurityContext{}}, }, EphemeralContainers: []api.EphemeralContainer{ {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e1"}}, {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e2", SecurityContext: &api.SecurityContext{}}}, }, }, wantContainers: []string{"i1", "i2", "c1", "c2", "e1", "e2"}, mask: AllContainers, }, } for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { if tc.mask == setAllFeatureEnabledContainersDuringTest { tc.mask = AllFeatureEnabledContainers() } gotContainers := []string{} VisitContainers(tc.spec, tc.mask, func(c *api.Container, containerType ContainerType) bool { gotContainers = append(gotContainers, c.Name) if c.SecurityContext != nil { c.SecurityContext = nil } return true }) if !cmp.Equal(gotContainers, tc.wantContainers) { t.Errorf("VisitContainers() = %+v, want %+v", gotContainers, tc.wantContainers) } for _, c := range tc.spec.Containers { if c.SecurityContext != nil { t.Errorf("VisitContainers() did not drop SecurityContext for container %q", c.Name) } } for _, c := range tc.spec.InitContainers { if c.SecurityContext != nil { t.Errorf("VisitContainers() did not drop SecurityContext for init container %q", c.Name) } } for _, c := range tc.spec.EphemeralContainers { if c.SecurityContext != nil { t.Errorf("VisitContainers() did not drop SecurityContext for ephemeral container %q", c.Name) } } }) } } func TestContainerIter(t *testing.T) { testCases := []struct { desc string spec *api.PodSpec wantContainers []string mask ContainerType }{ { desc: "empty podspec", spec: &api.PodSpec{}, wantContainers: []string{}, mask: AllContainers, }, { desc: "regular containers", spec: &api.PodSpec{ Containers: []api.Container{ {Name: "c1"}, {Name: "c2"}, }, InitContainers: []api.Container{ {Name: "i1"}, {Name: "i2"}, }, EphemeralContainers: []api.EphemeralContainer{ {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e1"}}, {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e2"}}, }, }, wantContainers: []string{"c1", "c2"}, mask: Containers, }, { desc: "init containers", spec: &api.PodSpec{ Containers: []api.Container{ {Name: "c1"}, {Name: "c2"}, }, InitContainers: []api.Container{ {Name: "i1"}, {Name: "i2"}, }, EphemeralContainers: []api.EphemeralContainer{ {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e1"}}, {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e2"}}, }, }, wantContainers: []string{"i1", "i2"}, mask: InitContainers, }, { desc: "init + main containers", spec: &api.PodSpec{ Containers: []api.Container{ {Name: "c1"}, {Name: "c2"}, }, InitContainers: []api.Container{ {Name: "i1"}, {Name: "i2"}, }, EphemeralContainers: []api.EphemeralContainer{ {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e1"}}, {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e2"}}, }, }, wantContainers: []string{"i1", "i2", "c1", "c2"}, mask: InitContainers | Containers, }, { desc: "ephemeral containers", spec: &api.PodSpec{ Containers: []api.Container{ {Name: "c1"}, {Name: "c2"}, }, InitContainers: []api.Container{ {Name: "i1"}, {Name: "i2"}, }, EphemeralContainers: []api.EphemeralContainer{ {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e1"}}, {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e2"}}, }, }, wantContainers: []string{"e1", "e2"}, mask: EphemeralContainers, }, { desc: "all container types", spec: &api.PodSpec{ Containers: []api.Container{ {Name: "c1"}, {Name: "c2"}, }, InitContainers: []api.Container{ {Name: "i1"}, {Name: "i2"}, }, EphemeralContainers: []api.EphemeralContainer{ {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e1"}}, {EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "e2"}}, }, }, wantContainers: []string{"i1", "i2", "c1", "c2", "e1", "e2"}, mask: AllContainers, }, } for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { gotContainers := []string{} for c, containerType := range ContainerIter(tc.spec, tc.mask) { gotContainers = append(gotContainers, c.Name) switch containerType { case InitContainers: if c.Name[0] != 'i' { t.Errorf("ContainerIter() yielded container type InitContainers for container %q", c.Name) } case Containers: if c.Name[0] != 'c' { t.Errorf("ContainerIter() yielded container type Containers for container %q", c.Name) } case EphemeralContainers: if c.Name[0] != 'e' { t.Errorf("ContainerIter() yielded container type EphemeralContainers for container %q", c.Name) } default: t.Errorf("ContainerIter() yielded unknown container type %d", containerType) } } if !cmp.Equal(gotContainers, tc.wantContainers) { t.Errorf("ContainerIter() = %+v, want %+v", gotContainers, tc.wantContainers) } }) } } func TestPodSecrets(t *testing.T) { // Stub containing all possible secret references in a pod. // The names of the referenced secrets match struct paths detected by reflection. pod := &api.Pod{ Spec: api.PodSpec{ Containers: []api.Container{{ EnvFrom: []api.EnvFromSource{{ SecretRef: &api.SecretEnvSource{ LocalObjectReference: api.LocalObjectReference{ Name: "Spec.Containers[*].EnvFrom[*].SecretRef"}}}}, Env: []api.EnvVar{{ ValueFrom: &api.EnvVarSource{ SecretKeyRef: &api.SecretKeySelector{ LocalObjectReference: api.LocalObjectReference{ Name: "Spec.Containers[*].Env[*].ValueFrom.SecretKeyRef"}}}}}}}, ImagePullSecrets: []api.LocalObjectReference{{ Name: "Spec.ImagePullSecrets"}}, InitContainers: []api.Container{{ EnvFrom: []api.EnvFromSource{{ SecretRef: &api.SecretEnvSource{ LocalObjectReference: api.LocalObjectReference{ Name: "Spec.InitContainers[*].EnvFrom[*].SecretRef"}}}}, Env: []api.EnvVar{{ ValueFrom: &api.EnvVarSource{ SecretKeyRef: &api.SecretKeySelector{ LocalObjectReference: api.LocalObjectReference{ Name: "Spec.InitContainers[*].Env[*].ValueFrom.SecretKeyRef"}}}}}}}, Volumes: []api.Volume{{ VolumeSource: api.VolumeSource{ AzureFile: &api.AzureFileVolumeSource{ SecretName: "Spec.Volumes[*].VolumeSource.AzureFile.SecretName"}}}, { VolumeSource: api.VolumeSource{ CephFS: &api.CephFSVolumeSource{ SecretRef: &api.LocalObjectReference{ Name: "Spec.Volumes[*].VolumeSource.CephFS.SecretRef"}}}}, { VolumeSource: api.VolumeSource{ Cinder: &api.CinderVolumeSource{ SecretRef: &api.LocalObjectReference{ Name: "Spec.Volumes[*].VolumeSource.Cinder.SecretRef"}}}}, { VolumeSource: api.VolumeSource{ FlexVolume: &api.FlexVolumeSource{ SecretRef: &api.LocalObjectReference{ Name: "Spec.Volumes[*].VolumeSource.FlexVolume.SecretRef"}}}}, { VolumeSource: api.VolumeSource{ Projected: &api.ProjectedVolumeSource{ Sources: []api.VolumeProjection{{ Secret: &api.SecretProjection{ LocalObjectReference: api.LocalObjectReference{ Name: "Spec.Volumes[*].VolumeSource.Projected.Sources[*].Secret"}}}}}}}, { VolumeSource: api.VolumeSource{ RBD: &api.RBDVolumeSource{ SecretRef: &api.LocalObjectReference{ Name: "Spec.Volumes[*].VolumeSource.RBD.SecretRef"}}}}, { VolumeSource: api.VolumeSource{ Secret: &api.SecretVolumeSource{ SecretName: "Spec.Volumes[*].VolumeSource.Secret.SecretName"}}}, { VolumeSource: api.VolumeSource{ Secret: &api.SecretVolumeSource{ SecretName: "Spec.Volumes[*].VolumeSource.Secret"}}}, { VolumeSource: api.VolumeSource{ ScaleIO: &api.ScaleIOVolumeSource{ SecretRef: &api.LocalObjectReference{ Name: "Spec.Volumes[*].VolumeSource.ScaleIO.SecretRef"}}}}, { VolumeSource: api.VolumeSource{ ISCSI: &api.ISCSIVolumeSource{ SecretRef: &api.LocalObjectReference{ Name: "Spec.Volumes[*].VolumeSource.ISCSI.SecretRef"}}}}, { VolumeSource: api.VolumeSource{ StorageOS: &api.StorageOSVolumeSource{ SecretRef: &api.LocalObjectReference{ Name: "Spec.Volumes[*].VolumeSource.StorageOS.SecretRef"}}}}, { VolumeSource: api.VolumeSource{ CSI: &api.CSIVolumeSource{ NodePublishSecretRef: &api.LocalObjectReference{ Name: "Spec.Volumes[*].VolumeSource.CSI.NodePublishSecretRef"}}}}}, EphemeralContainers: []api.EphemeralContainer{{ EphemeralContainerCommon: api.EphemeralContainerCommon{ EnvFrom: []api.EnvFromSource{{ SecretRef: &api.SecretEnvSource{ LocalObjectReference: api.LocalObjectReference{ Name: "Spec.EphemeralContainers[*].EphemeralContainerCommon.EnvFrom[*].SecretRef"}}}}, Env: []api.EnvVar{{ ValueFrom: &api.EnvVarSource{ SecretKeyRef: &api.SecretKeySelector{ LocalObjectReference: api.LocalObjectReference{ Name: "Spec.EphemeralContainers[*].EphemeralContainerCommon.Env[*].ValueFrom.SecretKeyRef"}}}}}}}}, }, } extractedNames := sets.New[string]() VisitPodSecretNames(pod, func(name string) bool { extractedNames.Insert(name) return true }, AllContainers) // excludedSecretPaths holds struct paths to fields with "secret" in the name that are not actually references to secret API objects excludedSecretPaths := sets.New[string]( "Spec.Volumes[*].VolumeSource.CephFS.SecretFile", ) // expectedSecretPaths holds struct paths to fields with "secret" in the name that are references to secret API objects. // every path here should be represented as an example in the Pod stub above, with the secret name set to the path. expectedSecretPaths := sets.New[string]( "Spec.Containers[*].EnvFrom[*].SecretRef", "Spec.Containers[*].Env[*].ValueFrom.SecretKeyRef", "Spec.EphemeralContainers[*].EphemeralContainerCommon.EnvFrom[*].SecretRef", "Spec.EphemeralContainers[*].EphemeralContainerCommon.Env[*].ValueFrom.SecretKeyRef", "Spec.ImagePullSecrets", "Spec.InitContainers[*].EnvFrom[*].SecretRef", "Spec.InitContainers[*].Env[*].ValueFrom.SecretKeyRef", "Spec.Volumes[*].VolumeSource.AzureFile.SecretName", "Spec.Volumes[*].VolumeSource.CephFS.SecretRef", "Spec.Volumes[*].VolumeSource.Cinder.SecretRef", "Spec.Volumes[*].VolumeSource.FlexVolume.SecretRef", "Spec.Volumes[*].VolumeSource.Projected.Sources[*].Secret", "Spec.Volumes[*].VolumeSource.RBD.SecretRef", "Spec.Volumes[*].VolumeSource.Secret", "Spec.Volumes[*].VolumeSource.Secret.SecretName", "Spec.Volumes[*].VolumeSource.ScaleIO.SecretRef", "Spec.Volumes[*].VolumeSource.ISCSI.SecretRef", "Spec.Volumes[*].VolumeSource.StorageOS.SecretRef", "Spec.Volumes[*].VolumeSource.CSI.NodePublishSecretRef", ) secretPaths := collectResourcePaths(t, "secret", nil, "", reflect.TypeOf(&api.Pod{})) secretPaths = secretPaths.Difference(excludedSecretPaths) if missingPaths := expectedSecretPaths.Difference(secretPaths); len(missingPaths) > 0 { t.Logf("Missing expected secret paths:\n%s", strings.Join(sets.List[string](missingPaths), "\n")) t.Error("Missing expected secret paths. Verify VisitPodSecretNames() is correctly finding the missing paths, then correct expectedSecretPaths") } if extraPaths := secretPaths.Difference(expectedSecretPaths); len(extraPaths) > 0 { t.Logf("Extra secret paths:\n%s", strings.Join(sets.List[string](extraPaths), "\n")) t.Error("Extra fields with 'secret' in the name found. Verify VisitPodSecretNames() is including these fields if appropriate, then correct expectedSecretPaths") } if missingNames := expectedSecretPaths.Difference(extractedNames); len(missingNames) > 0 { t.Logf("Missing expected secret names:\n%s", strings.Join(sets.List[string](missingNames), "\n")) t.Error("Missing expected secret names. Verify the pod stub above includes these references, then verify VisitPodSecretNames() is correctly finding the missing names") } if extraNames := extractedNames.Difference(expectedSecretPaths); len(extraNames) > 0 { t.Logf("Extra secret names:\n%s", strings.Join(sets.List[string](extraNames), "\n")) t.Error("Extra secret names extracted. Verify VisitPodSecretNames() is correctly extracting secret names") } // emptyPod is a stub containing empty object names emptyPod := &api.Pod{ Spec: api.PodSpec{ Containers: []api.Container{{ EnvFrom: []api.EnvFromSource{{ SecretRef: &api.SecretEnvSource{ LocalObjectReference: api.LocalObjectReference{ Name: ""}}}}}}, }, } VisitPodSecretNames(emptyPod, func(name string) bool { t.Fatalf("expected no empty names collected, got %q", name) return false }, AllContainers) } // collectResourcePaths traverses the object, computing all the struct paths that lead to fields with resourcename in the name. func collectResourcePaths(t *testing.T, resourcename string, path *field.Path, name string, tp reflect.Type) sets.Set[string] { resourcename = strings.ToLower(resourcename) resourcePaths := sets.New[string]() if tp.Kind() == reflect.Pointer { resourcePaths.Insert(sets.List[string](collectResourcePaths(t, resourcename, path, name, tp.Elem()))...) return resourcePaths } if strings.Contains(strings.ToLower(name), resourcename) { resourcePaths.Insert(path.String()) } switch tp.Kind() { case reflect.Pointer: resourcePaths.Insert(sets.List[string](collectResourcePaths(t, resourcename, path, name, tp.Elem()))...) case reflect.Struct: // ObjectMeta is generic and therefore should never have a field with a specific resource's name; // it contains cycles so it's easiest to just skip it. if name == "ObjectMeta" { break } for i := 0; i < tp.NumField(); i++ { field := tp.Field(i) resourcePaths.Insert(sets.List[string](collectResourcePaths(t, resourcename, path.Child(field.Name), field.Name, field.Type))...) } case reflect.Interface: t.Errorf("cannot find %s fields in interface{} field %s", resourcename, path.String()) case reflect.Map: resourcePaths.Insert(sets.List[string](collectResourcePaths(t, resourcename, path.Key("*"), "", tp.Elem()))...) case reflect.Slice: resourcePaths.Insert(sets.List[string](collectResourcePaths(t, resourcename, path.Key("*"), "", tp.Elem()))...) default: // all primitive types } return resourcePaths } func TestPodConfigmaps(t *testing.T) { // Stub containing all possible ConfigMap references in a pod. // The names of the referenced ConfigMaps match struct paths detected by reflection. pod := &api.Pod{ Spec: api.PodSpec{ Containers: []api.Container{{ EnvFrom: []api.EnvFromSource{{ ConfigMapRef: &api.ConfigMapEnvSource{ LocalObjectReference: api.LocalObjectReference{ Name: "Spec.Containers[*].EnvFrom[*].ConfigMapRef"}}}}, Env: []api.EnvVar{{ ValueFrom: &api.EnvVarSource{ ConfigMapKeyRef: &api.ConfigMapKeySelector{ LocalObjectReference: api.LocalObjectReference{ Name: "Spec.Containers[*].Env[*].ValueFrom.ConfigMapKeyRef"}}}}}}}, EphemeralContainers: []api.EphemeralContainer{{ EphemeralContainerCommon: api.EphemeralContainerCommon{ EnvFrom: []api.EnvFromSource{{ ConfigMapRef: &api.ConfigMapEnvSource{ LocalObjectReference: api.LocalObjectReference{ Name: "Spec.EphemeralContainers[*].EphemeralContainerCommon.EnvFrom[*].ConfigMapRef"}}}}, Env: []api.EnvVar{{ ValueFrom: &api.EnvVarSource{ ConfigMapKeyRef: &api.ConfigMapKeySelector{ LocalObjectReference: api.LocalObjectReference{ Name: "Spec.EphemeralContainers[*].EphemeralContainerCommon.Env[*].ValueFrom.ConfigMapKeyRef"}}}}}}}}, InitContainers: []api.Container{{ EnvFrom: []api.EnvFromSource{{ ConfigMapRef: &api.ConfigMapEnvSource{ LocalObjectReference: api.LocalObjectReference{ Name: "Spec.InitContainers[*].EnvFrom[*].ConfigMapRef"}}}}, Env: []api.EnvVar{{ ValueFrom: &api.EnvVarSource{ ConfigMapKeyRef: &api.ConfigMapKeySelector{ LocalObjectReference: api.LocalObjectReference{ Name: "Spec.InitContainers[*].Env[*].ValueFrom.ConfigMapKeyRef"}}}}}}}, Volumes: []api.Volume{{ VolumeSource: api.VolumeSource{ Projected: &api.ProjectedVolumeSource{ Sources: []api.VolumeProjection{{ ConfigMap: &api.ConfigMapProjection{ LocalObjectReference: api.LocalObjectReference{ Name: "Spec.Volumes[*].VolumeSource.Projected.Sources[*].ConfigMap"}}}}}}}, { VolumeSource: api.VolumeSource{ ConfigMap: &api.ConfigMapVolumeSource{ LocalObjectReference: api.LocalObjectReference{ Name: "Spec.Volumes[*].VolumeSource.ConfigMap"}}}}}, }, } extractedNames := sets.New[string]() VisitPodConfigmapNames(pod, func(name string) bool { extractedNames.Insert(name) return true }, AllContainers) // expectedPaths holds struct paths to fields with "ConfigMap" in the name that are references to ConfigMap API objects. // every path here should be represented as an example in the Pod stub above, with the ConfigMap name set to the path. expectedPaths := sets.New[string]( "Spec.Containers[*].EnvFrom[*].ConfigMapRef", "Spec.Containers[*].Env[*].ValueFrom.ConfigMapKeyRef", "Spec.EphemeralContainers[*].EphemeralContainerCommon.EnvFrom[*].ConfigMapRef", "Spec.EphemeralContainers[*].EphemeralContainerCommon.Env[*].ValueFrom.ConfigMapKeyRef", "Spec.InitContainers[*].EnvFrom[*].ConfigMapRef", "Spec.InitContainers[*].Env[*].ValueFrom.ConfigMapKeyRef", "Spec.Volumes[*].VolumeSource.Projected.Sources[*].ConfigMap", "Spec.Volumes[*].VolumeSource.ConfigMap", ) collectPaths := collectResourcePaths(t, "ConfigMap", nil, "", reflect.TypeOf(&api.Pod{})) if missingPaths := expectedPaths.Difference(collectPaths); len(missingPaths) > 0 { t.Logf("Missing expected paths:\n%s", strings.Join(sets.List[string](missingPaths), "\n")) t.Error("Missing expected paths. Verify VisitPodConfigmapNames() is correctly finding the missing paths, then correct expectedPaths") } if extraPaths := collectPaths.Difference(expectedPaths); len(extraPaths) > 0 { t.Logf("Extra paths:\n%s", strings.Join(sets.List[string](extraPaths), "\n")) t.Error("Extra fields with resource in the name found. Verify VisitPodConfigmapNames() is including these fields if appropriate, then correct expectedPaths") } if missingNames := expectedPaths.Difference(extractedNames); len(missingNames) > 0 { t.Logf("Missing expected names:\n%s", strings.Join(sets.List[string](missingNames), "\n")) t.Error("Missing expected names. Verify the pod stub above includes these references, then verify VisitPodConfigmapNames() is correctly finding the missing names") } if extraNames := extractedNames.Difference(expectedPaths); len(extraNames) > 0 { t.Logf("Extra names:\n%s", strings.Join(sets.List[string](extraNames), "\n")) t.Error("Extra names extracted. Verify VisitPodConfigmapNames() is correctly extracting resource names") } // emptyPod is a stub containing empty object names emptyPod := &api.Pod{ Spec: api.PodSpec{ Containers: []api.Container{{ EnvFrom: []api.EnvFromSource{{ ConfigMapRef: &api.ConfigMapEnvSource{ LocalObjectReference: api.LocalObjectReference{ Name: ""}}}}}}, }, } VisitPodConfigmapNames(emptyPod, func(name string) bool { t.Fatalf("expected no empty names collected, got %q", name) return false }, AllContainers) } func TestDropFSGroupFields(t *testing.T) { nofsGroupPod := func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{ Containers: []api.Container{ { Name: "container1", Image: "testimage", }, }, }, } } var podFSGroup int64 = 100 changePolicy := api.FSGroupChangeAlways fsGroupPod := func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{ Containers: []api.Container{ { Name: "container1", Image: "testimage", }, }, SecurityContext: &api.PodSecurityContext{ FSGroup: &podFSGroup, FSGroupChangePolicy: &changePolicy, }, }, } } podInfos := []struct { description string newPodHasFSGroupChangePolicy bool pod func() *api.Pod expectPolicyInPod bool }{ { description: "oldPod.FSGroupChangePolicy=nil, feature=true, newPod.FSGroupChangePolicy=true", pod: nofsGroupPod, newPodHasFSGroupChangePolicy: true, expectPolicyInPod: true, }, { description: "oldPod=nil, feature=true, newPod.FSGroupChangePolicy=true", pod: func() *api.Pod { return nil }, newPodHasFSGroupChangePolicy: true, expectPolicyInPod: true, }, { description: "oldPod.FSGroupChangePolicy=true, feature=true, newPod.FSGroupChangePolicy=false", pod: fsGroupPod, newPodHasFSGroupChangePolicy: false, expectPolicyInPod: false, }, } for _, podInfo := range podInfos { t.Run(podInfo.description, func(t *testing.T) { oldPod := podInfo.pod() newPod := oldPod.DeepCopy() if oldPod == nil && podInfo.newPodHasFSGroupChangePolicy { newPod = fsGroupPod() } if oldPod != nil { if podInfo.newPodHasFSGroupChangePolicy { newPod.Spec.SecurityContext = &api.PodSecurityContext{ FSGroup: &podFSGroup, FSGroupChangePolicy: &changePolicy, } } else { newPod.Spec.SecurityContext = &api.PodSecurityContext{} } } DropDisabledPodFields(newPod, oldPod) if podInfo.expectPolicyInPod { secContext := newPod.Spec.SecurityContext if secContext == nil || secContext.FSGroupChangePolicy == nil { t.Errorf("for %s, expected fsGroupChangepolicy found none", podInfo.description) } } else { secContext := newPod.Spec.SecurityContext if secContext != nil && secContext.FSGroupChangePolicy != nil { t.Errorf("for %s, unexpected fsGroupChangepolicy set", podInfo.description) } } }) } } func TestDropProcMount(t *testing.T) { procMount := api.UnmaskedProcMount defaultProcMount := api.DefaultProcMount podWithProcMount := func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyNever, Containers: []api.Container{{Name: "container1", Image: "testimage", SecurityContext: &api.SecurityContext{ProcMount: &procMount}}}, InitContainers: []api.Container{{Name: "container1", Image: "testimage", SecurityContext: &api.SecurityContext{ProcMount: &procMount}}}, }, } } podWithDefaultProcMount := func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyNever, Containers: []api.Container{{Name: "container1", Image: "testimage", SecurityContext: &api.SecurityContext{ProcMount: &defaultProcMount}}}, InitContainers: []api.Container{{Name: "container1", Image: "testimage", SecurityContext: &api.SecurityContext{ProcMount: &defaultProcMount}}}, }, } } podWithoutProcMount := func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyNever, Containers: []api.Container{{Name: "container1", Image: "testimage", SecurityContext: &api.SecurityContext{ProcMount: nil}}}, InitContainers: []api.Container{{Name: "container1", Image: "testimage", SecurityContext: &api.SecurityContext{ProcMount: nil}}}, }, } } podInfo := []struct { description string hasProcMount bool pod func() *api.Pod }{ { description: "has ProcMount", hasProcMount: true, pod: podWithProcMount, }, { description: "has default ProcMount", hasProcMount: false, pod: podWithDefaultProcMount, }, { description: "does not have ProcMount", hasProcMount: false, pod: podWithoutProcMount, }, { description: "is nil", hasProcMount: false, pod: func() *api.Pod { return nil }, }, } for _, enabled := range []bool{true, false} { for _, oldPodInfo := range podInfo { for _, newPodInfo := range podInfo { oldPodHasProcMount, oldPod := oldPodInfo.hasProcMount, oldPodInfo.pod() newPodHasProcMount, newPod := newPodInfo.hasProcMount, newPodInfo.pod() if newPod == nil { continue } t.Run(fmt.Sprintf("feature enabled=%v, old pod %v, new pod %v", enabled, oldPodInfo.description, newPodInfo.description), func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ProcMountType, enabled) var oldPodSpec *api.PodSpec if oldPod != nil { oldPodSpec = &oldPod.Spec } dropDisabledFields(&newPod.Spec, nil, oldPodSpec, nil) // old pod should never be changed if !reflect.DeepEqual(oldPod, oldPodInfo.pod()) { t.Errorf("old pod changed: %v", cmp.Diff(oldPod, oldPodInfo.pod())) } switch { case enabled || oldPodHasProcMount: // new pod should not be changed if the feature is enabled, or if the old pod had ProcMount if !reflect.DeepEqual(newPod, newPodInfo.pod()) { t.Errorf("new pod changed: %v", cmp.Diff(newPod, newPodInfo.pod())) } case newPodHasProcMount: // new pod should be changed if reflect.DeepEqual(newPod, newPodInfo.pod()) { t.Errorf("new pod was not changed") } // new pod should not have ProcMount if procMountInUse(&newPod.Spec) { t.Errorf("new pod had ProcMount: %#v", &newPod.Spec) } default: // new pod should not need to be changed if !reflect.DeepEqual(newPod, newPodInfo.pod()) { t.Errorf("new pod changed: %v", cmp.Diff(newPod, newPodInfo.pod())) } } }) } } } } func TestDropDynamicResourceAllocation(t *testing.T) { resourceClaimName := "external-claim" podWithClaims := &api.Pod{ Spec: api.PodSpec{ Containers: []api.Container{ { Resources: api.ResourceRequirements{ Claims: []api.ResourceClaim{{Name: "my-claim"}}, }, }, }, InitContainers: []api.Container{ { Resources: api.ResourceRequirements{ Claims: []api.ResourceClaim{{Name: "my-claim"}}, }, }, }, EphemeralContainers: []api.EphemeralContainer{ { EphemeralContainerCommon: api.EphemeralContainerCommon{ Resources: api.ResourceRequirements{ Claims: []api.ResourceClaim{{Name: "my-claim"}}, }, }, }, }, ResourceClaims: []api.PodResourceClaim{ { Name: "my-claim", ResourceClaimName: &resourceClaimName, }, }, }, Status: api.PodStatus{ ResourceClaimStatuses: []api.PodResourceClaimStatus{ {Name: "my-claim", ResourceClaimName: ptr.To("pod-my-claim")}, }, }, } podWithoutClaims := &api.Pod{ Spec: api.PodSpec{ Containers: []api.Container{{}}, InitContainers: []api.Container{{}}, EphemeralContainers: []api.EphemeralContainer{{}}, }, } podWithExtendedResource := &api.Pod{ Spec: api.PodSpec{ Containers: []api.Container{{}}, InitContainers: []api.Container{{}}, EphemeralContainers: []api.EphemeralContainer{{}}, }, Status: api.PodStatus{ ExtendedResourceClaimStatus: &api.PodExtendedResourceClaimStatus{ ResourceClaimName: "resource-claim-name", RequestMappings: []api.ContainerExtendedResourceRequest{ { ContainerName: "c", ResourceName: "e", RequestName: "c-0-r-0", }, }, }, }, } var noPod *api.Pod testcases := []struct { description string enabled bool extendedEnabled bool oldPod *api.Pod newPod *api.Pod wantPod *api.Pod }{ { description: "old with claims / new with claims / disabled", oldPod: podWithClaims, newPod: podWithClaims, wantPod: podWithClaims, }, { description: "old without claims / new with claims / disabled", oldPod: podWithoutClaims, newPod: podWithClaims, wantPod: podWithoutClaims, }, { description: "no old pod/ new with claims / disabled", oldPod: noPod, newPod: podWithClaims, wantPod: podWithoutClaims, }, { description: "old with claims / new without claims / disabled", oldPod: podWithClaims, newPod: podWithoutClaims, wantPod: podWithoutClaims, }, { description: "old without claims / new without claims / disabled", oldPod: podWithoutClaims, newPod: podWithoutClaims, wantPod: podWithoutClaims, }, { description: "no old pod/ new without claims / disabled", oldPod: noPod, newPod: podWithoutClaims, wantPod: podWithoutClaims, }, { description: "old with claims / new with claims / enabled", enabled: true, oldPod: podWithClaims, newPod: podWithClaims, wantPod: podWithClaims, }, { description: "old without claims / new with claims / enabled", enabled: true, oldPod: podWithoutClaims, newPod: podWithClaims, wantPod: podWithClaims, }, { description: "no old pod/ new with claims / enabled", enabled: true, oldPod: noPod, newPod: podWithClaims, wantPod: podWithClaims, }, { description: "old with claims / new without claims / enabled", enabled: true, oldPod: podWithClaims, newPod: podWithoutClaims, wantPod: podWithoutClaims, }, { description: "old without claims / new without claims / enabled", enabled: true, oldPod: podWithoutClaims, newPod: podWithoutClaims, wantPod: podWithoutClaims, }, { description: "no old pod/ new without claims / enabled", enabled: true, oldPod: noPod, newPod: podWithoutClaims, wantPod: podWithoutClaims, }, { description: "extended resource / no old pod/ new with extended resource / disabled", enabled: false, extendedEnabled: false, oldPod: noPod, newPod: podWithExtendedResource, wantPod: podWithoutClaims, }, { description: "extended resource / old without claim / new with extended resource / disabled", enabled: false, extendedEnabled: false, oldPod: podWithoutClaims, newPod: podWithExtendedResource, wantPod: podWithoutClaims, }, { description: "extended resource / no old pod/ new with extended resource / extended disabled only", enabled: true, extendedEnabled: false, oldPod: noPod, newPod: podWithExtendedResource, wantPod: podWithoutClaims, }, { description: "extended resource / old without claim / new with extended resource / extended disabled only", enabled: true, extendedEnabled: false, oldPod: podWithoutClaims, newPod: podWithExtendedResource, wantPod: podWithoutClaims, }, { description: "extended resource / no old pod/ new with extended resource / enabled", enabled: true, extendedEnabled: true, oldPod: noPod, newPod: podWithExtendedResource, wantPod: podWithExtendedResource, }, { description: "extended resource / old without claim / new with extended resource / enabled", enabled: true, extendedEnabled: true, oldPod: podWithoutClaims, newPod: podWithExtendedResource, wantPod: podWithExtendedResource, }, } for _, tc := range testcases { t.Run(tc.description, func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DynamicResourceAllocation, tc.enabled) featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAExtendedResource, tc.extendedEnabled) oldPod := tc.oldPod.DeepCopy() newPod := tc.newPod.DeepCopy() wantPod := tc.wantPod DropDisabledPodFields(newPod, oldPod) // old pod should never be changed if diff := cmp.Diff(oldPod, tc.oldPod); diff != "" { t.Errorf("old pod changed: %s", diff) } if diff := cmp.Diff(wantPod, newPod); diff != "" { t.Errorf("new pod changed (- want, + got): %s", diff) } }) } } func TestValidatePodDeletionCostOption(t *testing.T) { testCases := []struct { name string oldPodMeta *metav1.ObjectMeta featureEnabled bool wantAllowInvalidPodDeletionCost bool }{ { name: "CreateFeatureEnabled", featureEnabled: true, wantAllowInvalidPodDeletionCost: false, }, { name: "CreateFeatureDisabled", featureEnabled: false, wantAllowInvalidPodDeletionCost: true, }, { name: "UpdateFeatureDisabled", oldPodMeta: &metav1.ObjectMeta{Annotations: map[string]string{api.PodDeletionCost: "100"}}, featureEnabled: false, wantAllowInvalidPodDeletionCost: true, }, { name: "UpdateFeatureEnabledValidOldValue", oldPodMeta: &metav1.ObjectMeta{Annotations: map[string]string{api.PodDeletionCost: "100"}}, featureEnabled: true, wantAllowInvalidPodDeletionCost: false, }, { name: "UpdateFeatureEnabledValidOldValue", oldPodMeta: &metav1.ObjectMeta{Annotations: map[string]string{api.PodDeletionCost: "invalid-value"}}, featureEnabled: true, wantAllowInvalidPodDeletionCost: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodDeletionCost, tc.featureEnabled) // The new pod doesn't impact the outcome. gotOptions := GetValidationOptionsFromPodSpecAndMeta(nil, nil, nil, tc.oldPodMeta) if tc.wantAllowInvalidPodDeletionCost != gotOptions.AllowInvalidPodDeletionCost { t.Errorf("unexpected diff, want: %v, got: %v", tc.wantAllowInvalidPodDeletionCost, gotOptions.AllowInvalidPodDeletionCost) } }) } } func TestDropDisabledPodStatusFields_HostIPs(t *testing.T) { podWithHostIPs := func() *api.PodStatus { return &api.PodStatus{ HostIPs: makeHostIPs("10.0.0.1", "fd00:10::1"), } } podWithoutHostIPs := func() *api.PodStatus { return &api.PodStatus{ HostIPs: nil, } } tests := []struct { name string podStatus *api.PodStatus oldPodStatus *api.PodStatus wantPodStatus *api.PodStatus }{ { name: "old=without, new=without", oldPodStatus: podWithoutHostIPs(), podStatus: podWithoutHostIPs(), wantPodStatus: podWithoutHostIPs(), }, { name: "old=without, new=with", oldPodStatus: podWithoutHostIPs(), podStatus: podWithHostIPs(), wantPodStatus: podWithHostIPs(), }, { name: "old=with, new=without", oldPodStatus: podWithHostIPs(), podStatus: podWithoutHostIPs(), wantPodStatus: podWithoutHostIPs(), }, { name: "old=with, new=with", oldPodStatus: podWithHostIPs(), podStatus: podWithHostIPs(), wantPodStatus: podWithHostIPs(), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dropDisabledPodStatusFields(tt.podStatus, tt.oldPodStatus, &api.PodSpec{}, &api.PodSpec{}) if !reflect.DeepEqual(tt.podStatus, tt.wantPodStatus) { t.Errorf("dropDisabledStatusFields() = %v, want %v", tt.podStatus, tt.wantPodStatus) } }) } } func makeHostIPs(ips ...string) []api.HostIP { ret := []api.HostIP{} for _, ip := range ips { ret = append(ret, api.HostIP{IP: ip}) } return ret } func TestDropDisabledPodStatusFields_ObservedGeneration(t *testing.T) { now := metav1.NewTime(time.Now()) podWithObservedGen := func() *api.PodStatus { return &api.PodStatus{ ObservedGeneration: 1, Conditions: []api.PodCondition{{ LastProbeTime: now, LastTransitionTime: now, }}, } } podWithObservedGenInConditions := func() *api.PodStatus { return &api.PodStatus{ Conditions: []api.PodCondition{{ LastProbeTime: now, LastTransitionTime: now, ObservedGeneration: 1, }}, } } podWithoutObservedGen := func() *api.PodStatus { return &api.PodStatus{ Conditions: []api.PodCondition{{ LastProbeTime: now, LastTransitionTime: now, }}, } } tests := []struct { name string podStatus *api.PodStatus oldPodStatus *api.PodStatus featureGateOn bool wantPodStatus *api.PodStatus }{ { name: "old=without, new=without / feature gate off", oldPodStatus: podWithoutObservedGen(), podStatus: podWithoutObservedGen(), featureGateOn: false, wantPodStatus: podWithoutObservedGen(), }, { name: "old=without, new=without / feature gate on", oldPodStatus: podWithoutObservedGen(), podStatus: podWithoutObservedGen(), featureGateOn: true, wantPodStatus: podWithoutObservedGen(), }, { name: "old=without, new=with / feature gate off", oldPodStatus: podWithoutObservedGen(), podStatus: podWithObservedGen(), featureGateOn: false, wantPodStatus: podWithoutObservedGen(), }, { name: "old=with, new=without / feature gate on", oldPodStatus: podWithObservedGen(), podStatus: podWithoutObservedGen(), featureGateOn: true, wantPodStatus: podWithoutObservedGen(), }, { name: "old=with, new=with / feature gate off", oldPodStatus: podWithObservedGen(), podStatus: podWithObservedGen(), featureGateOn: false, wantPodStatus: podWithObservedGen(), }, { name: "old=with, new=with / feature gate on", oldPodStatus: podWithObservedGen(), podStatus: podWithObservedGen(), featureGateOn: true, wantPodStatus: podWithObservedGen(), }, { name: "old=without, new=withInConditions / feature gate off", oldPodStatus: podWithoutObservedGen(), podStatus: podWithObservedGenInConditions(), featureGateOn: false, wantPodStatus: podWithoutObservedGen(), }, { name: "old=without, new=withInConditions / feature gate on", oldPodStatus: podWithoutObservedGen(), podStatus: podWithObservedGenInConditions(), featureGateOn: true, wantPodStatus: podWithObservedGenInConditions(), }, { name: "old=withInConditions, new=without / feature gate off", oldPodStatus: podWithObservedGenInConditions(), podStatus: podWithoutObservedGen(), featureGateOn: false, wantPodStatus: podWithoutObservedGen(), }, { name: "old=withInConditions, new=without / feature gate on", oldPodStatus: podWithObservedGenInConditions(), podStatus: podWithoutObservedGen(), featureGateOn: true, wantPodStatus: podWithoutObservedGen(), }, { name: "old=withInConditions, new=withInCondtions / feature gate off", oldPodStatus: podWithObservedGenInConditions(), podStatus: podWithObservedGenInConditions(), featureGateOn: false, wantPodStatus: podWithObservedGenInConditions(), }, { name: "old=withInConditions, new=withInCondtions / feature gate on", oldPodStatus: podWithObservedGenInConditions(), podStatus: podWithObservedGenInConditions(), featureGateOn: true, wantPodStatus: podWithObservedGenInConditions(), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodObservedGenerationTracking, tt.featureGateOn) dropDisabledPodStatusFields(tt.podStatus, tt.oldPodStatus, &api.PodSpec{}, &api.PodSpec{}) if !reflect.DeepEqual(tt.podStatus, tt.wantPodStatus) { t.Errorf("dropDisabledStatusFields() = %v, want %v", tt.podStatus, tt.wantPodStatus) } }) } } func TestDropNodeInclusionPolicyFields(t *testing.T) { ignore := api.NodeInclusionPolicyIgnore honor := api.NodeInclusionPolicyHonor tests := []struct { name string enabled bool podSpec *api.PodSpec oldPodSpec *api.PodSpec wantPodSpec *api.PodSpec }{ { name: "feature disabled, both pods don't use the fields", enabled: false, oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, podSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, wantPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, }, { name: "feature disabled, only old pod use NodeAffinityPolicy field", enabled: false, oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {NodeAffinityPolicy: &honor}, }, }, podSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, wantPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, }, { name: "feature disabled, only old pod use NodeTaintsPolicy field", enabled: false, oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {NodeTaintsPolicy: &ignore}, }, }, podSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, wantPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, }, { name: "feature disabled, only current pod use NodeAffinityPolicy field", enabled: false, oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, podSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {NodeAffinityPolicy: &honor}, }, }, wantPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{{ NodeAffinityPolicy: nil, }}, }, }, { name: "feature disabled, only current pod use NodeTaintsPolicy field", enabled: false, oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, podSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {NodeTaintsPolicy: &ignore}, }, }, wantPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {NodeTaintsPolicy: nil}, }, }, }, { name: "feature disabled, both pods use NodeAffinityPolicy fields", enabled: false, oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {NodeAffinityPolicy: &honor}, }, }, podSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {NodeAffinityPolicy: &ignore}, }, }, wantPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {NodeAffinityPolicy: &ignore}, }, }, }, { name: "feature disabled, both pods use NodeTaintsPolicy fields", enabled: false, oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {NodeTaintsPolicy: &ignore}, }, }, podSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {NodeTaintsPolicy: &honor}, }, }, wantPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {NodeTaintsPolicy: &honor}, }, }, }, { name: "feature enabled, both pods use the fields", enabled: true, oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ { NodeAffinityPolicy: &ignore, NodeTaintsPolicy: &honor, }, }, }, podSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ { NodeAffinityPolicy: &honor, NodeTaintsPolicy: &ignore, }, }, }, wantPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ { NodeAffinityPolicy: &honor, NodeTaintsPolicy: &ignore, }, }, }, }, { name: "feature enabled, only old pod use NodeAffinityPolicy field", enabled: true, oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ { NodeAffinityPolicy: &honor, }, }, }, podSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, wantPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, }, { name: "feature enabled, only old pod use NodeTaintsPolicy field", enabled: true, oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ { NodeTaintsPolicy: &ignore, }, }, }, podSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, wantPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, }, { name: "feature enabled, only current pod use NodeAffinityPolicy field", enabled: true, oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, podSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ { NodeAffinityPolicy: &ignore, }, }, }, wantPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ { NodeAffinityPolicy: &ignore, }, }, }, }, { name: "feature enabled, only current pod use NodeTaintsPolicy field", enabled: true, oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, podSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ { NodeTaintsPolicy: &honor, }, }, }, wantPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ { NodeTaintsPolicy: &honor, }, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if !test.enabled { // TODO: this will be removed in 1.36 featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.32")) featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.NodeInclusionPolicyInPodTopologySpread, test.enabled) } dropDisabledFields(test.podSpec, nil, test.oldPodSpec, nil) if diff := cmp.Diff(test.wantPodSpec, test.podSpec); diff != "" { t.Errorf("unexpected pod spec (-want, +got):\n%s", diff) } }) } } func Test_dropDisabledMatchLabelKeysFieldInPodAffinity(t *testing.T) { tests := []struct { name string enabled bool podSpec *api.PodSpec oldPodSpec *api.PodSpec wantPodSpec *api.PodSpec }{ { name: "[PodAffinity/required] feature disabled, both pods don't use MatchLabelKeys/MismatchLabelKeys fields", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{}, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{}, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{}, }, }, }, }, { name: "[PodAffinity/required] feature disabled, only old pod uses MatchLabelKeys/MismatchLabelKeys field", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{}, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{}, }, }, }, }, { name: "[PodAffinity/required] feature disabled, only current pod uses MatchLabelKeys/MismatchLabelKeys field", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{}, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{{}}, }, }, }, }, { name: "[PodAffinity/required] feature disabled, both pods use MatchLabelKeys/MismatchLabelKeys fields", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, { name: "[PodAffinity/required] feature enabled, only old pod uses MatchLabelKeys/MismatchLabelKeys field", enabled: true, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{}, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{}, }, }, }, }, { name: "[PodAffinity/required] feature enabled, only current pod uses MatchLabelKeys/MismatchLabelKeys field", enabled: true, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{}, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, { name: "[PodAffinity/required] feature enabled, both pods use MatchLabelKeys/MismatchLabelKeys fields", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, { name: "[PodAffinity/preferred] feature disabled, both pods don't use MatchLabelKeys/MismatchLabelKeys fields", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{}, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{}, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{}, }, }, }, }, { name: "[PodAffinity/preferred] feature disabled, only old pod uses MatchLabelKeys/MismatchLabelKeys field", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{}, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{}, }, }, }, }, { name: "[PodAffinity/preferred] feature disabled, only current pod uses MatchLabelKeys/MismatchLabelKeys field", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{}, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{{}}, }, }, }, }, { name: "[PodAffinity/preferred] feature disabled, both pods use MatchLabelKeys/MismatchLabelKeys fields", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, }, { name: "[PodAffinity/preferred] feature enabled, only old pod uses MatchLabelKeys/MismatchLabelKeys field", enabled: true, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{}, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{}, }, }, }, }, { name: "[PodAffinity/preferred] feature enabled, only current pod uses MatchLabelKeys/MismatchLabelKeys field", enabled: true, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{}, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, }, { name: "[PodAffinity/preferred] feature enabled, both pods use MatchLabelKeys/MismatchLabelKeys fields", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAffinity: &api.PodAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, }, { name: "[PodAntiAffinity/required] feature disabled, both pods don't use MatchLabelKeys/MismatchLabelKeys fields", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{}, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{}, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{}, }, }, }, }, { name: "[PodAntiAffinity/required] feature disabled, only old pod uses MatchLabelKeys/MismatchLabelKeys field", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{}, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{}, }, }, }, }, { name: "[PodAntiAffinity/required] feature disabled, only current pod uses MatchLabelKeys/MismatchLabelKeys field", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{}, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{{}}, }, }, }, }, { name: "[PodAntiAffinity/required] feature disabled, both pods use MatchLabelKeys/MismatchLabelKeys fields", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, { name: "[PodAntiAffinity/required] feature enabled, only old pod uses MatchLabelKeys/MismatchLabelKeys field", enabled: true, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{}, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{}, }, }, }, }, { name: "[PodAntiAffinity/required] feature enabled, only current pod uses MatchLabelKeys/MismatchLabelKeys field", enabled: true, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{}, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, { name: "[PodAntiAffinity/required] feature enabled, both pods use MatchLabelKeys/MismatchLabelKeys fields", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{ {MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, { name: "[PodAntiAffinity/preferred] feature disabled, both pods don't use MatchLabelKeys/MismatchLabelKeys fields", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{}, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{}, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{}, }, }, }, }, { name: "[PodAntiAffinity/preferred] feature disabled, only old pod uses MatchLabelKeys/MismatchLabelKeys field", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{}, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{}, }, }, }, }, { name: "[PodAntiAffinity/preferred] feature disabled, only current pod uses MatchLabelKeys/MismatchLabelKeys field", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{}, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{{}}, }, }, }, }, { name: "[PodAntiAffinity/preferred] feature disabled, both pods use MatchLabelKeys/MismatchLabelKeys fields", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, }, { name: "[PodAntiAffinity/preferred] feature enabled, only old pod uses MatchLabelKeys/MismatchLabelKeys field", enabled: true, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{}, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{}, }, }, }, }, { name: "[PodAntiAffinity/preferred] feature enabled, only current pod uses MatchLabelKeys/MismatchLabelKeys field", enabled: true, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{}, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, }, { name: "[PodAntiAffinity/preferred] feature enabled, both pods use MatchLabelKeys/MismatchLabelKeys fields", enabled: false, oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, podSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, wantPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ PodAntiAffinity: &api.PodAntiAffinity{ PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{ { PodAffinityTerm: api.PodAffinityTerm{MatchLabelKeys: []string{"foo"}, MismatchLabelKeys: []string{"foo"}}, }, }, }, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if !test.enabled { featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.32")) featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.MatchLabelKeysInPodAffinity, false) } dropDisabledFields(test.podSpec, nil, test.oldPodSpec, nil) if diff := cmp.Diff(test.wantPodSpec, test.podSpec); diff != "" { t.Errorf("unexpected pod spec (-want, +got):\n%s", diff) } }) } } func Test_dropDisabledMatchLabelKeysFieldInTopologySpread(t *testing.T) { tests := []struct { name string enabled bool podSpec *api.PodSpec oldPodSpec *api.PodSpec wantPodSpec *api.PodSpec }{ { name: "feature disabled, both pods don't use MatchLabelKeys fields", enabled: false, oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, podSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, wantPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, }, { name: "feature disabled, only old pod uses MatchLabelKeys field", enabled: false, oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {MatchLabelKeys: []string{"foo"}}, }, }, podSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, wantPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, }, { name: "feature disabled, only current pod uses MatchLabelKeys field", enabled: false, oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, podSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {MatchLabelKeys: []string{"foo"}}, }, }, wantPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{{}}, }, }, { name: "feature disabled, both pods use MatchLabelKeys fields", enabled: false, oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {MatchLabelKeys: []string{"foo"}}, }, }, podSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {MatchLabelKeys: []string{"foo"}}, }, }, wantPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {MatchLabelKeys: []string{"foo"}}, }, }, }, { name: "feature enabled, only old pod uses MatchLabelKeys field", enabled: true, oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {MatchLabelKeys: []string{"foo"}}, }, }, podSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, wantPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, }, { name: "feature enabled, only current pod uses MatchLabelKeys field", enabled: true, oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{}, }, podSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {MatchLabelKeys: []string{"foo"}}, }, }, wantPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {MatchLabelKeys: []string{"foo"}}, }, }, }, { name: "feature enabled, both pods use MatchLabelKeys fields", enabled: false, oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {MatchLabelKeys: []string{"foo"}}, }, }, podSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {MatchLabelKeys: []string{"foo"}}, }, }, wantPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ {MatchLabelKeys: []string{"foo"}}, }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.MatchLabelKeysInPodTopologySpread, test.enabled) dropDisabledFields(test.podSpec, nil, test.oldPodSpec, nil) if diff := cmp.Diff(test.wantPodSpec, test.podSpec); diff != "" { t.Errorf("unexpected pod spec (-want, +got):\n%s", diff) } }) } } func TestDropHostUsers(t *testing.T) { falseVar := false trueVar := true podWithoutHostUsers := func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{ SecurityContext: &api.PodSecurityContext{}}, } } podWithHostUsersFalse := func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{ SecurityContext: &api.PodSecurityContext{ HostUsers: &falseVar, }, }, } } podWithHostUsersTrue := func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{ SecurityContext: &api.PodSecurityContext{ HostUsers: &trueVar, }, }, } } podInfo := []struct { description string hasHostUsers bool pod func() *api.Pod }{ { description: "with hostUsers=true", hasHostUsers: true, pod: podWithHostUsersTrue, }, { description: "with hostUsers=false", hasHostUsers: true, pod: podWithHostUsersFalse, }, { description: "with hostUsers=nil", pod: func() *api.Pod { return nil }, }, } for _, enabled := range []bool{true, false} { for _, oldPodInfo := range podInfo { for _, newPodInfo := range podInfo { oldPodHasHostUsers, oldPod := oldPodInfo.hasHostUsers, oldPodInfo.pod() newPodHasHostUsers, newPod := newPodInfo.hasHostUsers, newPodInfo.pod() if newPod == nil { continue } t.Run(fmt.Sprintf("feature enabled=%v, old pod %v, new pod %v", enabled, oldPodInfo.description, newPodInfo.description), func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.UserNamespacesSupport, enabled) DropDisabledPodFields(newPod, oldPod) // old pod should never be changed if !reflect.DeepEqual(oldPod, oldPodInfo.pod()) { t.Errorf("old pod changed: %v", cmp.Diff(oldPod, oldPodInfo.pod())) } switch { case enabled || oldPodHasHostUsers: // new pod should not be changed if the feature is enabled, or if the old pod had hostUsers if !reflect.DeepEqual(newPod, newPodInfo.pod()) { t.Errorf("new pod changed: %v", cmp.Diff(newPod, newPodInfo.pod())) } case newPodHasHostUsers: // new pod should be changed if reflect.DeepEqual(newPod, newPodInfo.pod()) { t.Errorf("new pod was not changed") } // new pod should not have hostUsers if exp := podWithoutHostUsers(); !reflect.DeepEqual(newPod, exp) { t.Errorf("new pod had hostUsers: %v", cmp.Diff(newPod, exp)) } default: // new pod should not need to be changed if !reflect.DeepEqual(newPod, newPodInfo.pod()) { t.Errorf("new pod changed: %v", cmp.Diff(newPod, newPodInfo.pod())) } } }) } } } } func TestValidateTopologySpreadConstraintLabelSelectorOption(t *testing.T) { testCases := []struct { name string oldPodSpec *api.PodSpec wantOption bool }{ { name: "Create", wantOption: false, }, { name: "UpdateInvalidLabelSelector", oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ { LabelSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "foo"}, }, }, }, }, wantOption: true, }, { name: "UpdateValidLabelSelector", oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ { LabelSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{"foo": "foo"}, }, }, }, }, wantOption: false, }, { name: "UpdateEmptyLabelSelector", oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{ { LabelSelector: nil, }, }, }, wantOption: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Pod meta doesn't impact the outcome. gotOptions := GetValidationOptionsFromPodSpecAndMeta(&api.PodSpec{}, tc.oldPodSpec, nil, nil) if tc.wantOption != gotOptions.AllowInvalidTopologySpreadConstraintLabelSelector { t.Errorf("Got AllowInvalidLabelValueInSelector=%t, want %t", gotOptions.AllowInvalidTopologySpreadConstraintLabelSelector, tc.wantOption) } }) } } func TestOldPodViolatesMatchLabelKeysValidationOption(t *testing.T) { testCases := []struct { name string oldPodSpec *api.PodSpec matchLabelKeysEnabled bool matchLabelKeysSelectorMergeEnabled bool wantOption bool }{ { name: "Create", oldPodSpec: &api.PodSpec{}, matchLabelKeysEnabled: true, matchLabelKeysSelectorMergeEnabled: true, wantOption: false, }, { name: "UpdateInvalidMatchLabelKeys", oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{{ MatchLabelKeys: []string{"foo"}, LabelSelector: &metav1.LabelSelector{ MatchExpressions: []metav1.LabelSelectorRequirement{ { Key: "foo", Operator: metav1.LabelSelectorOpNotIn, Values: []string{"value1"}, }, { Key: "foo", Operator: metav1.LabelSelectorOpNotIn, Values: []string{"value2", "value3"}, }, }, }, }}, }, matchLabelKeysEnabled: true, matchLabelKeysSelectorMergeEnabled: true, wantOption: true, }, { name: "UpdateValidMatchLabelKeys", oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{{ MatchLabelKeys: []string{"foo"}, LabelSelector: &metav1.LabelSelector{ MatchExpressions: []metav1.LabelSelectorRequirement{ { Key: "foo", Operator: metav1.LabelSelectorOpNotIn, Values: []string{"value1"}, }, }, }, }}, }, matchLabelKeysEnabled: true, matchLabelKeysSelectorMergeEnabled: true, wantOption: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.MatchLabelKeysInPodTopologySpread, tc.matchLabelKeysEnabled) featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.MatchLabelKeysInPodTopologySpreadSelectorMerge, tc.matchLabelKeysSelectorMergeEnabled) gotOptions := GetValidationOptionsFromPodSpecAndMeta(&api.PodSpec{}, tc.oldPodSpec, nil, nil) if tc.wantOption != gotOptions.OldPodViolatesMatchLabelKeysValidation { t.Errorf("Got OldPodViolatesMatchLabelKeysValidation=%t, want %t", gotOptions.OldPodViolatesMatchLabelKeysValidation, tc.wantOption) } }) } } func TestOldPodViolatesLegacyMatchLabelKeysValidationOption(t *testing.T) { testCases := []struct { name string oldPodSpec *api.PodSpec matchLabelKeysEnabled bool matchLabelKeysSelectorMergeEnabled bool wantOption bool }{ { name: "Create", oldPodSpec: &api.PodSpec{}, matchLabelKeysEnabled: true, matchLabelKeysSelectorMergeEnabled: false, wantOption: false, }, { name: "UpdateInvalidMatchLabelKeys", oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{{ MatchLabelKeys: []string{"foo"}, LabelSelector: &metav1.LabelSelector{ MatchExpressions: []metav1.LabelSelectorRequirement{ { Key: "foo", Operator: metav1.LabelSelectorOpNotIn, Values: []string{"value1"}, }, }, }, }}, }, matchLabelKeysEnabled: true, matchLabelKeysSelectorMergeEnabled: false, wantOption: true, }, { name: "UpdateValidMatchLabelKeys", oldPodSpec: &api.PodSpec{ TopologySpreadConstraints: []api.TopologySpreadConstraint{{ MatchLabelKeys: []string{"foo"}, LabelSelector: &metav1.LabelSelector{}, }}, }, matchLabelKeysEnabled: true, matchLabelKeysSelectorMergeEnabled: false, wantOption: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.MatchLabelKeysInPodTopologySpread, tc.matchLabelKeysEnabled) featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.MatchLabelKeysInPodTopologySpreadSelectorMerge, tc.matchLabelKeysSelectorMergeEnabled) gotOptions := GetValidationOptionsFromPodSpecAndMeta(&api.PodSpec{}, tc.oldPodSpec, nil, nil) if tc.wantOption != gotOptions.OldPodViolatesLegacyMatchLabelKeysValidation { t.Errorf("Got OldPodViolatesLegacyMatchLabelKeysValidation=%t, want %t", gotOptions.OldPodViolatesLegacyMatchLabelKeysValidation, tc.wantOption) } }) } } func TestValidateAllowNonLocalProjectedTokenPathOption(t *testing.T) { testCases := []struct { name string oldPodSpec *api.PodSpec wantOption bool }{ { name: "Create", wantOption: false, }, { name: "UpdateInvalidProjectedTokenPath", oldPodSpec: &api.PodSpec{ Volumes: []api.Volume{ { Name: "foo", VolumeSource: api.VolumeSource{ Projected: &api.ProjectedVolumeSource{ Sources: []api.VolumeProjection{ { ServiceAccountToken: &api.ServiceAccountTokenProjection{ Path: "../foo", }, }, }, }, }, }, }, }, wantOption: true, }, { name: "UpdateValidProjectedTokenPath", oldPodSpec: &api.PodSpec{ Volumes: []api.Volume{ { Name: "foo", VolumeSource: api.VolumeSource{ Projected: &api.ProjectedVolumeSource{ Sources: []api.VolumeProjection{ { ServiceAccountToken: &api.ServiceAccountTokenProjection{ Path: "foo", }, }, }, }, }, }, }, }, wantOption: false, }, { name: "UpdateEmptyProjectedTokenPath", oldPodSpec: &api.PodSpec{ Volumes: []api.Volume{ { Name: "foo", VolumeSource: api.VolumeSource{ Projected: nil, HostPath: &api.HostPathVolumeSource{Path: "foo"}, }, }, }, }, wantOption: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Pod meta doesn't impact the outcome. gotOptions := GetValidationOptionsFromPodSpecAndMeta(&api.PodSpec{}, tc.oldPodSpec, nil, nil) if tc.wantOption != gotOptions.AllowNonLocalProjectedTokenPath { t.Errorf("Got AllowNonLocalProjectedTokenPath=%t, want %t", gotOptions.AllowNonLocalProjectedTokenPath, tc.wantOption) } }) } } func TestDropInPlacePodVerticalScaling(t *testing.T) { podWithInPlaceVerticalScaling := func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{ Containers: []api.Container{ { Name: "c1", Image: "image", Resources: api.ResourceRequirements{ Requests: api.ResourceList{api.ResourceCPU: resource.MustParse("100m")}, Limits: api.ResourceList{api.ResourceCPU: resource.MustParse("200m")}, }, ResizePolicy: []api.ContainerResizePolicy{ {ResourceName: api.ResourceCPU, RestartPolicy: api.NotRequired}, {ResourceName: api.ResourceMemory, RestartPolicy: api.RestartContainer}, }, }, }, }, Status: api.PodStatus{ ContainerStatuses: []api.ContainerStatus{ { Name: "c1", Image: "image", AllocatedResources: api.ResourceList{api.ResourceCPU: resource.MustParse("100m")}, Resources: &api.ResourceRequirements{ Requests: api.ResourceList{api.ResourceCPU: resource.MustParse("200m")}, Limits: api.ResourceList{api.ResourceCPU: resource.MustParse("300m")}, }, }, }, }, } } podWithoutInPlaceVerticalScaling := func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{ Containers: []api.Container{ { Name: "c1", Image: "image", Resources: api.ResourceRequirements{ Requests: api.ResourceList{api.ResourceCPU: resource.MustParse("100m")}, Limits: api.ResourceList{api.ResourceCPU: resource.MustParse("200m")}, }, }, }, }, Status: api.PodStatus{ ContainerStatuses: []api.ContainerStatus{ { Name: "c1", Image: "image", }, }, }, } } podInfo := []struct { description string hasInPlaceVerticalScaling bool pod func() *api.Pod }{ { description: "has in-place vertical scaling enabled with resources", hasInPlaceVerticalScaling: true, pod: podWithInPlaceVerticalScaling, }, { description: "has in-place vertical scaling disabled", hasInPlaceVerticalScaling: false, pod: podWithoutInPlaceVerticalScaling, }, { description: "is nil", hasInPlaceVerticalScaling: false, pod: func() *api.Pod { return nil }, }, } for _, ippvsEnabled := range []bool{true, false} { t.Run(fmt.Sprintf("InPlacePodVerticalScaling=%t", ippvsEnabled), func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodVerticalScaling, ippvsEnabled) for _, oldPodInfo := range podInfo { for _, newPodInfo := range podInfo { oldPodHasInPlaceVerticalScaling, oldPod := oldPodInfo.hasInPlaceVerticalScaling, oldPodInfo.pod() newPodHasInPlaceVerticalScaling, newPod := newPodInfo.hasInPlaceVerticalScaling, newPodInfo.pod() if newPod == nil { continue } t.Run(fmt.Sprintf("old pod %v, new pod %v", oldPodInfo.description, newPodInfo.description), func(t *testing.T) { var oldPodSpec *api.PodSpec var oldPodStatus *api.PodStatus if oldPod != nil { oldPodSpec = &oldPod.Spec oldPodStatus = &oldPod.Status } dropDisabledFields(&newPod.Spec, nil, oldPodSpec, nil) dropDisabledPodStatusFields(&newPod.Status, oldPodStatus, &newPod.Spec, oldPodSpec) // old pod should never be changed if !reflect.DeepEqual(oldPod, oldPodInfo.pod()) { t.Errorf("old pod changed: %v", cmp.Diff(oldPod, oldPodInfo.pod())) } switch { case ippvsEnabled || oldPodHasInPlaceVerticalScaling: // new pod shouldn't change if feature enabled or if old pod has ResizePolicy set expected := newPodInfo.pod() if !reflect.DeepEqual(newPod, expected) { t.Errorf("new pod changed: %v", cmp.Diff(newPod, expected)) } case newPodHasInPlaceVerticalScaling: // new pod should be changed if reflect.DeepEqual(newPod, newPodInfo.pod()) { t.Errorf("new pod was not changed") } // new pod should not have ResizePolicy if !reflect.DeepEqual(newPod, podWithoutInPlaceVerticalScaling()) { t.Errorf("new pod has ResizePolicy: %v", cmp.Diff(newPod, podWithoutInPlaceVerticalScaling())) } default: // new pod should not need to be changed if !reflect.DeepEqual(newPod, newPodInfo.pod()) { t.Errorf("new pod changed: %v", cmp.Diff(newPod, newPodInfo.pod())) } } }) } } }) } } func TestDropPodLevelResources(t *testing.T) { containers := []api.Container{ { Name: "c1", Image: "image", Resources: api.ResourceRequirements{ Requests: api.ResourceList{api.ResourceCPU: resource.MustParse("100m")}, Limits: api.ResourceList{api.ResourceCPU: resource.MustParse("200m")}, }, }, } podWithPodLevelResources := func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{ Resources: &api.ResourceRequirements{ Requests: api.ResourceList{ api.ResourceCPU: resource.MustParse("100m"), api.ResourceMemory: resource.MustParse("50Gi"), }, Limits: api.ResourceList{ api.ResourceCPU: resource.MustParse("100m"), api.ResourceMemory: resource.MustParse("50Gi"), }, }, Containers: containers, }, } } podWithoutPodLevelResources := func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{ Containers: containers, }, } } podInfo := []struct { description string hasPodLevelResources bool pod func() *api.Pod }{ { description: "has pod-level resources", hasPodLevelResources: true, pod: podWithPodLevelResources, }, { description: "does not have pod-level resources", hasPodLevelResources: false, pod: podWithoutPodLevelResources, }, { description: "is nil", hasPodLevelResources: false, pod: func() *api.Pod { return nil }, }, { description: "is empty struct", hasPodLevelResources: false, // refactor to generalize and use podWithPodLevelResources() pod: func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{ Resources: &api.ResourceRequirements{}, Containers: containers, }, } }, }, { description: "is empty Requests list", hasPodLevelResources: false, pod: func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{Resources: &api.ResourceRequirements{ Requests: api.ResourceList{}, }}} }, }, { description: "is empty Limits list", hasPodLevelResources: false, pod: func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{Resources: &api.ResourceRequirements{ Limits: api.ResourceList{}, }}} }, }, } for _, enabled := range []bool{true, false} { for _, oldPodInfo := range podInfo { for _, newPodInfo := range podInfo { oldPodHasPodLevelResources, oldPod := oldPodInfo.hasPodLevelResources, oldPodInfo.pod() newPodHasPodLevelResources, newPod := newPodInfo.hasPodLevelResources, newPodInfo.pod() if newPod == nil { continue } t.Run(fmt.Sprintf("feature enabled=%v, old pod %v, new pod %v", enabled, oldPodInfo.description, newPodInfo.description), func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodLevelResources, enabled) var oldPodSpec *api.PodSpec if oldPod != nil { oldPodSpec = &oldPod.Spec } dropDisabledFields(&newPod.Spec, nil, oldPodSpec, nil) // old pod should never be changed if !reflect.DeepEqual(oldPod, oldPodInfo.pod()) { t.Errorf("old pod changed: %v", cmp.Diff(oldPod, oldPodInfo.pod())) } switch { case enabled || oldPodHasPodLevelResources: // new pod shouldn't change if feature enabled or if old pod has // any pod level resources if !reflect.DeepEqual(newPod, newPodInfo.pod()) { t.Errorf("new pod changed: %v", cmp.Diff(newPod, newPodInfo.pod())) } case newPodHasPodLevelResources: // new pod should be changed if reflect.DeepEqual(newPod, newPodInfo.pod()) { t.Errorf("new pod was not changed") } // new pod should not have any pod-level resources if !reflect.DeepEqual(newPod, podWithoutPodLevelResources()) { t.Errorf("new pod has pod-level resources: %v", cmp.Diff(newPod, podWithoutPodLevelResources())) } default: if newPod.Spec.Resources != nil { t.Errorf("expected nil, got: %v", newPod.Spec.Resources) } } }) } } } } func TestDropSidecarContainers(t *testing.T) { containerRestartPolicyAlways := api.ContainerRestartPolicyAlways podWithSidecarContainers := func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{ InitContainers: []api.Container{ { Name: "c1", Image: "image", RestartPolicy: &containerRestartPolicyAlways, }, }, }, } } podWithoutSidecarContainers := func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{ InitContainers: []api.Container{ { Name: "c1", Image: "image", }, }, }, } } podInfo := []struct { description string hasSidecarContainer bool pod func() *api.Pod }{ { description: "has a sidecar container", hasSidecarContainer: true, pod: podWithSidecarContainers, }, { description: "does not have a sidecar container", hasSidecarContainer: false, pod: podWithoutSidecarContainers, }, { description: "is nil", hasSidecarContainer: false, pod: func() *api.Pod { return nil }, }, } for _, enabled := range []bool{true, false} { for _, oldPodInfo := range podInfo { for _, newPodInfo := range podInfo { oldPodHasSidecarContainer, oldPod := oldPodInfo.hasSidecarContainer, oldPodInfo.pod() newPodHasSidecarContainer, newPod := newPodInfo.hasSidecarContainer, newPodInfo.pod() if newPod == nil { continue } t.Run(fmt.Sprintf("feature enabled=%v, old pod %v, new pod %v", enabled, oldPodInfo.description, newPodInfo.description), func(t *testing.T) { if !enabled { // TODO: Remove this in v1.36 featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.32")) featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SidecarContainers, false) } var oldPodSpec *api.PodSpec if oldPod != nil { oldPodSpec = &oldPod.Spec } dropDisabledFields(&newPod.Spec, nil, oldPodSpec, nil) // old pod should never be changed if !reflect.DeepEqual(oldPod, oldPodInfo.pod()) { t.Errorf("old pod changed: %v", cmp.Diff(oldPod, oldPodInfo.pod())) } switch { case enabled || oldPodHasSidecarContainer: // new pod shouldn't change if feature enabled or if old pod has // any sidecar container if !reflect.DeepEqual(newPod, newPodInfo.pod()) { t.Errorf("new pod changed: %v", cmp.Diff(newPod, newPodInfo.pod())) } case newPodHasSidecarContainer: // new pod should be changed if reflect.DeepEqual(newPod, newPodInfo.pod()) { t.Errorf("new pod was not changed") } // new pod should not have any sidecar container if !reflect.DeepEqual(newPod, podWithoutSidecarContainers()) { t.Errorf("new pod has a sidecar container: %v", cmp.Diff(newPod, podWithoutSidecarContainers())) } default: // new pod should not need to be changed if !reflect.DeepEqual(newPod, newPodInfo.pod()) { t.Errorf("new pod changed: %v", cmp.Diff(newPod, newPodInfo.pod())) } } }) } } } } func TestDropClusterTrustBundleProjectedVolumes(t *testing.T) { testCases := []struct { description string clusterTrustBundleProjectionEnabled bool oldPod *api.PodSpec newPod *api.PodSpec wantPod *api.PodSpec }{ { description: "feature gate disabled, cannot add CTB volume to pod", oldPod: &api.PodSpec{ Volumes: []api.Volume{}, }, newPod: &api.PodSpec{ Volumes: []api.Volume{ { Name: "foo", VolumeSource: api.VolumeSource{ Projected: &api.ProjectedVolumeSource{ Sources: []api.VolumeProjection{ { ClusterTrustBundle: &api.ClusterTrustBundleProjection{ Name: ptr.To("foo"), }, }, }, }}, }, }, }, wantPod: &api.PodSpec{ Volumes: []api.Volume{ { Name: "foo", VolumeSource: api.VolumeSource{ Projected: &api.ProjectedVolumeSource{ Sources: []api.VolumeProjection{ {}, }, }}, }, }, }, }, { description: "feature gate disabled, can keep CTB volume on pod", oldPod: &api.PodSpec{ Volumes: []api.Volume{ { Name: "foo", VolumeSource: api.VolumeSource{ Projected: &api.ProjectedVolumeSource{ Sources: []api.VolumeProjection{ { ClusterTrustBundle: &api.ClusterTrustBundleProjection{ Name: ptr.To("foo"), }, }, }, }}, }, }, }, newPod: &api.PodSpec{ Volumes: []api.Volume{ { Name: "foo", VolumeSource: api.VolumeSource{ Projected: &api.ProjectedVolumeSource{ Sources: []api.VolumeProjection{ { ClusterTrustBundle: &api.ClusterTrustBundleProjection{ Name: ptr.To("foo"), }, }, }, }}, }, }, }, wantPod: &api.PodSpec{ Volumes: []api.Volume{ { Name: "foo", VolumeSource: api.VolumeSource{ Projected: &api.ProjectedVolumeSource{ Sources: []api.VolumeProjection{ { ClusterTrustBundle: &api.ClusterTrustBundleProjection{ Name: ptr.To("foo"), }, }, }, }}, }, }, }, }, { description: "feature gate enabled, can add CTB volume to pod", clusterTrustBundleProjectionEnabled: true, oldPod: &api.PodSpec{ Volumes: []api.Volume{}, }, newPod: &api.PodSpec{ Volumes: []api.Volume{ { Name: "foo", VolumeSource: api.VolumeSource{ Projected: &api.ProjectedVolumeSource{ Sources: []api.VolumeProjection{ { ClusterTrustBundle: &api.ClusterTrustBundleProjection{ Name: ptr.To("foo"), }, }, }, }}, }, }, }, wantPod: &api.PodSpec{ Volumes: []api.Volume{ { Name: "foo", VolumeSource: api.VolumeSource{ Projected: &api.ProjectedVolumeSource{ Sources: []api.VolumeProjection{ { ClusterTrustBundle: &api.ClusterTrustBundleProjection{ Name: ptr.To("foo"), }, }, }, }}, }, }, }, }, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ClusterTrustBundleProjection, tc.clusterTrustBundleProjectionEnabled) dropDisabledClusterTrustBundleProjection(tc.newPod, tc.oldPod) if diff := cmp.Diff(tc.newPod, tc.wantPod); diff != "" { t.Fatalf("Unexpected modification to new pod; diff (-got +want)\n%s", diff) } }) } } func TestDropPodCertificateProjectedVolumes(t *testing.T) { testCases := []struct { description string podCertificateProjectionEnabled bool oldPod *api.PodSpec newPod *api.PodSpec wantPod *api.PodSpec }{ { description: "feature gate disabled, cannot add volume to pod", oldPod: &api.PodSpec{ Volumes: []api.Volume{}, }, newPod: &api.PodSpec{ Volumes: []api.Volume{ { Name: "foo", VolumeSource: api.VolumeSource{ Projected: &api.ProjectedVolumeSource{ Sources: []api.VolumeProjection{ { PodCertificate: &api.PodCertificateProjection{ SignerName: "foo.example.com/bar", }, }, }, }}, }, }, }, wantPod: &api.PodSpec{ Volumes: []api.Volume{ { Name: "foo", VolumeSource: api.VolumeSource{ Projected: &api.ProjectedVolumeSource{ Sources: []api.VolumeProjection{ {}, }, }}, }, }, }, }, { description: "feature gate disabled, can keep volume on pod", oldPod: &api.PodSpec{ Volumes: []api.Volume{ { Name: "foo", VolumeSource: api.VolumeSource{ Projected: &api.ProjectedVolumeSource{ Sources: []api.VolumeProjection{ { PodCertificate: &api.PodCertificateProjection{ SignerName: "foo.example.com/bar", }, }, }, }}, }, }, }, newPod: &api.PodSpec{ Volumes: []api.Volume{ { Name: "foo", VolumeSource: api.VolumeSource{ Projected: &api.ProjectedVolumeSource{ Sources: []api.VolumeProjection{ { PodCertificate: &api.PodCertificateProjection{ SignerName: "foo.example.com/bar", }, }, }, }}, }, }, }, wantPod: &api.PodSpec{ Volumes: []api.Volume{ { Name: "foo", VolumeSource: api.VolumeSource{ Projected: &api.ProjectedVolumeSource{ Sources: []api.VolumeProjection{ { PodCertificate: &api.PodCertificateProjection{ SignerName: "foo.example.com/bar", }, }, }, }}, }, }, }, }, { description: "feature gate enabled, can add volume to pod", podCertificateProjectionEnabled: true, oldPod: &api.PodSpec{ Volumes: []api.Volume{}, }, newPod: &api.PodSpec{ Volumes: []api.Volume{ { Name: "foo", VolumeSource: api.VolumeSource{ Projected: &api.ProjectedVolumeSource{ Sources: []api.VolumeProjection{ { PodCertificate: &api.PodCertificateProjection{ SignerName: "foo.example.com/bar", }, }, }, }}, }, }, }, wantPod: &api.PodSpec{ Volumes: []api.Volume{ { Name: "foo", VolumeSource: api.VolumeSource{ Projected: &api.ProjectedVolumeSource{ Sources: []api.VolumeProjection{ { PodCertificate: &api.PodCertificateProjection{ SignerName: "foo.example.com/bar", }, }, }, }}, }, }, }, }, } for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodCertificateRequest, tc.podCertificateProjectionEnabled) dropDisabledPodCertificateProjection(tc.newPod, tc.oldPod) if diff := cmp.Diff(tc.newPod, tc.wantPod); diff != "" { t.Fatalf("Unexpected modification to new pod; diff (-got +want)\n%s", diff) } }) } } func TestDropPodLifecycleSleepAction(t *testing.T) { makeSleepHandler := func() *api.LifecycleHandler { return &api.LifecycleHandler{ Sleep: &api.SleepAction{Seconds: 1}, } } makeExecHandler := func() *api.LifecycleHandler { return &api.LifecycleHandler{ Exec: &api.ExecAction{Command: []string{"foo"}}, } } makeHTTPGetHandler := func() *api.LifecycleHandler { return &api.LifecycleHandler{ HTTPGet: &api.HTTPGetAction{Host: "foo"}, } } makeContainer := func(preStop, postStart *api.LifecycleHandler) api.Container { container := api.Container{Name: "foo"} if preStop != nil || postStart != nil { container.Lifecycle = &api.Lifecycle{ PostStart: postStart, PreStop: preStop, } } return container } makeEphemeralContainer := func(preStop, postStart *api.LifecycleHandler) api.EphemeralContainer { container := api.EphemeralContainer{ EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "foo"}, } if preStop != nil || postStart != nil { container.Lifecycle = &api.Lifecycle{ PostStart: postStart, PreStop: preStop, } } return container } makePod := func(containers []api.Container, initContainers []api.Container, ephemeralContainers []api.EphemeralContainer) *api.PodSpec { return &api.PodSpec{ Containers: containers, InitContainers: initContainers, EphemeralContainers: ephemeralContainers, } } testCases := []struct { gateEnabled bool oldLifecycleHandler *api.LifecycleHandler newLifecycleHandler *api.LifecycleHandler expectLifecycleHandler *api.LifecycleHandler }{ // nil -> nil { gateEnabled: false, oldLifecycleHandler: nil, newLifecycleHandler: nil, expectLifecycleHandler: nil, }, { gateEnabled: true, oldLifecycleHandler: nil, newLifecycleHandler: nil, expectLifecycleHandler: nil, }, // nil -> exec { gateEnabled: false, oldLifecycleHandler: nil, newLifecycleHandler: makeExecHandler(), expectLifecycleHandler: makeExecHandler(), }, { gateEnabled: true, oldLifecycleHandler: nil, newLifecycleHandler: makeExecHandler(), expectLifecycleHandler: makeExecHandler(), }, // nil -> sleep { gateEnabled: false, oldLifecycleHandler: nil, newLifecycleHandler: makeSleepHandler(), expectLifecycleHandler: nil, }, { gateEnabled: true, oldLifecycleHandler: nil, newLifecycleHandler: makeSleepHandler(), expectLifecycleHandler: makeSleepHandler(), }, // exec -> exec { gateEnabled: false, oldLifecycleHandler: makeExecHandler(), newLifecycleHandler: makeExecHandler(), expectLifecycleHandler: makeExecHandler(), }, { gateEnabled: true, oldLifecycleHandler: makeExecHandler(), newLifecycleHandler: makeExecHandler(), expectLifecycleHandler: makeExecHandler(), }, // exec -> http { gateEnabled: false, oldLifecycleHandler: makeExecHandler(), newLifecycleHandler: makeHTTPGetHandler(), expectLifecycleHandler: makeHTTPGetHandler(), }, { gateEnabled: true, oldLifecycleHandler: makeExecHandler(), newLifecycleHandler: makeHTTPGetHandler(), expectLifecycleHandler: makeHTTPGetHandler(), }, // exec -> sleep { gateEnabled: false, oldLifecycleHandler: makeExecHandler(), newLifecycleHandler: makeSleepHandler(), expectLifecycleHandler: nil, }, { gateEnabled: true, oldLifecycleHandler: makeExecHandler(), newLifecycleHandler: makeSleepHandler(), expectLifecycleHandler: makeSleepHandler(), }, // sleep -> exec { gateEnabled: false, oldLifecycleHandler: makeSleepHandler(), newLifecycleHandler: makeExecHandler(), expectLifecycleHandler: makeExecHandler(), }, { gateEnabled: true, oldLifecycleHandler: makeSleepHandler(), newLifecycleHandler: makeExecHandler(), expectLifecycleHandler: makeExecHandler(), }, // sleep -> sleep { gateEnabled: false, oldLifecycleHandler: makeSleepHandler(), newLifecycleHandler: makeSleepHandler(), expectLifecycleHandler: makeSleepHandler(), }, { gateEnabled: true, oldLifecycleHandler: makeSleepHandler(), newLifecycleHandler: makeSleepHandler(), expectLifecycleHandler: makeSleepHandler(), }, } for i, tc := range testCases { t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) { featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.33")) featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodLifecycleSleepAction, tc.gateEnabled) // preStop // container { oldPod := makePod([]api.Container{makeContainer(tc.oldLifecycleHandler.DeepCopy(), nil)}, nil, nil) newPod := makePod([]api.Container{makeContainer(tc.newLifecycleHandler.DeepCopy(), nil)}, nil, nil) expectPod := makePod([]api.Container{makeContainer(tc.expectLifecycleHandler.DeepCopy(), nil)}, nil, nil) dropDisabledFields(newPod, nil, oldPod, nil) if diff := cmp.Diff(expectPod, newPod); diff != "" { t.Fatalf("Unexpected modification to new pod; diff (-got +want)\n%s", diff) } } // InitContainer { oldPod := makePod(nil, []api.Container{makeContainer(tc.oldLifecycleHandler.DeepCopy(), nil)}, nil) newPod := makePod(nil, []api.Container{makeContainer(tc.newLifecycleHandler.DeepCopy(), nil)}, nil) expectPod := makePod(nil, []api.Container{makeContainer(tc.expectLifecycleHandler.DeepCopy(), nil)}, nil) dropDisabledFields(newPod, nil, oldPod, nil) if diff := cmp.Diff(expectPod, newPod); diff != "" { t.Fatalf("Unexpected modification to new pod; diff (-got +want)\n%s", diff) } } // EphemeralContainer { oldPod := makePod(nil, nil, []api.EphemeralContainer{makeEphemeralContainer(tc.oldLifecycleHandler.DeepCopy(), nil)}) newPod := makePod(nil, nil, []api.EphemeralContainer{makeEphemeralContainer(tc.newLifecycleHandler.DeepCopy(), nil)}) expectPod := makePod(nil, nil, []api.EphemeralContainer{makeEphemeralContainer(tc.expectLifecycleHandler.DeepCopy(), nil)}) dropDisabledFields(newPod, nil, oldPod, nil) if diff := cmp.Diff(expectPod, newPod); diff != "" { t.Fatalf("Unexpected modification to new pod; diff (-got +want)\n%s", diff) } } // postStart // container { oldPod := makePod([]api.Container{makeContainer(nil, tc.oldLifecycleHandler.DeepCopy())}, nil, nil) newPod := makePod([]api.Container{makeContainer(nil, tc.newLifecycleHandler.DeepCopy())}, nil, nil) expectPod := makePod([]api.Container{makeContainer(nil, tc.expectLifecycleHandler.DeepCopy())}, nil, nil) dropDisabledFields(newPod, nil, oldPod, nil) if diff := cmp.Diff(expectPod, newPod); diff != "" { t.Fatalf("Unexpected modification to new pod; diff (-got +want)\n%s", diff) } } // InitContainer { oldPod := makePod(nil, []api.Container{makeContainer(nil, tc.oldLifecycleHandler.DeepCopy())}, nil) newPod := makePod(nil, []api.Container{makeContainer(nil, tc.newLifecycleHandler.DeepCopy())}, nil) expectPod := makePod(nil, []api.Container{makeContainer(nil, tc.expectLifecycleHandler.DeepCopy())}, nil) dropDisabledFields(newPod, nil, oldPod, nil) if diff := cmp.Diff(expectPod, newPod); diff != "" { t.Fatalf("Unexpected modification to new pod; diff (-got +want)\n%s", diff) } } // EphemeralContainer { oldPod := makePod(nil, nil, []api.EphemeralContainer{makeEphemeralContainer(nil, tc.oldLifecycleHandler.DeepCopy())}) newPod := makePod(nil, nil, []api.EphemeralContainer{makeEphemeralContainer(nil, tc.newLifecycleHandler.DeepCopy())}) expectPod := makePod(nil, nil, []api.EphemeralContainer{makeEphemeralContainer(nil, tc.expectLifecycleHandler.DeepCopy())}) dropDisabledFields(newPod, nil, oldPod, nil) if diff := cmp.Diff(expectPod, newPod); diff != "" { t.Fatalf("Unexpected modification to new pod; diff (-got +want)\n%s", diff) } } }) } } func TestDropContainerStopSignals(t *testing.T) { makeContainer := func(lifecycle *api.Lifecycle) api.Container { container := api.Container{Name: "foo"} if lifecycle != nil { container.Lifecycle = lifecycle } return container } makeEphemeralContainer := func(lifecycle *api.Lifecycle) api.EphemeralContainer { container := api.EphemeralContainer{ EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "foo"}, } if lifecycle != nil { container.Lifecycle = lifecycle } return container } makePod := func(os api.OSName, containers []api.Container, initContainers []api.Container, ephemeralContainers []api.EphemeralContainer) *api.PodSpec { return &api.PodSpec{ OS: &api.PodOS{Name: os}, Containers: containers, InitContainers: initContainers, EphemeralContainers: ephemeralContainers, } } testCases := []struct { featuregateEnabled bool oldLifecycle *api.Lifecycle newLifecycle *api.Lifecycle expectedLifecycle *api.Lifecycle }{ // feature gate is turned on and stopsignal is not in use - Lifecycle stays nil { featuregateEnabled: true, oldLifecycle: nil, newLifecycle: nil, expectedLifecycle: nil, }, // feature gate is turned off and StopSignal is in use - StopSignal is not dropped { featuregateEnabled: false, oldLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM)}, newLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM)}, expectedLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM)}, }, // feature gate is turned off and StopSignal is not in use - Entire lifecycle is dropped { featuregateEnabled: false, oldLifecycle: &api.Lifecycle{StopSignal: nil}, newLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM)}, expectedLifecycle: nil, }, // feature gate is turned on and StopSignal is in use - StopSignal is not dropped { featuregateEnabled: true, oldLifecycle: &api.Lifecycle{StopSignal: nil}, newLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM)}, expectedLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM)}, }, // feature gate is turned off and PreStop is in use - StopSignal alone is dropped { featuregateEnabled: false, oldLifecycle: &api.Lifecycle{StopSignal: nil, PreStop: &api.LifecycleHandler{ Exec: &api.ExecAction{Command: []string{"foo"}}, }}, newLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM), PreStop: &api.LifecycleHandler{ Exec: &api.ExecAction{Command: []string{"foo"}}, }}, expectedLifecycle: &api.Lifecycle{StopSignal: nil, PreStop: &api.LifecycleHandler{ Exec: &api.ExecAction{Command: []string{"foo"}}, }}, }, // feature gate is turned on and PreStop is in use - StopSignal is not dropped { featuregateEnabled: true, oldLifecycle: &api.Lifecycle{StopSignal: nil, PreStop: &api.LifecycleHandler{ Exec: &api.ExecAction{Command: []string{"foo"}}, }}, newLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM), PreStop: &api.LifecycleHandler{ Exec: &api.ExecAction{Command: []string{"foo"}}, }}, expectedLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM), PreStop: &api.LifecycleHandler{ Exec: &api.ExecAction{Command: []string{"foo"}}, }}, }, // feature gate is turned off and PreStop and StopSignal are in use - nothing is dropped { featuregateEnabled: true, oldLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM), PreStop: &api.LifecycleHandler{ Exec: &api.ExecAction{Command: []string{"foo"}}, }}, newLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM), PreStop: &api.LifecycleHandler{ Exec: &api.ExecAction{Command: []string{"foo"}}, }}, expectedLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM), PreStop: &api.LifecycleHandler{ Exec: &api.ExecAction{Command: []string{"foo"}}, }}, }, } for i, tc := range testCases { t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ContainerStopSignals, tc.featuregateEnabled) // Containers { oldPod := makePod(api.Linux, []api.Container{makeContainer(tc.oldLifecycle.DeepCopy())}, nil, nil) newPod := makePod(api.Linux, []api.Container{makeContainer(tc.newLifecycle.DeepCopy())}, nil, nil) expectedPod := makePod(api.Linux, []api.Container{makeContainer(tc.expectedLifecycle.DeepCopy())}, nil, nil) dropDisabledFields(newPod, nil, oldPod, nil) if diff := cmp.Diff(expectedPod, newPod); diff != "" { t.Fatalf("Unexpected modification to new pod; diff (-got +want)\n%s", diff) } } // InitContainers { oldPod := makePod(api.Linux, nil, []api.Container{makeContainer(tc.oldLifecycle.DeepCopy())}, nil) newPod := makePod(api.Linux, nil, []api.Container{makeContainer(tc.newLifecycle.DeepCopy())}, nil) expectPod := makePod(api.Linux, nil, []api.Container{makeContainer(tc.expectedLifecycle.DeepCopy())}, nil) dropDisabledFields(newPod, nil, oldPod, nil) if diff := cmp.Diff(expectPod, newPod); diff != "" { t.Fatalf("Unexpected modification to new pod; diff (-got +want)\n%s", diff) } } // EphemeralContainers { oldPod := makePod(api.Linux, nil, nil, []api.EphemeralContainer{makeEphemeralContainer(tc.oldLifecycle.DeepCopy())}) newPod := makePod(api.Linux, nil, nil, []api.EphemeralContainer{makeEphemeralContainer(tc.newLifecycle.DeepCopy())}) expectPod := makePod(api.Linux, nil, nil, []api.EphemeralContainer{makeEphemeralContainer(tc.expectedLifecycle.DeepCopy())}) dropDisabledFields(newPod, nil, oldPod, nil) if diff := cmp.Diff(expectPod, newPod); diff != "" { t.Fatalf("Unexpected modification to new pod; diff (-got +want)\n%s", diff) } } }) } } func TestDropSupplementalGroupsPolicy(t *testing.T) { supplementalGroupsPolicyMerge := api.SupplementalGroupsPolicyMerge podWithSupplementalGroupsPolicy := func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{ SecurityContext: &api.PodSecurityContext{ SupplementalGroupsPolicy: &supplementalGroupsPolicyMerge, }, Containers: []api.Container{ { Name: "c1", Image: "image", }, }, }, Status: api.PodStatus{ ContainerStatuses: []api.ContainerStatus{ { Name: "c1", Image: "image", User: &api.ContainerUser{ Linux: &api.LinuxContainerUser{ UID: 0, GID: 0, SupplementalGroups: []int64{0, 1000}, }, }, }, }, }, } } podWithoutSupplementalGroupsPolicy := func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{ SecurityContext: &api.PodSecurityContext{}, Containers: []api.Container{ { Name: "c1", Image: "image", }, }, }, Status: api.PodStatus{ ContainerStatuses: []api.ContainerStatus{ { Name: "c1", Image: "image", }, }, }, } } podInfo := []struct { description string hasSupplementalGroupsPolicy bool pod func() *api.Pod }{ { description: "with SupplementalGroupsPolicy and User", hasSupplementalGroupsPolicy: true, pod: podWithSupplementalGroupsPolicy, }, { description: "without SupplementalGroupsPolicy and User", hasSupplementalGroupsPolicy: false, pod: podWithoutSupplementalGroupsPolicy, }, { description: "is nil", hasSupplementalGroupsPolicy: false, pod: func() *api.Pod { return nil }, }, } for _, enabled := range []bool{true, false} { for _, oldPodInfo := range podInfo { for _, newPodInfo := range podInfo { oldPodHasSupplementalGroupsPolicy, oldPod := oldPodInfo.hasSupplementalGroupsPolicy, oldPodInfo.pod() newPodHasSupplementalGroupsPolicy, newPod := newPodInfo.hasSupplementalGroupsPolicy, newPodInfo.pod() if newPod == nil { continue } t.Run( fmt.Sprintf( "feature enabled=%v, old pod %v, new pod %v", enabled, oldPodInfo.description, newPodInfo.description, ), func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SupplementalGroupsPolicy, enabled) var oldPodSpec *api.PodSpec var oldPodStatus *api.PodStatus if oldPod != nil { oldPodSpec = &oldPod.Spec oldPodStatus = &oldPod.Status } dropDisabledFields(&newPod.Spec, nil, oldPodSpec, nil) dropDisabledPodStatusFields(&newPod.Status, oldPodStatus, &newPod.Spec, oldPodSpec) // old pod should never be changed if !reflect.DeepEqual(oldPod, oldPodInfo.pod()) { t.Errorf("old pod changed: %v", cmp.Diff(oldPod, oldPodInfo.pod())) } switch { case enabled || oldPodHasSupplementalGroupsPolicy: // new pod shouldn't change if feature enabled or if old pod has SupplementalGroupsPolicy and User set if !reflect.DeepEqual(newPod, newPodInfo.pod()) { t.Errorf("new pod changed: %v", cmp.Diff(newPod, newPodInfo.pod())) } case newPodHasSupplementalGroupsPolicy: // new pod should be changed if reflect.DeepEqual(newPod, newPodInfo.pod()) { t.Errorf("new pod was not changed") } // new pod should not have SupplementalGroupsPolicy and User fields if !reflect.DeepEqual(newPod, podWithoutSupplementalGroupsPolicy()) { t.Errorf("new pod has SupplementalGroups and User: %v", cmp.Diff(newPod, podWithoutSupplementalGroupsPolicy())) } default: // new pod should not need to be changed if !reflect.DeepEqual(newPod, newPodInfo.pod()) { t.Errorf("new pod changed: %v", cmp.Diff(newPod, newPodInfo.pod())) } } }, ) } } } } func TestDropImageVolumes(t *testing.T) { const ( volumeNameImage = "volume" volumeNameOther = "volume-other" ) anotherVolume := api.Volume{Name: volumeNameOther, VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{}}} podWithVolume := &api.Pod{ Spec: api.PodSpec{ Volumes: []api.Volume{ {Name: volumeNameImage, VolumeSource: api.VolumeSource{Image: &api.ImageVolumeSource{}}}, anotherVolume, }, Containers: []api.Container{{ VolumeMounts: []api.VolumeMount{{Name: volumeNameImage}, {Name: volumeNameOther}}, }}, InitContainers: []api.Container{{ VolumeMounts: []api.VolumeMount{{Name: volumeNameImage}}, }}, EphemeralContainers: []api.EphemeralContainer{ {EphemeralContainerCommon: api.EphemeralContainerCommon{ VolumeMounts: []api.VolumeMount{{Name: volumeNameImage}}, }}, }, }, } podWithoutVolume := &api.Pod{ Spec: api.PodSpec{ Volumes: []api.Volume{anotherVolume}, Containers: []api.Container{{VolumeMounts: []api.VolumeMount{{Name: volumeNameOther}}}}, InitContainers: []api.Container{{}}, EphemeralContainers: []api.EphemeralContainer{{}}, }, } noPod := &api.Pod{} testcases := []struct { description string enabled bool oldPod *api.Pod newPod *api.Pod wantPod *api.Pod }{ { description: "old with volume / new with volume / disabled", oldPod: podWithVolume, newPod: podWithVolume, wantPod: podWithVolume, }, { description: "old without volume / new with volume / disabled", oldPod: podWithoutVolume, newPod: podWithVolume, wantPod: podWithoutVolume, }, { description: "no old pod/ new with volume / disabled", oldPod: noPod, newPod: podWithVolume, wantPod: podWithoutVolume, }, { description: "nil old pod/ new with volume / disabled", oldPod: nil, newPod: podWithVolume, wantPod: podWithoutVolume, }, { description: "old with volume / new without volume / disabled", oldPod: podWithVolume, newPod: podWithoutVolume, wantPod: podWithoutVolume, }, { description: "old without volume / new without volume / disabled", oldPod: podWithoutVolume, newPod: podWithoutVolume, wantPod: podWithoutVolume, }, { description: "no old pod/ new without volume / disabled", oldPod: noPod, newPod: podWithoutVolume, wantPod: podWithoutVolume, }, { description: "old with volume / new with volume / enabled", enabled: true, oldPod: podWithVolume, newPod: podWithVolume, wantPod: podWithVolume, }, { description: "old without volume / new with volume / enabled", enabled: true, oldPod: podWithoutVolume, newPod: podWithVolume, wantPod: podWithVolume, }, { description: "no old pod/ new with volume / enabled", enabled: true, oldPod: noPod, newPod: podWithVolume, wantPod: podWithVolume, }, { description: "old with volume / new without volume / enabled", enabled: true, oldPod: podWithVolume, newPod: podWithoutVolume, wantPod: podWithoutVolume, }, { description: "old without volume / new without volume / enabled", enabled: true, oldPod: podWithoutVolume, newPod: podWithoutVolume, wantPod: podWithoutVolume, }, { description: "no old pod/ new without volume / enabled", enabled: true, oldPod: noPod, newPod: podWithoutVolume, wantPod: podWithoutVolume, }, } for _, tc := range testcases { t.Run(tc.description, func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ImageVolume, tc.enabled) oldPod := tc.oldPod.DeepCopy() newPod := tc.newPod.DeepCopy() wantPod := tc.wantPod DropDisabledPodFields(newPod, oldPod) // old pod should never be changed if diff := cmp.Diff(oldPod, tc.oldPod); diff != "" { t.Errorf("old pod changed: %s", diff) } if diff := cmp.Diff(wantPod, newPod); diff != "" { t.Errorf("new pod changed (- want, + got): %s", diff) } }) } } func TestDropSELinuxChangePolicy(t *testing.T) { podRecursive := &api.Pod{ Spec: api.PodSpec{ SecurityContext: &api.PodSecurityContext{ SELinuxChangePolicy: ptr.To(api.SELinuxChangePolicyRecursive), }, }, } podMountOption := &api.Pod{ Spec: api.PodSpec{ SecurityContext: &api.PodSecurityContext{ SELinuxChangePolicy: ptr.To(api.SELinuxChangePolicyMountOption), }, }, } podNull := &api.Pod{ Spec: api.PodSpec{ SecurityContext: &api.PodSecurityContext{ SELinuxChangePolicy: nil, }, }, } tests := []struct { name string oldPod *api.Pod newPod *api.Pod gates []featuregate.Feature wantPod *api.Pod }{ { name: "no old pod, new pod with Recursive, all features disabled", oldPod: nil, newPod: podRecursive, gates: nil, wantPod: podNull, }, { name: "no old pod, new pod with MountOption, all features disabled", oldPod: nil, newPod: podMountOption, gates: nil, wantPod: podNull, }, { name: "old pod with Recursive, new pod with Recursive, all features disabled", oldPod: podRecursive, newPod: podRecursive, gates: nil, wantPod: podRecursive, }, { name: "old pod with MountOption, new pod with Recursive, all features disabled", oldPod: podMountOption, newPod: podRecursive, gates: nil, wantPod: podRecursive, }, { name: "no old pod, new pod with Recursive, SELinuxChangePolicy feature enabled", oldPod: nil, newPod: podRecursive, gates: []featuregate.Feature{features.SELinuxChangePolicy}, wantPod: podRecursive, }, { name: "no old pod, new pod with MountOption, SELinuxChangePolicy feature enabled", oldPod: nil, newPod: podMountOption, gates: []featuregate.Feature{features.SELinuxChangePolicy}, wantPod: podMountOption, }, { name: "old pod with Recursive, new pod with Recursive, SELinuxChangePolicy feature enabled", oldPod: podRecursive, newPod: podRecursive, gates: []featuregate.Feature{features.SELinuxChangePolicy}, wantPod: podRecursive, }, { name: "old pod with MountOption, new pod with Recursive, SELinuxChangePolicy feature enabled", oldPod: podMountOption, newPod: podRecursive, gates: []featuregate.Feature{features.SELinuxChangePolicy}, wantPod: podRecursive, }, // In theory, SELinuxMount does not have any effect on dropping SELinuxChangePolicy field, but for completeness: { name: "no old pod, new pod with Recursive, SELinuxChangePolicy + SELinuxMount features enabled", oldPod: nil, newPod: podRecursive, gates: []featuregate.Feature{features.SELinuxChangePolicy, features.SELinuxMount}, wantPod: podRecursive, }, { name: "no old pod, new pod with MountOption, SELinuxChangePolicy + SELinuxMount features enabled", oldPod: nil, newPod: podMountOption, gates: []featuregate.Feature{features.SELinuxChangePolicy, features.SELinuxMount}, wantPod: podMountOption, }, { name: "old pod with Recursive, new pod with Recursive, SELinuxChangePolicy + SELinuxMount features enabled", oldPod: podRecursive, newPod: podRecursive, gates: []featuregate.Feature{features.SELinuxChangePolicy, features.SELinuxMount}, wantPod: podRecursive, }, { name: "old pod with MountOption, new pod with Recursive, SELinuxChangePolicy + SELinuxMount features enabled", oldPod: podMountOption, newPod: podRecursive, gates: []featuregate.Feature{features.SELinuxChangePolicy, features.SELinuxMount}, wantPod: podRecursive, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Set feature gates for the test. *Disable* those that are not in tc.gates. allGates := []featuregate.Feature{features.SELinuxChangePolicy, features.SELinuxMount} enabledGates := sets.New(tc.gates...) for _, gate := range allGates { enable := enabledGates.Has(gate) featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, gate, enable) } oldPod := tc.oldPod.DeepCopy() newPod := tc.newPod.DeepCopy() DropDisabledPodFields(newPod, oldPod) // old pod should never be changed if diff := cmp.Diff(oldPod, tc.oldPod); diff != "" { t.Errorf("old pod changed: %s", diff) } if diff := cmp.Diff(tc.wantPod, newPod); diff != "" { t.Errorf("new pod changed (- want, + got): %s", diff) } }) } } func TestValidateAllowSidecarResizePolicy(t *testing.T) { restartPolicyAlways := api.ContainerRestartPolicyAlways testCases := []struct { name string oldPodSpec *api.PodSpec wantOption bool }{ { name: "old pod spec is nil", wantOption: false, }, { name: "one sidecar container + one regular init container, no resize policy set on any of them", oldPodSpec: &api.PodSpec{ InitContainers: []api.Container{ { Name: "c1-restartable-init", Image: "image", RestartPolicy: &restartPolicyAlways, }, { Name: "c1-init", Image: "image", }, }, }, wantOption: false, }, { name: "one sidecar container + one regular init container, resize policy set on regular init container", oldPodSpec: &api.PodSpec{ InitContainers: []api.Container{ { Name: "c1-restartable-init", Image: "image", RestartPolicy: &restartPolicyAlways, }, { Name: "c1-init", Image: "image", ResizePolicy: []api.ContainerResizePolicy{ {ResourceName: api.ResourceCPU, RestartPolicy: api.NotRequired}, }, }, }, }, wantOption: false, }, { name: "one sidecar container + one regular init container, resize policy set on sidecar container", oldPodSpec: &api.PodSpec{ InitContainers: []api.Container{ { Name: "c1-restartable-init", Image: "image", RestartPolicy: &restartPolicyAlways, ResizePolicy: []api.ContainerResizePolicy{ {ResourceName: api.ResourceCPU, RestartPolicy: api.NotRequired}, }, }, { Name: "c1-init", Image: "image", }, }, }, wantOption: true, }, { name: "one sidecar container + one regular init container, resize policy set on both of them", oldPodSpec: &api.PodSpec{ InitContainers: []api.Container{ { Name: "c1-restartable-init", Image: "image", RestartPolicy: &restartPolicyAlways, ResizePolicy: []api.ContainerResizePolicy{ {ResourceName: api.ResourceCPU, RestartPolicy: api.NotRequired}, }, }, { Name: "c1-init", Image: "image", ResizePolicy: []api.ContainerResizePolicy{ {ResourceName: api.ResourceCPU, RestartPolicy: api.NotRequired}, }, }, }, }, wantOption: true, }, { name: "two sidecar containers, resize policy set on one of them", oldPodSpec: &api.PodSpec{ InitContainers: []api.Container{ { Name: "c1-restartable-init", Image: "image", RestartPolicy: &restartPolicyAlways, ResizePolicy: []api.ContainerResizePolicy{ {ResourceName: api.ResourceCPU, RestartPolicy: api.NotRequired}, }, }, { Name: "c2-restartable-init", Image: "image", RestartPolicy: &restartPolicyAlways, }, }, }, wantOption: true, }, { name: "two regular init containers, resize policy set on both of them", oldPodSpec: &api.PodSpec{ InitContainers: []api.Container{ { Name: "c1-init", Image: "image", ResizePolicy: []api.ContainerResizePolicy{ {ResourceName: api.ResourceCPU, RestartPolicy: api.NotRequired}, }, }, { Name: "c2-init", Image: "image", ResizePolicy: []api.ContainerResizePolicy{ {ResourceName: api.ResourceCPU, RestartPolicy: api.NotRequired}, }, }, }, }, wantOption: false, }, { name: "two non-init containers, resize policy set on both of them", oldPodSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "c1", Image: "image", ResizePolicy: []api.ContainerResizePolicy{ {ResourceName: api.ResourceCPU, RestartPolicy: api.NotRequired}, }, }, { Name: "c2", Image: "image", ResizePolicy: []api.ContainerResizePolicy{ {ResourceName: api.ResourceCPU, RestartPolicy: api.NotRequired}, }, }, }, }, wantOption: false, }, } for _, tc := range testCases { for _, ippvsEnabled := range []bool{true, false} { t.Run(fmt.Sprintf("%s/%t", tc.name, ippvsEnabled), func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodVerticalScaling, ippvsEnabled) gotOptions := GetValidationOptionsFromPodSpecAndMeta(&api.PodSpec{}, tc.oldPodSpec, nil, nil) expected := tc.wantOption || ippvsEnabled assert.Equal(t, expected, gotOptions.AllowSidecarResizePolicy, "AllowSidecarResizePolicy") }) } } } func TestValidateInvalidLabelValueInNodeSelectorOption(t *testing.T) { testCases := []struct { name string oldPodSpec *api.PodSpec wantOption bool }{ { name: "Create", wantOption: false, }, { name: "UpdateInvalidLabelSelector", oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ NodeAffinity: &api.NodeAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: &api.NodeSelector{ NodeSelectorTerms: []api.NodeSelectorTerm{{ MatchExpressions: []api.NodeSelectorRequirement{{ Key: "foo", Operator: api.NodeSelectorOpIn, Values: []string{"-1"}, }}, }}, }, }, }, }, wantOption: true, }, { name: "UpdateValidLabelSelector", oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ NodeAffinity: &api.NodeAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: &api.NodeSelector{ NodeSelectorTerms: []api.NodeSelectorTerm{{ MatchExpressions: []api.NodeSelectorRequirement{{ Key: "foo", Operator: api.NodeSelectorOpIn, Values: []string{"bar"}, }}, }}, }, }, }, }, wantOption: false, }, { name: "UpdateEmptyLabelSelector", oldPodSpec: &api.PodSpec{ Affinity: &api.Affinity{ NodeAffinity: &api.NodeAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: &api.NodeSelector{ NodeSelectorTerms: []api.NodeSelectorTerm{}, }, }, }, }, wantOption: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Pod meta doesn't impact the outcome. gotOptions := GetValidationOptionsFromPodSpecAndMeta(&api.PodSpec{}, tc.oldPodSpec, nil, nil) if tc.wantOption != gotOptions.AllowInvalidLabelValueInRequiredNodeAffinity { t.Errorf("Got AllowInvalidLabelValueInRequiredNodeAffinity=%t, want %t", gotOptions.AllowInvalidLabelValueInRequiredNodeAffinity, tc.wantOption) } }) } } func TestValidateAllowPodLifecycleSleepActionZeroValue(t *testing.T) { testCases := []struct { name string podSpec *api.PodSpec featureEnabled bool expectAllowPodLifecycleSleepActionZeroValue bool }{ { name: "no lifecycle hooks", podSpec: &api.PodSpec{}, featureEnabled: true, expectAllowPodLifecycleSleepActionZeroValue: true, }, { name: "Prestop with non-zero second duration", podSpec: &api.PodSpec{ Containers: []api.Container{ { Lifecycle: &api.Lifecycle{ PreStop: &api.LifecycleHandler{ Sleep: &api.SleepAction{ Seconds: 1, }, }, }, }, }, }, featureEnabled: true, expectAllowPodLifecycleSleepActionZeroValue: true, }, { name: "PostStart with non-zero second duration", podSpec: &api.PodSpec{ Containers: []api.Container{ { Lifecycle: &api.Lifecycle{ PostStart: &api.LifecycleHandler{ Sleep: &api.SleepAction{ Seconds: 1, }, }, }, }, }, }, featureEnabled: true, expectAllowPodLifecycleSleepActionZeroValue: true, }, { name: "PreStop with zero seconds", podSpec: &api.PodSpec{ Containers: []api.Container{ { Lifecycle: &api.Lifecycle{ PreStop: &api.LifecycleHandler{ Sleep: &api.SleepAction{ Seconds: 0, }, }, }, }, }, }, featureEnabled: true, expectAllowPodLifecycleSleepActionZeroValue: true, }, { name: "PostStart with zero seconds", podSpec: &api.PodSpec{ Containers: []api.Container{ { Lifecycle: &api.Lifecycle{ PostStart: &api.LifecycleHandler{ Sleep: &api.SleepAction{ Seconds: 0, }, }, }, }, }, }, featureEnabled: true, expectAllowPodLifecycleSleepActionZeroValue: true, }, { name: "no lifecycle hooks with feature gate disabled", podSpec: &api.PodSpec{}, featureEnabled: false, expectAllowPodLifecycleSleepActionZeroValue: false, }, { name: "Prestop with non-zero second duration with feature gate disabled", podSpec: &api.PodSpec{ Containers: []api.Container{ { Lifecycle: &api.Lifecycle{ PreStop: &api.LifecycleHandler{ Sleep: &api.SleepAction{ Seconds: 1, }, }, }, }, }, }, featureEnabled: false, expectAllowPodLifecycleSleepActionZeroValue: false, }, { name: "PostStart with non-zero second duration with feature gate disabled", podSpec: &api.PodSpec{ Containers: []api.Container{ { Lifecycle: &api.Lifecycle{ PostStart: &api.LifecycleHandler{ Sleep: &api.SleepAction{ Seconds: 1, }, }, }, }, }, }, featureEnabled: false, expectAllowPodLifecycleSleepActionZeroValue: false, }, { name: "PreStop with zero seconds with feature gate disabled", podSpec: &api.PodSpec{ Containers: []api.Container{ { Lifecycle: &api.Lifecycle{ PreStop: &api.LifecycleHandler{ Sleep: &api.SleepAction{ Seconds: 0, }, }, }, }, }, }, featureEnabled: false, expectAllowPodLifecycleSleepActionZeroValue: true, }, { name: "PostStart with zero seconds with feature gate disabled", podSpec: &api.PodSpec{ Containers: []api.Container{ { Lifecycle: &api.Lifecycle{ PostStart: &api.LifecycleHandler{ Sleep: &api.SleepAction{ Seconds: 0, }, }, }, }, }, }, featureEnabled: false, expectAllowPodLifecycleSleepActionZeroValue: true, }, } featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.33")) for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodLifecycleSleepActionAllowZero, tc.featureEnabled) gotOptions := GetValidationOptionsFromPodSpecAndMeta(&api.PodSpec{}, tc.podSpec, nil, nil) assert.Equal(t, tc.expectAllowPodLifecycleSleepActionZeroValue, gotOptions.AllowPodLifecycleSleepActionZeroValue, "AllowPodLifecycleSleepActionZeroValue") }) } } func TestHasAPIReferences(t *testing.T) { tests := []struct { name string pod *api.Pod expectRejection bool resource string }{ { name: "Empty ServiceAccount in Static Pod", pod: &api.Pod{Spec: api.PodSpec{}}, expectRejection: false, }, { name: "Non empty ServiceAccount", pod: &api.Pod{Spec: api.PodSpec{ServiceAccountName: "default"}}, expectRejection: true, resource: "serviceaccounts", }, { name: "Empty Volume list", pod: &api.Pod{Spec: api.PodSpec{Volumes: nil}}, expectRejection: false, }, { name: "Non empty volume list with HostPath volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-hostpath", VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with EmptyDir volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-emptydir", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with Secret volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-secret", VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{}}}, }}}, expectRejection: true, resource: "secrets (via secret volumes)", }, { name: "Non empty volume list with ConfigMap volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-configmap", VolumeSource: api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{}}}, }}}, expectRejection: true, resource: "configmaps (via configmap volumes)", }, { name: "Non empty volume list with GCEPersistentDisk volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-gce", VolumeSource: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with AWSElasticBlockStore volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-aws", VolumeSource: api.VolumeSource{AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with GitRepo volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-gitrepo", VolumeSource: api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with NFS volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-nfs", VolumeSource: api.VolumeSource{NFS: &api.NFSVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with ISCSI volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-iscsi", VolumeSource: api.VolumeSource{ISCSI: &api.ISCSIVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with Glusterfs volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-glusterfs", VolumeSource: api.VolumeSource{Glusterfs: &api.GlusterfsVolumeSource{}}}, }}}, expectRejection: true, resource: "endpoints (via glusterFS volumes)", }, { name: "Non empty volume list with PersistentVolumeClaim", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-pvc", VolumeSource: api.VolumeSource{PersistentVolumeClaim: &api.PersistentVolumeClaimVolumeSource{}}}, }}}, expectRejection: true, resource: "persistentvolumeclaims", }, { name: "Non empty volume list with RBD volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-rbd", VolumeSource: api.VolumeSource{RBD: &api.RBDVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with FlexVolume volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-flexvolume", VolumeSource: api.VolumeSource{FlexVolume: &api.FlexVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with Cinder volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-cinder", VolumeSource: api.VolumeSource{Cinder: &api.CinderVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with CephFS volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-cephfs", VolumeSource: api.VolumeSource{CephFS: &api.CephFSVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with Flocker volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-flocker", VolumeSource: api.VolumeSource{Flocker: &api.FlockerVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with DownwardAPI volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-downwardapi", VolumeSource: api.VolumeSource{DownwardAPI: &api.DownwardAPIVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with FC volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-fc", VolumeSource: api.VolumeSource{FC: &api.FCVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with AzureFile volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-azurefile", VolumeSource: api.VolumeSource{AzureFile: &api.AzureFileVolumeSource{}}}, }}}, expectRejection: true, resource: "secrets (via azureFile volumes)", }, { name: "Non empty volume list with VsphereVolume volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-vsphere", VolumeSource: api.VolumeSource{VsphereVolume: &api.VsphereVirtualDiskVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with Quobyte volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-quobyte", VolumeSource: api.VolumeSource{Quobyte: &api.QuobyteVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with AzureDisk volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-azuredisk", VolumeSource: api.VolumeSource{AzureDisk: &api.AzureDiskVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with PhotonPersistentDisk volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-photon", VolumeSource: api.VolumeSource{PhotonPersistentDisk: &api.PhotonPersistentDiskVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with Projected volume with clustertrustbundles", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-projected", VolumeSource: api.VolumeSource{Projected: &api.ProjectedVolumeSource{Sources: []api.VolumeProjection{{ClusterTrustBundle: &api.ClusterTrustBundleProjection{}}}}}}, }}}, expectRejection: true, resource: "clustertrustbundles", }, { name: "Non empty volume list with Projected volume with podcertificates", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-projected", VolumeSource: api.VolumeSource{Projected: &api.ProjectedVolumeSource{Sources: []api.VolumeProjection{{PodCertificate: &api.PodCertificateProjection{}}}}}}, }}}, expectRejection: true, resource: "podcertificates", }, { name: "Non empty volume list with Projected volume with secrets", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-projected", VolumeSource: api.VolumeSource{Projected: &api.ProjectedVolumeSource{Sources: []api.VolumeProjection{{Secret: &api.SecretProjection{}}}}}}, }}}, expectRejection: true, resource: "secrets (via projected volumes)", }, { name: "Non empty volume list with Projected volume with configmap", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-projected", VolumeSource: api.VolumeSource{Projected: &api.ProjectedVolumeSource{Sources: []api.VolumeProjection{{ConfigMap: &api.ConfigMapProjection{}}}}}}, }}}, expectRejection: true, resource: "configmaps (via projected volumes)", }, { name: "Non empty volume list with Projected volume with serviceaccounttoken", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-projected", VolumeSource: api.VolumeSource{Projected: &api.ProjectedVolumeSource{Sources: []api.VolumeProjection{{ServiceAccountToken: &api.ServiceAccountTokenProjection{}}}}}}, }}}, expectRejection: true, resource: "serviceaccounts (via projected volumes)", }, { name: "Non empty volume list with Projected volume with downwardapi", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-projected", VolumeSource: api.VolumeSource{Projected: &api.ProjectedVolumeSource{Sources: []api.VolumeProjection{{DownwardAPI: &api.DownwardAPIProjection{}}}}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with Portworx volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-portworx", VolumeSource: api.VolumeSource{PortworxVolume: &api.PortworxVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with ScaleIO volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-scaleio", VolumeSource: api.VolumeSource{ScaleIO: &api.ScaleIOVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with StorageOS volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-storageos", VolumeSource: api.VolumeSource{StorageOS: &api.StorageOSVolumeSource{}}}, }}}, expectRejection: false, }, { name: "Non empty volume list with CSI volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-csi", VolumeSource: api.VolumeSource{CSI: &api.CSIVolumeSource{}}}, }}}, expectRejection: true, resource: "csidrivers (via CSI volumes)", }, { name: "Non empty volume list with Ephemeral volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-ephemeral", VolumeSource: api.VolumeSource{Ephemeral: &api.EphemeralVolumeSource{}}}, }}}, expectRejection: true, resource: "persistentvolumeclaims (via ephemeral volumes)", }, { name: "Non empty volume list with Image volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-image", VolumeSource: api.VolumeSource{Image: &api.ImageVolumeSource{}}}, }}}, expectRejection: false, }, { name: "No envs", pod: &api.Pod{Spec: api.PodSpec{}}, expectRejection: false, }, { name: "Non empty Env with value", pod: &api.Pod{Spec: api.PodSpec{ Containers: []api.Container{ { Name: "test-container", Env: []api.EnvVar{ { Name: "test-env", Value: "TEST_ENV_VAL", }, }, }, }, }}, expectRejection: false, }, { name: "Non empty EnvFrom with ConfigMap", pod: &api.Pod{Spec: api.PodSpec{ Containers: []api.Container{ { Name: "test-container", EnvFrom: []api.EnvFromSource{ {ConfigMapRef: &api.ConfigMapEnvSource{LocalObjectReference: api.LocalObjectReference{Name: "test"}}}, }, }, }, }}, expectRejection: true, resource: "configmaps", }, { name: "Non empty EnvFrom with Secret", pod: &api.Pod{Spec: api.PodSpec{ Containers: []api.Container{ { Name: "test-container", EnvFrom: []api.EnvFromSource{ {SecretRef: &api.SecretEnvSource{LocalObjectReference: api.LocalObjectReference{Name: "test"}}}, }, }, }, }}, expectRejection: true, resource: "secrets", }, { name: "Non empty Env with ConfigMap", pod: &api.Pod{Spec: api.PodSpec{ Containers: []api.Container{ { Name: "test-container", Env: []api.EnvVar{ { Name: "test-env", ValueFrom: &api.EnvVarSource{ConfigMapKeyRef: &api.ConfigMapKeySelector{LocalObjectReference: api.LocalObjectReference{Name: "test"}}}, }, }, }, }, }}, expectRejection: true, resource: "configmaps", }, { name: "Non empty Env with Secret", pod: &api.Pod{Spec: api.PodSpec{ Containers: []api.Container{ { Name: "test-container", Env: []api.EnvVar{ { Name: "test-env", ValueFrom: &api.EnvVarSource{SecretKeyRef: &api.SecretKeySelector{LocalObjectReference: api.LocalObjectReference{Name: "test"}}}, }, }, }, }, }}, expectRejection: true, resource: "secrets", }, { name: "Multiple volume list where invalid volume comes after valid volume source", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-portworx", VolumeSource: api.VolumeSource{PortworxVolume: &api.PortworxVolumeSource{}}}, {Name: "test-volume-configmap", VolumeSource: api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{}}}, }}}, expectRejection: true, resource: "configmaps (via configmap volumes)", }, { name: "Multiple volume list where invalid configmap volume comes after valid downwardapi projected volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-projected", VolumeSource: api.VolumeSource{Projected: &api.ProjectedVolumeSource{Sources: []api.VolumeProjection{{DownwardAPI: &api.DownwardAPIProjection{}}}}}}, {Name: "test-volume-configmap", VolumeSource: api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{}}}, }}}, expectRejection: true, resource: "configmaps (via configmap volumes)", }, { name: "Multiple volume list where invalid configmap projected volume comes after valid downwardapi projected volume", pod: &api.Pod{Spec: api.PodSpec{Volumes: []api.Volume{ {Name: "test-volume-projected", VolumeSource: api.VolumeSource{Projected: &api.ProjectedVolumeSource{Sources: []api.VolumeProjection{{DownwardAPI: &api.DownwardAPIProjection{}}}}}}, {Name: "test-volume-projected", VolumeSource: api.VolumeSource{Projected: &api.ProjectedVolumeSource{Sources: []api.VolumeProjection{{ConfigMap: &api.ConfigMapProjection{}}}}}}, }}}, expectRejection: true, resource: "configmaps (via projected volumes)", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { actualResult, resource, _ := HasAPIObjectReference(test.pod) if test.expectRejection != actualResult || resource != test.resource { t.Errorf("unexpected result, expected %v but got %v, expected resource %v, but got %v", test.expectRejection, actualResult, test.resource, resource) } }) } } func TestDropHostnameOverride(t *testing.T) { podWithoutHostnameOverride := func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{}, } } podWithHostnameOverride := func() *api.Pod { return &api.Pod{ Spec: api.PodSpec{ HostnameOverride: ptr.To("custom-hostname"), }, } } oldPodInfo := []struct { description string hasHostnameOverride bool pod func() *api.Pod }{ { description: "with HostnameOverride=true", hasHostnameOverride: true, pod: podWithHostnameOverride, }, { description: "with HostnameOverride=nil", hasHostnameOverride: false, pod: podWithoutHostnameOverride, }, } newPodInfo := []struct { description string hasHostnameOverride bool pod func() *api.Pod }{ { description: "with HostnameOverride=true", hasHostnameOverride: true, pod: podWithHostnameOverride, }, { description: "with HostnameOverride=nil", hasHostnameOverride: false, pod: podWithoutHostnameOverride, }, } for _, enabled := range []bool{true, false} { for _, oldPodInfo := range oldPodInfo { for _, newPodInfo := range newPodInfo { oldPodHasHostnameOverride, oldPod := oldPodInfo.hasHostnameOverride, oldPodInfo.pod() newPodHasHostnameOverride, newPod := newPodInfo.hasHostnameOverride, newPodInfo.pod() t.Run(fmt.Sprintf("feature enabled=%v, old pod %v, new pod %v", enabled, oldPodInfo.description, newPodInfo.description), func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HostnameOverride, enabled) DropDisabledPodFields(newPod, oldPod) if !reflect.DeepEqual(oldPod, oldPodInfo.pod()) { t.Errorf("old pod changed: %v", cmp.Diff(oldPod, oldPodInfo.pod())) } switch { case enabled || oldPodHasHostnameOverride: if !reflect.DeepEqual(newPod, newPodInfo.pod()) { t.Errorf("new pod changed: %v", cmp.Diff(newPod, newPodInfo.pod())) } case newPodHasHostnameOverride: if exp := podWithoutHostnameOverride(); !reflect.DeepEqual(newPod, exp) { t.Errorf("new pod had HostnameOverride: %v", cmp.Diff(newPod, exp)) } default: if !reflect.DeepEqual(newPod, newPodInfo.pod()) { t.Errorf("new pod changed: %v", cmp.Diff(newPod, newPodInfo.pod())) } } }) } } } } func TestDropFileKeyRefInUse(t *testing.T) { testCases := []struct { name string featureEnabled bool oldPodSpec *api.PodSpec newPodSpec *api.PodSpec expectedSpec *api.PodSpec }{ { name: "feature enabled - should not drop FileKeyRef", featureEnabled: true, oldPodSpec: nil, newPodSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "test-container", Env: []api.EnvVar{ { Name: "TEST_ENV", ValueFrom: &api.EnvVarSource{ FileKeyRef: &api.FileKeySelector{ VolumeName: "test-volume", Path: "/path/to/file", Key: "test-key", }, }, }, }, }, }, }, expectedSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "test-container", Env: []api.EnvVar{ { Name: "TEST_ENV", ValueFrom: &api.EnvVarSource{ FileKeyRef: &api.FileKeySelector{ VolumeName: "test-volume", Path: "/path/to/file", Key: "test-key", }, }, }, }, }, }, }, }, { name: "feature disabled - old pod has FileKeyRef - should not drop", featureEnabled: false, oldPodSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "old-container", Env: []api.EnvVar{ { Name: "OLD_ENV", ValueFrom: &api.EnvVarSource{ FileKeyRef: &api.FileKeySelector{ VolumeName: "old-volume", Path: "/old/path", Key: "old-key", }, }, }, }, }, }, }, newPodSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "new-container", Env: []api.EnvVar{ { Name: "NEW_ENV", ValueFrom: &api.EnvVarSource{ FileKeyRef: &api.FileKeySelector{ VolumeName: "new-volume", Path: "/new/path", Key: "new-key", }, }, }, }, }, }, }, expectedSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "new-container", Env: []api.EnvVar{ { Name: "NEW_ENV", ValueFrom: &api.EnvVarSource{ FileKeyRef: &api.FileKeySelector{ VolumeName: "new-volume", Path: "/new/path", Key: "new-key", }, }, }, }, }, }, }, }, { name: "feature disabled - old pod has no FileKeyRef - should drop", featureEnabled: false, oldPodSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "old-container", Env: []api.EnvVar{ { Name: "OLD_ENV", Value: "old-value", }, }, }, }, }, newPodSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "new-container", Env: []api.EnvVar{ { Name: "NEW_ENV", ValueFrom: &api.EnvVarSource{ FileKeyRef: &api.FileKeySelector{ VolumeName: "new-volume", Path: "/new/path", Key: "new-key", }, }, }, { Name: "REGULAR_ENV", Value: "regular-value", }, }, }, }, }, expectedSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "new-container", Env: []api.EnvVar{ { Name: "NEW_ENV", ValueFrom: &api.EnvVarSource{ FileKeyRef: nil, }, }, { Name: "REGULAR_ENV", Value: "regular-value", }, }, }, }, }, }, { name: "feature disabled - old pod is nil - should drop", featureEnabled: false, oldPodSpec: nil, newPodSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "test-container", Env: []api.EnvVar{ { Name: "TEST_ENV", ValueFrom: &api.EnvVarSource{ FileKeyRef: &api.FileKeySelector{ VolumeName: "test-volume", Path: "/path/to/file", Key: "test-key", }, }, }, }, }, }, }, expectedSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "test-container", Env: []api.EnvVar{ { Name: "TEST_ENV", ValueFrom: &api.EnvVarSource{ FileKeyRef: nil, }, }, }, }, }, }, }, { name: "feature disabled - multiple containers with FileKeyRef - should drop all", featureEnabled: false, oldPodSpec: nil, newPodSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "container1", Env: []api.EnvVar{ { Name: "ENV1", ValueFrom: &api.EnvVarSource{ FileKeyRef: &api.FileKeySelector{ VolumeName: "volume1", Path: "/path1", Key: "key1", }, }, }, }, }, { Name: "container2", Env: []api.EnvVar{ { Name: "ENV2", ValueFrom: &api.EnvVarSource{ FileKeyRef: &api.FileKeySelector{ VolumeName: "volume2", Path: "/path2", Key: "key2", }, }, }, }, }, }, InitContainers: []api.Container{ { Name: "init-container", Env: []api.EnvVar{ { Name: "INIT_ENV", ValueFrom: &api.EnvVarSource{ FileKeyRef: &api.FileKeySelector{ VolumeName: "init-volume", Path: "/init/path", Key: "init-key", }, }, }, }, }, }, EphemeralContainers: []api.EphemeralContainer{ { EphemeralContainerCommon: api.EphemeralContainerCommon{ Name: "ephemeral-container", Env: []api.EnvVar{ { Name: "EPHEMERAL_ENV", ValueFrom: &api.EnvVarSource{ FileKeyRef: &api.FileKeySelector{ VolumeName: "ephemeral-volume", Path: "/ephemeral/path", Key: "ephemeral-key", }, }, }, }, }, }, }, }, expectedSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "container1", Env: []api.EnvVar{ { Name: "ENV1", ValueFrom: &api.EnvVarSource{ FileKeyRef: nil, }, }, }, }, { Name: "container2", Env: []api.EnvVar{ { Name: "ENV2", ValueFrom: &api.EnvVarSource{ FileKeyRef: nil, }, }, }, }, }, InitContainers: []api.Container{ { Name: "init-container", Env: []api.EnvVar{ { Name: "INIT_ENV", ValueFrom: &api.EnvVarSource{ FileKeyRef: nil, }, }, }, }, }, EphemeralContainers: []api.EphemeralContainer{ { EphemeralContainerCommon: api.EphemeralContainerCommon{ Name: "ephemeral-container", Env: []api.EnvVar{ { Name: "EPHEMERAL_ENV", ValueFrom: &api.EnvVarSource{ FileKeyRef: nil, }, }, }, }, }, }, }, }, { name: "feature disabled - mixed env vars - should only drop FileKeyRef", featureEnabled: false, oldPodSpec: nil, newPodSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "test-container", Env: []api.EnvVar{ { Name: "REGULAR_ENV", Value: "regular-value", }, { Name: "FILE_KEY_ENV", ValueFrom: &api.EnvVarSource{ FileKeyRef: &api.FileKeySelector{ VolumeName: "test-volume", Path: "/path/to/file", Key: "test-key", }, }, }, { Name: "SECRET_ENV", ValueFrom: &api.EnvVarSource{ SecretKeyRef: &api.SecretKeySelector{ LocalObjectReference: api.LocalObjectReference{ Name: "test-secret", }, Key: "secret-key", }, }, }, }, }, }, }, expectedSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "test-container", Env: []api.EnvVar{ { Name: "REGULAR_ENV", Value: "regular-value", }, { Name: "FILE_KEY_ENV", ValueFrom: &api.EnvVarSource{ FileKeyRef: nil, }, }, { Name: "SECRET_ENV", ValueFrom: &api.EnvVarSource{ SecretKeyRef: &api.SecretKeySelector{ LocalObjectReference: api.LocalObjectReference{ Name: "test-secret", }, Key: "secret-key", }, }, }, }, }, }, }, }, { name: "feature disabled - container with nil Env - should not panic", featureEnabled: false, oldPodSpec: nil, newPodSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "test-container", Env: nil, }, }, }, expectedSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "test-container", Env: nil, }, }, }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.EnvFiles, tc.featureEnabled) newPodSpecCopy := tc.newPodSpec.DeepCopy() dropFileKeyRefInUse(newPodSpecCopy, tc.oldPodSpec) if diff := cmp.Diff(tc.expectedSpec, newPodSpecCopy); diff != "" { t.Errorf("new pod changed (- want, + got): %s", diff) } if tc.oldPodSpec != nil { oldPodSpecCopy := tc.oldPodSpec.DeepCopy() // old pod should never be changed if diff := cmp.Diff(tc.oldPodSpec, oldPodSpecCopy); diff != "" { t.Errorf("old pod changed: %s", diff) } } }) } } // TestPodFileKeyRefInUse tests the podFileKeyRefInUse function func TestPodFileKeyRefInUse(t *testing.T) { testCases := []struct { name string podSpec *api.PodSpec expected bool }{ { name: "nil pod spec", podSpec: nil, expected: false, }, { name: "pod spec with FileKeyRef in container", podSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "test-container", Env: []api.EnvVar{ { Name: "TEST_ENV", ValueFrom: &api.EnvVarSource{ FileKeyRef: &api.FileKeySelector{ VolumeName: "test-volume", Path: "/path/to/file", Key: "test-key", }, }, }, }, }, }, }, expected: true, }, { name: "pod spec with FileKeyRef in init container", podSpec: &api.PodSpec{ InitContainers: []api.Container{ { Name: "init-container", Env: []api.EnvVar{ { Name: "INIT_ENV", ValueFrom: &api.EnvVarSource{ FileKeyRef: &api.FileKeySelector{ VolumeName: "init-volume", Path: "/init/path", Key: "init-key", }, }, }, }, }, }, }, expected: true, }, { name: "pod spec with FileKeyRef in ephemeral container", podSpec: &api.PodSpec{ EphemeralContainers: []api.EphemeralContainer{ { EphemeralContainerCommon: api.EphemeralContainerCommon{ Name: "ephemeral-container", Env: []api.EnvVar{ { Name: "EPHEMERAL_ENV", ValueFrom: &api.EnvVarSource{ FileKeyRef: &api.FileKeySelector{ VolumeName: "ephemeral-volume", Path: "/ephemeral/path", Key: "ephemeral-key", }, }, }, }, }, }, }, }, expected: true, }, { name: "pod spec without FileKeyRef", podSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "test-container", Env: []api.EnvVar{ { Name: "REGULAR_ENV", Value: "regular-value", }, { Name: "SECRET_ENV", ValueFrom: &api.EnvVarSource{ SecretKeyRef: &api.SecretKeySelector{ LocalObjectReference: api.LocalObjectReference{ Name: "test-secret", }, Key: "secret-key", }, }, }, }, }, }, }, expected: false, }, { name: "pod spec with nil Env in container", podSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "test-container", Env: nil, }, }, }, expected: false, }, { name: "pod spec with env var without ValueFrom", podSpec: &api.PodSpec{ Containers: []api.Container{ { Name: "test-container", Env: []api.EnvVar{ { Name: "REGULAR_ENV", Value: "regular-value", }, }, }, }, }, expected: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := podFileKeyRefInUse(tc.podSpec) if result != tc.expected { t.Errorf("expected %v, got %v", tc.expected, result) } }) } } func TestValidateContainerRestartRulesOption(t *testing.T) { policyNever := api.ContainerRestartPolicyNever testCases := []struct { name string oldPodSpec *api.PodSpec featureEnabled bool want bool }{ { name: "feature enabled", featureEnabled: true, want: true, }, { name: "feature disabled", featureEnabled: false, want: false, }, { name: "old pod spec has regular containers with restart policy", oldPodSpec: &api.PodSpec{ Containers: []api.Container{{ RestartPolicy: &policyNever, }}, }, featureEnabled: false, want: true, }, { name: "old pod spec has regular containers with restart policy rules", oldPodSpec: &api.PodSpec{ Containers: []api.Container{{ RestartPolicy: &policyNever, RestartPolicyRules: []api.ContainerRestartRule{{ Action: api.ContainerRestartRuleActionRestart, ExitCodes: &api.ContainerRestartRuleOnExitCodes{ Operator: api.ContainerRestartRuleOnExitCodesOpIn, Values: []int32{42}, }, }}, }}, }, featureEnabled: false, want: true, }, { name: "old pod spec has init containers with restart policy", oldPodSpec: &api.PodSpec{ InitContainers: []api.Container{{ RestartPolicy: &policyNever, }}, }, featureEnabled: false, want: true, }, { name: "old pod spec has regular containers with restart policy rules", oldPodSpec: &api.PodSpec{ InitContainers: []api.Container{{ RestartPolicy: &policyNever, RestartPolicyRules: []api.ContainerRestartRule{{ Action: api.ContainerRestartRuleActionRestart, ExitCodes: &api.ContainerRestartRuleOnExitCodes{ Operator: api.ContainerRestartRuleOnExitCodesOpIn, Values: []int32{42}, }, }}, }}, }, featureEnabled: false, want: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ContainerRestartRules, tc.featureEnabled) // The new pod doesn't impact the outcome. gotOptions := GetValidationOptionsFromPodSpecAndMeta(nil, tc.oldPodSpec, nil, nil) if tc.want != gotOptions.AllowContainerRestartPolicyRules { t.Errorf("unexpected diff, want: %v, got: %v", tc.want, gotOptions.AllowInvalidPodDeletionCost) } }) } } func Test_dropContainerRestartRules(t *testing.T) { var ( always = api.ContainerRestartPolicyAlways rules = []api.ContainerRestartRule{{ Action: api.ContainerRestartRuleActionRestart, ExitCodes: &api.ContainerRestartRuleOnExitCodes{ Operator: api.ContainerRestartRuleOnExitCodesOpIn, Values: []int32{42}, }, }} ) initContainerWithRules := &api.Pod{ Spec: api.PodSpec{ InitContainers: []api.Container{{ RestartPolicy: &always, RestartPolicyRules: rules, }}, }, } initContainerWithoutRules := &api.Pod{ Spec: api.PodSpec{ InitContainers: []api.Container{{ RestartPolicy: &always, }}, }, } regularContainerWithRules := &api.Pod{ Spec: api.PodSpec{ Containers: []api.Container{{ RestartPolicy: &always, RestartPolicyRules: rules, }}, }, } regularContainerWithoutRules := &api.Pod{ Spec: api.PodSpec{ Containers: []api.Container{{}}, }, } ephemeralContainerWithRules := &api.Pod{ Spec: api.PodSpec{ EphemeralContainers: []api.EphemeralContainer{{ EphemeralContainerCommon: api.EphemeralContainerCommon{ RestartPolicy: &always, RestartPolicyRules: rules, }, }}, }, } ephemeralContainerWithoutRules := &api.Pod{ Spec: api.PodSpec{ EphemeralContainers: []api.EphemeralContainer{{}}, }, } cases := []struct { name string oldPod *api.Pod newPod *api.Pod featureEnabled bool expected *api.Pod }{ { name: "drop init container rules", newPod: initContainerWithRules, expected: initContainerWithoutRules, }, { name: "drop regular container rules", newPod: regularContainerWithRules, expected: regularContainerWithoutRules, }, { name: "drop ephemeral container rules", newPod: ephemeralContainerWithRules, expected: ephemeralContainerWithoutRules, }, { name: "not drop init container rules when feature gate enabled", newPod: initContainerWithRules, featureEnabled: true, expected: initContainerWithRules, }, { name: "not drop regular container rules when feature gate enabled", newPod: regularContainerWithRules, featureEnabled: true, expected: regularContainerWithRules, }, { name: "not drop regular container rules when old container has rules", oldPod: regularContainerWithRules, newPod: regularContainerWithRules, expected: regularContainerWithRules, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ContainerRestartRules, tc.featureEnabled) oldPod := tc.oldPod.DeepCopy() newPod := tc.newPod.DeepCopy() wantPod := tc.expected DropDisabledPodFields(newPod, oldPod) // old pod should never be changed if diff := cmp.Diff(oldPod, tc.oldPod); diff != "" { t.Errorf("old pod changed: %s", diff) } if diff := cmp.Diff(wantPod, newPod); diff != "" { t.Errorf("new pod changed (- want, + got): %s", diff) } }) } } func TestHasUserNamespacesWithVolumeDevices(t *testing.T) { falseVar := false trueVar := true tests := []struct { name string spec *api.PodSpec expected bool }{ { name: "hostUsers=nil", spec: &api.PodSpec{}, }, { name: "hostUsers=false & no volume devices", spec: &api.PodSpec{ SecurityContext: &api.PodSecurityContext{ HostUsers: &falseVar, }, }, }, { name: "hostUsers=true & container volumeDevice", spec: &api.PodSpec{ SecurityContext: &api.PodSecurityContext{ HostUsers: &trueVar, }, Containers: []api.Container{{ Name: "test-container", VolumeDevices: []api.VolumeDevice{{ Name: "test-volume", DevicePath: "/dev/test-device", }}, }}, }, }, { name: "hostUsers=false & container volumeDevice", expected: true, spec: &api.PodSpec{ SecurityContext: &api.PodSecurityContext{ HostUsers: &falseVar, }, Containers: []api.Container{{ Name: "test-container", VolumeDevices: []api.VolumeDevice{{ Name: "test-volume", DevicePath: "/dev/test-device", }}, }}, }, }, { name: "hostUsers=false & initContainer volumeDevice", expected: true, spec: &api.PodSpec{ SecurityContext: &api.PodSecurityContext{ HostUsers: &falseVar, }, InitContainers: []api.Container{{ Name: "test-container", VolumeDevices: []api.VolumeDevice{{ Name: "test-volume", DevicePath: "/dev/test-device", }}, }}, }, }, { name: "hostUsers=false & ephemeralContainer volumeDevice", expected: true, spec: &api.PodSpec{ SecurityContext: &api.PodSecurityContext{ HostUsers: &falseVar, }, EphemeralContainers: []api.EphemeralContainer{{ EphemeralContainerCommon: api.EphemeralContainerCommon{ Name: "test-container", VolumeDevices: []api.VolumeDevice{{ Name: "test-volume", DevicePath: "/dev/test-device", }}}, }}, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { actual := hasUserNamespacesWithVolumeDevices(test.spec) if test.expected != actual { t.Errorf("expected %v, got %v", test.expected, actual) } }) } }