/* 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" utilfeature "k8s.io/apiserver/pkg/util/feature" nodeapi "k8s.io/kubernetes/pkg/api/node" pvcutil "k8s.io/kubernetes/pkg/api/persistentvolumeclaim" api "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/apis/core/pods" "k8s.io/kubernetes/pkg/features" ) 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 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: `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 := nodeapi.GetNodeLabelDeprecatedMessage(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 { termFldPath := fieldPath.Child("spec", "affinity", "nodeAffinity", "requiredDuringSchedulingIgnoredDuringExecution", "nodeSelectorTerms") for i, term := range n.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms { warnings = append(warnings, nodeapi.GetWarningsForNodeSelectorTerm(term, termFldPath.Index(i))...) } } preferredFldPath := fieldPath.Child("spec", "affinity", "nodeAffinity", "preferredDuringSchedulingIgnoredDuringExecution") for i, term := range n.PreferredDuringSchedulingIgnoredDuringExecution { warnings = append(warnings, nodeapi.GetWarningsForNodeSelectorTerm(term.Preference, preferredFldPath.Index(i).Child("preference"))...) } } for i, t := range podSpec.TopologySpreadConstraints { if msg, deprecated := nodeapi.GetNodeLabelDeprecatedMessage(t.TopologyKey); deprecated { warnings = append(warnings, fmt.Sprintf( "%s: %s is %s", fieldPath.Child("spec", "topologySpreadConstraints").Index(i).Child("topologyKey"), t.TopologyKey, msg, )) } // warn if labelSelector is empty which is no-match. if t.LabelSelector == nil { warnings = append(warnings, fmt.Sprintf("%s: a null labelSelector results in matching no pod", fieldPath.Child("spec", "topologySpreadConstraints").Index(i).Child("labelSelector"))) } } // 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 } } } } // deprecated and removed volume plugins for i, v := range podSpec.Volumes { if v.PhotonPersistentDisk != nil { warnings = append(warnings, fmt.Sprintf("%s: deprecated in v1.11, non-functional in v1.16+", fieldPath.Child("spec", "volumes").Index(i).Child("photonPersistentDisk"))) } if v.GitRepo != nil { warnings = append(warnings, fmt.Sprintf("%s: deprecated in v1.11", fieldPath.Child("spec", "volumes").Index(i).Child("gitRepo"))) } if v.ScaleIO != nil { warnings = append(warnings, fmt.Sprintf("%s: deprecated in v1.16, non-functional in v1.22+", fieldPath.Child("spec", "volumes").Index(i).Child("scaleIO"))) } if v.Flocker != nil { warnings = append(warnings, fmt.Sprintf("%s: deprecated in v1.22, non-functional in v1.25+", fieldPath.Child("spec", "volumes").Index(i).Child("flocker"))) } if v.StorageOS != nil { warnings = append(warnings, fmt.Sprintf("%s: deprecated in v1.22, non-functional in v1.25+", fieldPath.Child("spec", "volumes").Index(i).Child("storageOS"))) } if v.Quobyte != nil { warnings = append(warnings, fmt.Sprintf("%s: deprecated in v1.22, non-functional in v1.25+", fieldPath.Child("spec", "volumes").Index(i).Child("quobyte"))) } if v.Glusterfs != nil { warnings = append(warnings, fmt.Sprintf("%s: deprecated in v1.25, non-functional in v1.26+", fieldPath.Child("spec", "volumes").Index(i).Child("glusterfs"))) } if v.Ephemeral != nil && v.Ephemeral.VolumeClaimTemplate != nil { warnings = append(warnings, pvcutil.GetWarningsForPersistentVolumeClaimSpec(fieldPath.Child("spec", "volumes").Index(i).Child("ephemeral").Child("volumeClaimTemplate").Child("spec"), v.Ephemeral.VolumeClaimTemplate.Spec)...) } if v.CephFS != nil { warnings = append(warnings, fmt.Sprintf("%s: deprecated in v1.28, non-functional in v1.31+", fieldPath.Child("spec", "volumes").Index(i).Child("cephfs"))) } if v.RBD != nil { warnings = append(warnings, fmt.Sprintf("%s: deprecated in v1.28, non-functional in v1.31+", fieldPath.Child("spec", "volumes").Index(i).Child("rbd"))) } } // duplicate hostAliases (#91670, #58477) if len(podSpec.HostAliases) > 1 { items := sets.New[string]() 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.New[string]() 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)) } } // 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())) } // use of pod seccomp annotation without accompanying field if podSpec.SecurityContext == nil || podSpec.SecurityContext.SeccompProfile == nil { if _, exists := meta.Annotations[api.SeccompPodAnnotationKey]; exists { warnings = append(warnings, fmt.Sprintf(`%s: non-functional in v1.27+; use the "seccompProfile" field instead`, fieldPath.Child("metadata", "annotations").Key(api.SeccompPodAnnotationKey))) } } hasPodAppArmorProfile := podSpec.SecurityContext != nil && podSpec.SecurityContext.AppArmorProfile != nil pods.VisitContainersWithPath(podSpec, fieldPath.Child("spec"), func(c *api.Container, p *field.Path) bool { // use of container seccomp annotation without accompanying field if c.SecurityContext == nil || c.SecurityContext.SeccompProfile == nil { if _, exists := meta.Annotations[api.SeccompContainerAnnotationKeyPrefix+c.Name]; exists { warnings = append(warnings, fmt.Sprintf(`%s: non-functional in v1.27+; use the "seccompProfile" field instead`, fieldPath.Child("metadata", "annotations").Key(api.SeccompContainerAnnotationKeyPrefix+c.Name))) } } // use of container AppArmor annotation without accompanying field if utilfeature.DefaultFeatureGate.Enabled(features.AppArmorFields) { isPodTemplate := fieldPath != nil // Pod warnings are emitted through applyAppArmorVersionSkew instead. hasAppArmorField := hasPodAppArmorProfile || (c.SecurityContext != nil && c.SecurityContext.AppArmorProfile != nil) if isPodTemplate && !hasAppArmorField { key := api.DeprecatedAppArmorAnnotationKeyPrefix + c.Name if _, exists := meta.Annotations[key]; exists { warnings = append(warnings, fmt.Sprintf(`%s: deprecated since v1.30; use the "appArmorProfile" field instead`, fieldPath.Child("metadata", "annotations").Key(key))) } } } // 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.New[string]() for i, item := range c.Env { if items.Has(item.Name) { // a previous value exists, but it might be OK bad := false ref := fmt.Sprintf("$(%s)", item.Name) // what does a ref to this name look like // if we are replacing it with a valueFrom, warn if item.ValueFrom != nil { bad = true } // if this is X="$(X)", warn if item.Value == ref { bad = true } // if the new value does not contain a reference to the old // value (e.g. X="abc"; X="$(X)123"), warn if !strings.Contains(item.Value, ref) { bad = true } if bad { warnings = append(warnings, fmt.Sprintf("%s: hides previous definition of %q, which may be dropped when using apply", p.Child("env").Index(i), item.Name)) } } else { items.Insert(item.Name) } } } return true }) type portBlock struct { field *field.Path port api.ContainerPort } // Accumulate ports across all containers allPorts := map[string][]portBlock{} pods.VisitContainersWithPath(podSpec, fieldPath.Child("spec"), func(c *api.Container, fldPath *field.Path) bool { for i, port := range c.Ports { if port.HostIP != "" && port.HostPort == 0 { warnings = append(warnings, fmt.Sprintf("%s: hostIP set without hostPort: %+v", fldPath.Child("ports").Index(i), port)) } k := fmt.Sprintf("%d/%s", port.ContainerPort, port.Protocol) if others, found := allPorts[k]; found { // Someone else has this protcol+port, but it still might not be a conflict. for _, other := range others { if port.HostIP == other.port.HostIP && port.HostPort == other.port.HostPort { // Exactly-equal is obvious. Validation should already filter for this except when these are unspecified. warnings = append(warnings, fmt.Sprintf("%s: duplicate port definition with %s", fldPath.Child("ports").Index(i), other.field)) } else if port.HostPort == 0 || other.port.HostPort == 0 { // HostPort = 0 is redundant with any other value, which is odd but not really dangerous. HostIP doesn't matter here. warnings = append(warnings, fmt.Sprintf("%s: overlapping port definition with %s", fldPath.Child("ports").Index(i), other.field)) } else if a, b := port.HostIP == "", other.port.HostIP == ""; port.HostPort == other.port.HostPort && ((a || b) && !(a && b)) { // If the HostPorts are the same and either HostIP is not specified while the other is not, the behavior is undefined. warnings = append(warnings, fmt.Sprintf("%s: dangerously ambiguous port definition with %s", fldPath.Child("ports").Index(i), other.field)) } } allPorts[k] = append(allPorts[k], portBlock{field: fldPath.Child("ports").Index(i), port: port}) } else { allPorts[k] = []portBlock{{field: fldPath.Child("ports").Index(i), port: port}} } } return true }) // warn if the terminationGracePeriodSeconds is negative. if podSpec.TerminationGracePeriodSeconds != nil && *podSpec.TerminationGracePeriodSeconds < 0 { warnings = append(warnings, fmt.Sprintf("%s: must be >= 0; negative values are invalid and will be treated as 1", fieldPath.Child("spec", "terminationGracePeriodSeconds"))) } if podSpec.Affinity != nil { if affinity := podSpec.Affinity.PodAffinity; affinity != nil { warnings = append(warnings, warningsForPodAffinityTerms(affinity.RequiredDuringSchedulingIgnoredDuringExecution, fieldPath.Child("spec", "affinity", "podAffinity", "requiredDuringSchedulingIgnoredDuringExecution"))...) warnings = append(warnings, warningsForWeightedPodAffinityTerms(affinity.PreferredDuringSchedulingIgnoredDuringExecution, fieldPath.Child("spec", "affinity", "podAffinity", "preferredDuringSchedulingIgnoredDuringExecution"))...) } if affinity := podSpec.Affinity.PodAntiAffinity; affinity != nil { warnings = append(warnings, warningsForPodAffinityTerms(affinity.RequiredDuringSchedulingIgnoredDuringExecution, fieldPath.Child("spec", "affinity", "podAntiAffinity", "requiredDuringSchedulingIgnoredDuringExecution"))...) warnings = append(warnings, warningsForWeightedPodAffinityTerms(affinity.PreferredDuringSchedulingIgnoredDuringExecution, fieldPath.Child("spec", "affinity", "podAntiAffinity", "preferredDuringSchedulingIgnoredDuringExecution"))...) } } return warnings } func warningsForPodAffinityTerms(terms []api.PodAffinityTerm, fieldPath *field.Path) []string { var warnings []string for i, t := range terms { if t.LabelSelector == nil { warnings = append(warnings, fmt.Sprintf("%s: a null labelSelector results in matching no pod", fieldPath.Index(i).Child("labelSelector"))) } } return warnings } func warningsForWeightedPodAffinityTerms(terms []api.WeightedPodAffinityTerm, fieldPath *field.Path) []string { var warnings []string for i, t := range terms { // warn if labelSelector is empty which is no-match. if t.PodAffinityTerm.LabelSelector == nil { warnings = append(warnings, fmt.Sprintf("%s: a null labelSelector results in matching no pod", fieldPath.Index(i).Child("podAffinityTerm", "labelSelector"))) } } return warnings }