diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go index 4d4ae037f86..192214c1fe8 100644 --- a/pkg/apis/core/validation/validation.go +++ b/pkg/apis/core/validation/validation.go @@ -3481,15 +3481,35 @@ func validatePodDNSConfig(dnsConfig *core.PodDNSConfig, dnsPolicy *core.DNSPolic return allErrs } -func validateHostNetwork(hostNetwork bool, containers []core.Container, fldPath *field.Path) field.ErrorList { +// validatePodHostNetworkDeps checks fields which depend on whether HostNetwork is +// true or not. It should be called on all PodSpecs, but opts can change what +// is enforce. E.g. opts.ResourceIsPod should only be set when called in the +// context of a Pod, and not on PodSpecs which are embedded in other resources +// (e.g. Deployments). +func validatePodHostNetworkDeps(spec *core.PodSpec, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { + // For we keep `.HostNetwork` in .SecurityContext on the internal + // version of Pod. + hostNetwork := false + if spec.SecurityContext != nil { + hostNetwork = spec.SecurityContext.HostNetwork + } + allErrors := field.ErrorList{} + if hostNetwork { - for i, container := range containers { + fldPath := fldPath.Child("containers") + for i, container := range spec.Containers { portsPath := fldPath.Index(i).Child("ports") for i, port := range container.Ports { idxPath := portsPath.Index(i) - if port.HostPort != port.ContainerPort { - allErrors = append(allErrors, field.Invalid(idxPath.Child("containerPort"), port.ContainerPort, "must match `hostPort` when `hostNetwork` is true")) + // At this point, we know that HostNetwork is true. If this + // PodSpec is in a Pod (opts.ResourceIsPod), then HostPort must + // be the same value as ContainerPort. If this PodSpec is in + // some other resource (e.g. Deployment) we allow 0 (i.e. + // unspecified) because it will be defaulted when the Pod is + // ultimately created, but we do not allow any other values. + if hp, cp := port.HostPort, port.ContainerPort; (opts.ResourceIsPod || hp != 0) && hp != cp { + allErrors = append(allErrors, field.Invalid(idxPath.Child("hostPort"), port.HostPort, "must match `containerPort` when `hostNetwork` is true")) } } } @@ -3694,6 +3714,9 @@ type PodValidationOptions struct { AllowInvalidTopologySpreadConstraintLabelSelector bool // Allow node selector additions for gated pods. AllowMutableNodeSelectorAndNodeAffinity bool + // The top-level resource being validated is a Pod, not just a PodSpec + // embedded in some other resource. + ResourceIsPod bool } // validatePodMetadataAndSpec tests if required fields in the pod.metadata and pod.spec are set, @@ -3709,8 +3732,6 @@ func validatePodMetadataAndSpec(pod *core.Pod, opts PodValidationOptions) field. // we do additional validation only pertinent for pods and not pod templates // this was done to preserve backwards compatibility - allErrs = append(allErrs, validatePodSecurityContext(pod.Spec.SecurityContext, &pod.Spec, specPath, specPath.Child("securityContext"), opts)...) - if pod.Spec.ServiceAccountName == "" { for vi, volume := range pod.Spec.Volumes { path := specPath.Child("volumes").Index(vi).Child("projected") @@ -3793,6 +3814,7 @@ func ValidatePodSpec(spec *core.PodSpec, podMeta *metav1.ObjectMeta, fldPath *fi allErrs = append(allErrs, validateContainers(spec.Containers, vols, podClaimNames, fldPath.Child("containers"), opts)...) allErrs = append(allErrs, validateInitContainers(spec.InitContainers, spec.Containers, vols, podClaimNames, fldPath.Child("initContainers"), opts)...) allErrs = append(allErrs, validateEphemeralContainers(spec.EphemeralContainers, spec.Containers, spec.InitContainers, vols, podClaimNames, fldPath.Child("ephemeralContainers"), opts)...) + allErrs = append(allErrs, validatePodHostNetworkDeps(spec, fldPath, opts)...) allErrs = append(allErrs, validateRestartPolicy(&spec.RestartPolicy, fldPath.Child("restartPolicy"))...) allErrs = append(allErrs, validateDNSPolicy(&spec.DNSPolicy, fldPath.Child("dnsPolicy"))...) allErrs = append(allErrs, unversionedvalidation.ValidateLabels(spec.NodeSelector, fldPath.Child("nodeSelector"))...) @@ -4399,18 +4421,6 @@ func validateSysctls(sysctls []core.Sysctl, fldPath *field.Path) field.ErrorList return allErrs } -// validatePodSecurityContext verifies the SecurityContext of a Pod, but only -// when it is a Pod and not an embedded PodSpec. -func validatePodSecurityContext(securityContext *core.PodSecurityContext, spec *core.PodSpec, specPath, scPath *field.Path, opts PodValidationOptions) field.ErrorList { - allErrs := field.ErrorList{} - - if securityContext != nil { - allErrs = append(allErrs, validateHostNetwork(securityContext.HostNetwork, spec.Containers, specPath.Child("containers"))...) - } - - return allErrs -} - // validatePodSpecSecurityContext verifies the SecurityContext of a PodSpec, // whether that is defined in a Pod or in an embedded PodSpec (e.g. a // Deployment's pod template). diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go index 437c888448b..80f2be937c1 100644 --- a/pkg/apis/core/validation/validation_test.go +++ b/pkg/apis/core/validation/validation_test.go @@ -8816,7 +8816,10 @@ func TestValidatePodSpec(t *testing.T) { } for k, v := range successCases { t.Run(k, func(t *testing.T) { - if errs := ValidatePodSpec(&v, nil, field.NewPath("field"), PodValidationOptions{}); len(errs) != 0 { + opts := PodValidationOptions{ + ResourceIsPod: true, + } + if errs := ValidatePodSpec(&v, nil, field.NewPath("field"), opts); len(errs) != 0 { t.Errorf("expected success: %v", errs) } }) @@ -8868,6 +8871,18 @@ func TestValidatePodSpec(t *testing.T) { DNSPolicy: core.DNSClusterFirst, Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, }, + "with hostNetwork hostPort unspecified": { + Containers: []core.Container{ + {Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", Ports: []core.ContainerPort{ + {HostPort: 0, ContainerPort: 2600, Protocol: "TCP"}}, + }, + }, + SecurityContext: &core.PodSecurityContext{ + HostNetwork: true, + }, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSClusterFirst, + }, "with hostNetwork hostPort not equal to containerPort": { Containers: []core.Container{ {Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", Ports: []core.ContainerPort{ @@ -9036,7 +9051,10 @@ func TestValidatePodSpec(t *testing.T) { }, } for k, v := range failureCases { - if errs := ValidatePodSpec(&v, nil, field.NewPath("field"), PodValidationOptions{}); len(errs) == 0 { + opts := PodValidationOptions{ + ResourceIsPod: true, + } + if errs := ValidatePodSpec(&v, nil, field.NewPath("field"), opts); len(errs) == 0 { t.Errorf("expected failure for %q", k) } } diff --git a/pkg/registry/core/pod/strategy.go b/pkg/registry/core/pod/strategy.go index 3a87de349d5..ec5e2195c6d 100644 --- a/pkg/registry/core/pod/strategy.go +++ b/pkg/registry/core/pod/strategy.go @@ -112,6 +112,7 @@ func (podStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object func (podStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { pod := obj.(*api.Pod) opts := podutil.GetValidationOptionsFromPodSpecAndMeta(&pod.Spec, nil, &pod.ObjectMeta, nil) + opts.ResourceIsPod = true return corevalidation.ValidatePodCreate(pod, opts) } @@ -141,6 +142,7 @@ func (podStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) pod := obj.(*api.Pod) oldPod := old.(*api.Pod) opts := podutil.GetValidationOptionsFromPodSpecAndMeta(&pod.Spec, &oldPod.Spec, &pod.ObjectMeta, &oldPod.ObjectMeta) + opts.ResourceIsPod = true return corevalidation.ValidatePodUpdate(obj.(*api.Pod), old.(*api.Pod), opts) } @@ -225,6 +227,7 @@ func (podStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Ob pod := obj.(*api.Pod) oldPod := old.(*api.Pod) opts := podutil.GetValidationOptionsFromPodSpecAndMeta(&pod.Spec, &oldPod.Spec, &pod.ObjectMeta, &oldPod.ObjectMeta) + opts.ResourceIsPod = true return corevalidation.ValidatePodStatusUpdate(obj.(*api.Pod), old.(*api.Pod), opts) } @@ -264,6 +267,7 @@ func (podEphemeralContainersStrategy) ValidateUpdate(ctx context.Context, obj, o newPod := obj.(*api.Pod) oldPod := old.(*api.Pod) opts := podutil.GetValidationOptionsFromPodSpecAndMeta(&newPod.Spec, &oldPod.Spec, &newPod.ObjectMeta, &oldPod.ObjectMeta) + opts.ResourceIsPod = true return corevalidation.ValidatePodEphemeralContainersUpdate(newPod, oldPod, opts) }