diff --git a/pkg/api/pod/warnings.go b/pkg/api/pod/warnings.go new file mode 100644 index 00000000000..e1f93e85dec --- /dev/null +++ b/pkg/api/pod/warnings.go @@ -0,0 +1,257 @@ +/* +Copyright 2021 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 ( + "context" + "fmt" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation/field" + api "k8s.io/kubernetes/pkg/apis/core" + "k8s.io/kubernetes/pkg/apis/core/pods" +) + +func GetWarningsForPod(ctx context.Context, pod, oldPod *api.Pod) []string { + if pod == nil { + return nil + } + + var ( + oldSpec *api.PodSpec + oldMeta *metav1.ObjectMeta + ) + if oldPod != nil { + oldSpec = &oldPod.Spec + oldMeta = &oldPod.ObjectMeta + } + return warningsForPodSpecAndMeta(nil, &pod.Spec, &pod.ObjectMeta, oldSpec, oldMeta) +} + +func GetWarningsForPodTemplate(ctx context.Context, fieldPath *field.Path, podTemplate, oldPodTemplate *api.PodTemplateSpec) []string { + if podTemplate == nil { + return nil + } + + var ( + oldSpec *api.PodSpec + oldMeta *metav1.ObjectMeta + ) + if oldPodTemplate != nil { + oldSpec = &oldPodTemplate.Spec + oldMeta = &oldPodTemplate.ObjectMeta + } + return warningsForPodSpecAndMeta(fieldPath, &podTemplate.Spec, &podTemplate.ObjectMeta, oldSpec, oldMeta) +} + +var deprecatedNodeLabels = map[string]string{ + `beta.kubernetes.io/arch`: `deprecated since v1.14; use "kubernetes.io/arch" instead`, + `beta.kubernetes.io/os`: `deprecated since v1.14; use "kubernetes.io/os" instead`, + `failure-domain.beta.kubernetes.io/region`: `deprecated since v1.17; use "topology.kubernetes.io/region" instead`, + `failure-domain.beta.kubernetes.io/zone`: `deprecated since v1.17; use "topology.kubernetes.io/zone" instead`, + `beta.kubernetes.io/instance-type`: `deprecated since v1.17; use "node.kubernetes.io/instance-type" instead`, +} + +var deprecatedAnnotations = []struct { + key string + prefix string + message string +}{ + { + key: `scheduler.alpha.kubernetes.io/critical-pod`, + message: `non-functional in v1.16+; use the "priorityClassName" field instead`, + }, + { + key: `seccomp.security.alpha.kubernetes.io/pod`, + prefix: `container.seccomp.security.alpha.kubernetes.io/`, + message: `deprecated since v1.19; use the "seccompProfile" field instead`, + }, + { + key: `security.alpha.kubernetes.io/sysctls`, + message: `non-functional in v1.11+; use the "sysctls" field instead`, + }, + { + key: `security.alpha.kubernetes.io/unsafe-sysctls`, + message: `non-functional in v1.11+; use the "sysctls" field instead`, + }, +} + +func warningsForPodSpecAndMeta(fieldPath *field.Path, podSpec *api.PodSpec, meta *metav1.ObjectMeta, oldPodSpec *api.PodSpec, oldMeta *metav1.ObjectMeta) []string { + var warnings []string + + // use of deprecated node labels in selectors/affinity/topology + for k := range podSpec.NodeSelector { + if msg, deprecated := deprecatedNodeLabels[k]; deprecated { + warnings = append(warnings, fmt.Sprintf("%s: %s", fieldPath.Child("spec", "nodeSelector").Key(k), msg)) + } + } + if podSpec.Affinity != nil && podSpec.Affinity.NodeAffinity != nil { + n := podSpec.Affinity.NodeAffinity + if n.RequiredDuringSchedulingIgnoredDuringExecution != nil { + for i, t := range n.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms { + for j, e := range t.MatchExpressions { + if msg, deprecated := deprecatedNodeLabels[e.Key]; deprecated { + warnings = append( + warnings, + fmt.Sprintf( + "%s: %s is %s", + fieldPath.Child("spec", "affinity", "nodeAffinity", "requiredDuringSchedulingIgnoredDuringExecution", "nodeSelectorTerms").Index(i). + Child("matchExpressions").Index(j). + Child("key"), + e.Key, + msg, + ), + ) + } + } + } + } + for i, t := range n.PreferredDuringSchedulingIgnoredDuringExecution { + for j, e := range t.Preference.MatchExpressions { + if msg, deprecated := deprecatedNodeLabels[e.Key]; deprecated { + warnings = append( + warnings, + fmt.Sprintf( + "%s: %s is %s", + fieldPath.Child("spec", "affinity", "nodeAffinity", "preferredDuringSchedulingIgnoredDuringExecution").Index(i). + Child("preference"). + Child("matchExpressions").Index(j). + Child("key"), + e.Key, + msg, + ), + ) + } + } + } + } + for i, t := range podSpec.TopologySpreadConstraints { + if msg, deprecated := deprecatedNodeLabels[t.TopologyKey]; deprecated { + warnings = append(warnings, fmt.Sprintf( + "%s: %s is %s", + fieldPath.Child("spec", "topologySpreadConstraints").Index(i).Child("topologyKey"), + t.TopologyKey, + msg, + )) + } + } + + // use of deprecated annotations + for _, deprecated := range deprecatedAnnotations { + if _, exists := meta.Annotations[deprecated.key]; exists { + warnings = append(warnings, fmt.Sprintf("%s: %s", fieldPath.Child("metadata", "annotations").Key(deprecated.key), deprecated.message)) + } + if len(deprecated.prefix) > 0 { + for k := range meta.Annotations { + if strings.HasPrefix(k, deprecated.prefix) { + warnings = append(warnings, fmt.Sprintf("%s: %s", fieldPath.Child("metadata", "annotations").Key(k), deprecated.message)) + break + } + } + } + } + + // removed volume plugins + for i, v := range podSpec.Volumes { + if v.PhotonPersistentDisk != nil { + warnings = append(warnings, fmt.Sprintf("%s: non-functional in v1.16+", fieldPath.Child("spec", "volumes").Index(i).Child("photonPersistentDisk"))) + } + } + + // duplicate hostAliases (#91670, #58477) + if len(podSpec.HostAliases) > 1 { + items := sets.NewString() + for i, item := range podSpec.HostAliases { + if items.Has(item.IP) { + warnings = append(warnings, fmt.Sprintf("%s: duplicate ip %q", fieldPath.Child("spec", "hostAliases").Index(i).Child("ip"), item.IP)) + } else { + items.Insert(item.IP) + } + } + } + + // duplicate imagePullSecrets (#91629, #58477) + if len(podSpec.ImagePullSecrets) > 1 { + items := sets.NewString() + for i, item := range podSpec.ImagePullSecrets { + if items.Has(item.Name) { + warnings = append(warnings, fmt.Sprintf("%s: duplicate name %q", fieldPath.Child("spec", "imagePullSecrets").Index(i).Child("name"), item.Name)) + } else { + items.Insert(item.Name) + } + } + } + // imagePullSecrets with empty name (#99454#issuecomment-787838112) + for i, item := range podSpec.ImagePullSecrets { + if len(item.Name) == 0 { + warnings = append(warnings, fmt.Sprintf("%s: invalid empty name %q", fieldPath.Child("spec", "imagePullSecrets").Index(i).Child("name"), item.Name)) + } + } + + // duplicate volume names (#78266, #58477) + if len(podSpec.Volumes) > 1 { + items := sets.NewString() + for i, item := range podSpec.Volumes { + if items.Has(item.Name) { + warnings = append(warnings, fmt.Sprintf("%s: duplicate name %q", fieldPath.Child("spec", "volumes").Index(i).Child("name"), item.Name)) + } else { + items.Insert(item.Name) + } + } + } + + // fractional memory/ephemeral-storage requests/limits (#79950, #49442, #18538) + if value, ok := podSpec.Overhead[api.ResourceMemory]; ok && value.MilliValue()%int64(1000) != int64(0) { + warnings = append(warnings, fmt.Sprintf("%s: fractional byte value %q is invalid, must be an integer", fieldPath.Child("spec", "overhead").Key(string(api.ResourceMemory)), value.String())) + } + if value, ok := podSpec.Overhead[api.ResourceEphemeralStorage]; ok && value.MilliValue()%int64(1000) != int64(0) { + warnings = append(warnings, fmt.Sprintf("%s: fractional byte value %q is invalid, must be an integer", fieldPath.Child("spec", "overhead").Key(string(api.ResourceEphemeralStorage)), value.String())) + } + + pods.VisitContainersWithPath(podSpec, fieldPath.Child("spec"), func(c *api.Container, p *field.Path) bool { + // fractional memory/ephemeral-storage requests/limits (#79950, #49442, #18538) + if value, ok := c.Resources.Limits[api.ResourceMemory]; ok && value.MilliValue()%int64(1000) != int64(0) { + warnings = append(warnings, fmt.Sprintf("%s: fractional byte value %q is invalid, must be an integer", p.Child("resources", "limits").Key(string(api.ResourceMemory)), value.String())) + } + if value, ok := c.Resources.Requests[api.ResourceMemory]; ok && value.MilliValue()%int64(1000) != int64(0) { + warnings = append(warnings, fmt.Sprintf("%s: fractional byte value %q is invalid, must be an integer", p.Child("resources", "requests").Key(string(api.ResourceMemory)), value.String())) + } + if value, ok := c.Resources.Limits[api.ResourceEphemeralStorage]; ok && value.MilliValue()%int64(1000) != int64(0) { + warnings = append(warnings, fmt.Sprintf("%s: fractional byte value %q is invalid, must be an integer", p.Child("resources", "limits").Key(string(api.ResourceEphemeralStorage)), value.String())) + } + if value, ok := c.Resources.Requests[api.ResourceEphemeralStorage]; ok && value.MilliValue()%int64(1000) != int64(0) { + warnings = append(warnings, fmt.Sprintf("%s: fractional byte value %q is invalid, must be an integer", p.Child("resources", "requests").Key(string(api.ResourceEphemeralStorage)), value.String())) + } + + // duplicate containers[*].env (#86163, #93266, #58477) + if len(c.Env) > 1 { + items := sets.NewString() + for i, item := range c.Env { + if items.Has(item.Name) { + warnings = append(warnings, fmt.Sprintf("%s: duplicate name %q", p.Child("env").Index(i).Child("name"), item.Name)) + } else { + items.Insert(item.Name) + } + } + } + return true + }) + + return warnings +} diff --git a/pkg/api/pod/warnings_test.go b/pkg/api/pod/warnings_test.go new file mode 100644 index 00000000000..bfb59dfeb9a --- /dev/null +++ b/pkg/api/pod/warnings_test.go @@ -0,0 +1,416 @@ +/* +Copyright 2021 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 ( + "context" + "testing" + + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + api "k8s.io/kubernetes/pkg/apis/core" +) + +func BenchmarkNoWarnings(b *testing.B) { + ctx := context.TODO() + resources := api.ResourceList{ + api.ResourceCPU: resource.MustParse("100m"), + api.ResourceMemory: resource.MustParse("4M"), + api.ResourceEphemeralStorage: resource.MustParse("4G"), + } + env := []api.EnvVar{ + {Name: "a"}, + {Name: "b"}, + } + pod := &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{`foo`: `bar`}, + }, + Spec: api.PodSpec{ + NodeSelector: map[string]string{"foo": "bar", "baz": "quux"}, + Affinity: &api.Affinity{ + NodeAffinity: &api.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &api.NodeSelector{ + NodeSelectorTerms: []api.NodeSelectorTerm{ + {MatchExpressions: []api.NodeSelectorRequirement{{Key: `foo`}}}, + }, + }, + PreferredDuringSchedulingIgnoredDuringExecution: []api.PreferredSchedulingTerm{ + {Preference: api.NodeSelectorTerm{MatchExpressions: []api.NodeSelectorRequirement{{Key: `foo`}}}}, + }, + }, + }, + TopologySpreadConstraints: []api.TopologySpreadConstraint{ + {TopologyKey: `foo`}, + }, + HostAliases: []api.HostAlias{ + {IP: "1.1.1.1"}, + {IP: "2.2.2.2"}, + }, + ImagePullSecrets: []api.LocalObjectReference{ + {Name: "secret1"}, + {Name: "secret2"}, + }, + InitContainers: []api.Container{ + {Name: "init1", Env: env, Resources: api.ResourceRequirements{Requests: resources, Limits: resources}}, + {Name: "init2", Env: env, Resources: api.ResourceRequirements{Requests: resources, Limits: resources}}, + }, + Containers: []api.Container{ + {Name: "container1", Env: env, Resources: api.ResourceRequirements{Requests: resources, Limits: resources}}, + {Name: "container2", Env: env, Resources: api.ResourceRequirements{Requests: resources, Limits: resources}}, + }, + Overhead: resources, + Volumes: []api.Volume{ + {Name: "a"}, + {Name: "b"}, + }, + }, + } + oldPod := &api.Pod{} + b.ResetTimer() + for i := 0; i < b.N; i++ { + w := GetWarningsForPod(ctx, pod, oldPod) + if len(w) > 0 { + b.Fatalf("expected 0 warnings, got %q", w) + } + } +} + +func BenchmarkWarnings(b *testing.B) { + ctx := context.TODO() + resources := api.ResourceList{ + api.ResourceCPU: resource.MustParse("100m"), + api.ResourceMemory: resource.MustParse("4m"), + api.ResourceEphemeralStorage: resource.MustParse("4m"), + } + env := []api.EnvVar{ + {Name: "a"}, + {Name: "a"}, + } + pod := &api.Pod{ + Spec: api.PodSpec{ + HostAliases: []api.HostAlias{ + {IP: "1.1.1.1"}, + {IP: "1.1.1.1"}, + }, + ImagePullSecrets: []api.LocalObjectReference{ + {Name: "secret1"}, + {Name: "secret1"}, + {Name: ""}, + }, + InitContainers: []api.Container{ + {Name: "init1", Env: env, Resources: api.ResourceRequirements{Requests: resources, Limits: resources}}, + {Name: "init2", Env: env, Resources: api.ResourceRequirements{Requests: resources, Limits: resources}}, + }, + Containers: []api.Container{ + {Name: "container1", Env: env, Resources: api.ResourceRequirements{Requests: resources, Limits: resources}}, + {Name: "container2", Env: env, Resources: api.ResourceRequirements{Requests: resources, Limits: resources}}, + }, + Overhead: resources, + Volumes: []api.Volume{ + {Name: "a"}, + {Name: "a"}, + }, + }, + } + oldPod := &api.Pod{} + b.ResetTimer() + for i := 0; i < b.N; i++ { + GetWarningsForPod(ctx, pod, oldPod) + } +} + +func TestWarnings(t *testing.T) { + resources := api.ResourceList{ + api.ResourceCPU: resource.MustParse("100m"), + api.ResourceMemory: resource.MustParse("4m"), + api.ResourceEphemeralStorage: resource.MustParse("4m"), + } + testcases := []struct { + name string + template *api.PodTemplateSpec + expected []string + }{ + { + name: "null", + template: nil, + expected: nil, + }, + { + name: "photon", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{ + {Name: "p", VolumeSource: api.VolumeSource{PhotonPersistentDisk: &api.PhotonPersistentDiskVolumeSource{}}}, + }}, + }, + expected: []string{`spec.volumes[0].photonPersistentDisk: non-functional in v1.16+`}, + }, + { + name: "duplicate hostAlias", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + HostAliases: []api.HostAlias{ + {IP: "1.1.1.1"}, + {IP: "1.1.1.1"}, + {IP: "1.1.1.1"}, + }}, + }, + expected: []string{ + `spec.hostAliases[1].ip: duplicate ip "1.1.1.1"`, + `spec.hostAliases[2].ip: duplicate ip "1.1.1.1"`, + }, + }, + { + name: "duplicate imagePullSecret", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + ImagePullSecrets: []api.LocalObjectReference{ + {Name: "a"}, + {Name: "a"}, + {Name: "a"}, + }}, + }, + expected: []string{ + `spec.imagePullSecrets[1].name: duplicate name "a"`, + `spec.imagePullSecrets[2].name: duplicate name "a"`, + }, + }, + { + name: "empty imagePullSecret", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + ImagePullSecrets: []api.LocalObjectReference{ + {Name: ""}, + }}, + }, + expected: []string{ + `spec.imagePullSecrets[0].name: invalid empty name ""`, + }, + }, + { + name: "duplicate volume", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + Volumes: []api.Volume{ + {Name: "a"}, + {Name: "a"}, + {Name: "a"}, + }}, + }, + expected: []string{ + `spec.volumes[1].name: duplicate name "a"`, + `spec.volumes[2].name: duplicate name "a"`, + }, + }, + { + name: "duplicate env", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + InitContainers: []api.Container{{Env: []api.EnvVar{ + {Name: "a"}, + {Name: "a"}, + {Name: "a"}, + }}}, + Containers: []api.Container{{Env: []api.EnvVar{ + {Name: "b"}, + {Name: "b"}, + {Name: "b"}, + }}}, + }}, + expected: []string{ + `spec.initContainers[0].env[1].name: duplicate name "a"`, + `spec.initContainers[0].env[2].name: duplicate name "a"`, + `spec.containers[0].env[1].name: duplicate name "b"`, + `spec.containers[0].env[2].name: duplicate name "b"`, + }, + }, + { + name: "fractional resources", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + InitContainers: []api.Container{{ + Resources: api.ResourceRequirements{Requests: resources, Limits: resources}, + }}, + Containers: []api.Container{{ + Resources: api.ResourceRequirements{Requests: resources, Limits: resources}, + }}, + Overhead: resources, + }}, + expected: []string{ + `spec.initContainers[0].resources.requests[ephemeral-storage]: fractional byte value "4m" is invalid, must be an integer`, + `spec.initContainers[0].resources.requests[memory]: fractional byte value "4m" is invalid, must be an integer`, + `spec.initContainers[0].resources.limits[ephemeral-storage]: fractional byte value "4m" is invalid, must be an integer`, + `spec.initContainers[0].resources.limits[memory]: fractional byte value "4m" is invalid, must be an integer`, + `spec.containers[0].resources.requests[ephemeral-storage]: fractional byte value "4m" is invalid, must be an integer`, + `spec.containers[0].resources.requests[memory]: fractional byte value "4m" is invalid, must be an integer`, + `spec.containers[0].resources.limits[ephemeral-storage]: fractional byte value "4m" is invalid, must be an integer`, + `spec.containers[0].resources.limits[memory]: fractional byte value "4m" is invalid, must be an integer`, + `spec.overhead[ephemeral-storage]: fractional byte value "4m" is invalid, must be an integer`, + `spec.overhead[memory]: fractional byte value "4m" is invalid, must be an integer`, + }, + }, + { + name: "node labels in nodeSelector", + template: &api.PodTemplateSpec{Spec: api.PodSpec{ + NodeSelector: map[string]string{ + `beta.kubernetes.io/arch`: `true`, + `beta.kubernetes.io/os`: `true`, + `failure-domain.beta.kubernetes.io/region`: `true`, + `failure-domain.beta.kubernetes.io/zone`: `true`, + `beta.kubernetes.io/instance-type`: `true`, + }, + }}, + expected: []string{ + `spec.nodeSelector[beta.kubernetes.io/arch]: deprecated since v1.14; use "kubernetes.io/arch" instead`, + `spec.nodeSelector[beta.kubernetes.io/instance-type]: deprecated since v1.17; use "node.kubernetes.io/instance-type" instead`, + `spec.nodeSelector[beta.kubernetes.io/os]: deprecated since v1.14; use "kubernetes.io/os" instead`, + `spec.nodeSelector[failure-domain.beta.kubernetes.io/region]: deprecated since v1.17; use "topology.kubernetes.io/region" instead`, + `spec.nodeSelector[failure-domain.beta.kubernetes.io/zone]: deprecated since v1.17; use "topology.kubernetes.io/zone" instead`, + }, + }, + { + name: "node labels in affinity requiredDuringSchedulingIgnoredDuringExecution", + template: &api.PodTemplateSpec{ + Spec: api.PodSpec{ + Affinity: &api.Affinity{ + NodeAffinity: &api.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &api.NodeSelector{ + NodeSelectorTerms: []api.NodeSelectorTerm{ + { + MatchExpressions: []api.NodeSelectorRequirement{ + {Key: `foo`}, + {Key: `beta.kubernetes.io/arch`}, + {Key: `beta.kubernetes.io/os`}, + {Key: `failure-domain.beta.kubernetes.io/region`}, + {Key: `failure-domain.beta.kubernetes.io/zone`}, + {Key: `beta.kubernetes.io/instance-type`}, + }, + }, + }, + }, + }, + }, + }, + }, + expected: []string{ + `spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[1].key: beta.kubernetes.io/arch is deprecated since v1.14; use "kubernetes.io/arch" instead`, + `spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[2].key: beta.kubernetes.io/os is deprecated since v1.14; use "kubernetes.io/os" instead`, + `spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[3].key: failure-domain.beta.kubernetes.io/region is deprecated since v1.17; use "topology.kubernetes.io/region" instead`, + `spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[4].key: failure-domain.beta.kubernetes.io/zone is deprecated since v1.17; use "topology.kubernetes.io/zone" instead`, + `spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms[0].matchExpressions[5].key: beta.kubernetes.io/instance-type is deprecated since v1.17; use "node.kubernetes.io/instance-type" instead`, + }, + }, + { + name: "node labels in affinity preferredDuringSchedulingIgnoredDuringExecution", + template: &api.PodTemplateSpec{ + Spec: api.PodSpec{ + Affinity: &api.Affinity{ + NodeAffinity: &api.NodeAffinity{ + PreferredDuringSchedulingIgnoredDuringExecution: []api.PreferredSchedulingTerm{ + { + Preference: api.NodeSelectorTerm{ + MatchExpressions: []api.NodeSelectorRequirement{ + {Key: `foo`}, + {Key: `beta.kubernetes.io/arch`}, + {Key: `beta.kubernetes.io/os`}, + {Key: `failure-domain.beta.kubernetes.io/region`}, + {Key: `failure-domain.beta.kubernetes.io/zone`}, + {Key: `beta.kubernetes.io/instance-type`}, + }, + }, + }, + }, + }, + }, + }, + }, + expected: []string{ + `spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].preference.matchExpressions[1].key: beta.kubernetes.io/arch is deprecated since v1.14; use "kubernetes.io/arch" instead`, + `spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].preference.matchExpressions[2].key: beta.kubernetes.io/os is deprecated since v1.14; use "kubernetes.io/os" instead`, + `spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].preference.matchExpressions[3].key: failure-domain.beta.kubernetes.io/region is deprecated since v1.17; use "topology.kubernetes.io/region" instead`, + `spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].preference.matchExpressions[4].key: failure-domain.beta.kubernetes.io/zone is deprecated since v1.17; use "topology.kubernetes.io/zone" instead`, + `spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].preference.matchExpressions[5].key: beta.kubernetes.io/instance-type is deprecated since v1.17; use "node.kubernetes.io/instance-type" instead`, + }, + }, + { + name: "node labels in topologySpreadConstraints", + template: &api.PodTemplateSpec{ + Spec: api.PodSpec{ + TopologySpreadConstraints: []api.TopologySpreadConstraint{ + {TopologyKey: `foo`}, + {TopologyKey: `beta.kubernetes.io/arch`}, + {TopologyKey: `beta.kubernetes.io/os`}, + {TopologyKey: `failure-domain.beta.kubernetes.io/region`}, + {TopologyKey: `failure-domain.beta.kubernetes.io/zone`}, + {TopologyKey: `beta.kubernetes.io/instance-type`}, + }, + }, + }, + expected: []string{ + `spec.topologySpreadConstraints[1].topologyKey: beta.kubernetes.io/arch is deprecated since v1.14; use "kubernetes.io/arch" instead`, + `spec.topologySpreadConstraints[2].topologyKey: beta.kubernetes.io/os is deprecated since v1.14; use "kubernetes.io/os" instead`, + `spec.topologySpreadConstraints[3].topologyKey: failure-domain.beta.kubernetes.io/region is deprecated since v1.17; use "topology.kubernetes.io/region" instead`, + `spec.topologySpreadConstraints[4].topologyKey: failure-domain.beta.kubernetes.io/zone is deprecated since v1.17; use "topology.kubernetes.io/zone" instead`, + `spec.topologySpreadConstraints[5].topologyKey: beta.kubernetes.io/instance-type is deprecated since v1.17; use "node.kubernetes.io/instance-type" instead`, + }, + }, + { + name: "annotations", + template: &api.PodTemplateSpec{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ + `foo`: `bar`, + `scheduler.alpha.kubernetes.io/critical-pod`: `true`, + `seccomp.security.alpha.kubernetes.io/pod`: `default`, + `container.seccomp.security.alpha.kubernetes.io/foo`: `default`, + `security.alpha.kubernetes.io/sysctls`: `a,b,c`, + `security.alpha.kubernetes.io/unsafe-sysctls`: `d,e,f`, + }}}, + expected: []string{ + `metadata.annotations[scheduler.alpha.kubernetes.io/critical-pod]: non-functional in v1.16+; use the "priorityClassName" field instead`, + `metadata.annotations[seccomp.security.alpha.kubernetes.io/pod]: deprecated since v1.19; use the "seccompProfile" field instead`, + `metadata.annotations[container.seccomp.security.alpha.kubernetes.io/foo]: deprecated since v1.19; use the "seccompProfile" field instead`, + `metadata.annotations[security.alpha.kubernetes.io/sysctls]: non-functional in v1.11+; use the "sysctls" field instead`, + `metadata.annotations[security.alpha.kubernetes.io/unsafe-sysctls]: non-functional in v1.11+; use the "sysctls" field instead`, + }, + }, + } + + for _, tc := range testcases { + t.Run("podspec_"+tc.name, func(t *testing.T) { + actual := sets.NewString(GetWarningsForPodTemplate(context.TODO(), nil, tc.template, &api.PodTemplateSpec{})...) + expected := sets.NewString(tc.expected...) + for _, missing := range expected.Difference(actual).List() { + t.Errorf("missing: %s", missing) + } + for _, extra := range actual.Difference(expected).List() { + t.Errorf("extra: %s", extra) + } + }) + + t.Run("pod_"+tc.name, func(t *testing.T) { + var pod *api.Pod + if tc.template != nil { + pod = &api.Pod{ + ObjectMeta: tc.template.ObjectMeta, + Spec: tc.template.Spec, + } + } + actual := sets.NewString(GetWarningsForPod(context.TODO(), pod, &api.Pod{})...) + expected := sets.NewString(tc.expected...) + for _, missing := range expected.Difference(actual).List() { + t.Errorf("missing: %s", missing) + } + for _, extra := range actual.Difference(expected).List() { + t.Errorf("extra: %s", extra) + } + }) + } +}