diff --git a/pkg/apis/apps/validation/validation_test.go b/pkg/apis/apps/validation/validation_test.go index c79ab04c827..07d2dc8c773 100644 --- a/pkg/apis/apps/validation/validation_test.go +++ b/pkg/apis/apps/validation/validation_test.go @@ -213,6 +213,30 @@ func TestValidateStatefulSet(t *testing.T) { }, } + validHostNetPodTemplate := api.PodTemplate{ + Template: api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: validLabels, + }, + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{ + HostNetwork: true, + }, + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{ + Name: "abc", + Image: "image", + ImagePullPolicy: "IfNotPresent", + Ports: []api.ContainerPort{{ + ContainerPort: 12345, + Protocol: api.ProtocolTCP, + }}, + }}, + }, + }, + } + invalidLabels := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"} invalidPodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ @@ -254,6 +278,9 @@ func TestValidateStatefulSet(t *testing.T) { }, { name: "alphanumeric name", set: mkStatefulSet(&validPodTemplate, tweakName("abc-123")), + }, { + name: "hostNetwork true", + set: mkStatefulSet(&validHostNetPodTemplate), }, { name: "parallel pod management", set: mkStatefulSet(&validPodTemplate, tweakPodManagementPolicy(apps.ParallelPodManagement)), @@ -1988,6 +2015,30 @@ func TestValidateDaemonSet(t *testing.T) { }, }, } + validHostNetPodTemplate := api.PodTemplate{ + Template: api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: validSelector, + }, + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{ + HostNetwork: true, + }, + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{ + Name: "abc", + Image: "image", + ImagePullPolicy: "IfNotPresent", + TerminationMessagePolicy: api.TerminationMessageReadFile, + Ports: []api.ContainerPort{{ + ContainerPort: 12345, + Protocol: api.ProtocolTCP, + }}, + }}, + }, + }, + } invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"} invalidPodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ @@ -2018,6 +2069,15 @@ func TestValidateDaemonSet(t *testing.T) { Type: apps.OnDeleteDaemonSetStrategyType, }, }, + }, { + ObjectMeta: metav1.ObjectMeta{Name: "hostnet", Namespace: metav1.NamespaceDefault}, + Spec: apps.DaemonSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: validSelector}, + Template: validHostNetPodTemplate.Template, + UpdateStrategy: apps.DaemonSetUpdateStrategy{ + Type: apps.OnDeleteDaemonSetStrategyType, + }, + }, }, } for _, successCase := range successCases { @@ -2187,8 +2247,8 @@ func TestValidateDaemonSet(t *testing.T) { } } -func validDeployment() *apps.Deployment { - return &apps.Deployment{ +func validDeployment(tweaks ...func(d *apps.Deployment)) *apps.Deployment { + d := &apps.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, @@ -2230,11 +2290,26 @@ func validDeployment() *apps.Deployment { }, }, } + + for _, tweak := range tweaks { + tweak(d) + } + + return d } func TestValidateDeployment(t *testing.T) { successCases := []*apps.Deployment{ validDeployment(), + validDeployment(func(d *apps.Deployment) { + d.Spec.Template.Spec.SecurityContext = &api.PodSecurityContext{ + HostNetwork: true, + } + d.Spec.Template.Spec.Containers[0].Ports = []api.ContainerPort{{ + ContainerPort: 12345, + Protocol: api.ProtocolTCP, + }} + }), } for _, successCase := range successCases { if errs := ValidateDeployment(successCase, corevalidation.PodValidationOptions{}); len(errs) != 0 { @@ -3174,6 +3249,30 @@ func TestValidateReplicaSet(t *testing.T) { }, }, } + validHostNetPodTemplate := api.PodTemplate{ + Template: api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: validLabels, + }, + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{ + HostNetwork: true, + }, + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{ + Name: "abc", + Image: "image", + ImagePullPolicy: "IfNotPresent", + TerminationMessagePolicy: api.TerminationMessageReadFile, + Ports: []api.ContainerPort{{ + ContainerPort: 12345, + Protocol: api.ProtocolTCP, + }}, + }}, + }, + }, + } readWriteVolumePodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -3211,6 +3310,12 @@ func TestValidateReplicaSet(t *testing.T) { Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, }, + }, { + ObjectMeta: metav1.ObjectMeta{Name: "hostnet", Namespace: metav1.NamespaceDefault}, + Spec: apps.ReplicaSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: validLabels}, + Template: validHostNetPodTemplate.Template, + }, }, { ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ diff --git a/pkg/apis/batch/validation/validation_test.go b/pkg/apis/batch/validation/validation_test.go index 630540c6ea8..b5ff032ffd6 100644 --- a/pkg/apis/batch/validation/validation_test.go +++ b/pkg/apis/batch/validation/validation_test.go @@ -99,6 +99,17 @@ func TestValidateJob(t *testing.T) { validPodTemplateSpecForGenerated := getValidPodTemplateSpecForGenerated(validGeneratedSelector) validPodTemplateSpecForGeneratedRestartPolicyNever := getValidPodTemplateSpecForGenerated(validGeneratedSelector) validPodTemplateSpecForGeneratedRestartPolicyNever.Spec.RestartPolicy = api.RestartPolicyNever + validHostNetPodTemplateSpec := func() api.PodTemplateSpec { + spec := getValidPodTemplateSpecForGenerated(validGeneratedSelector) + spec.Spec.SecurityContext = &api.PodSecurityContext{ + HostNetwork: true, + } + spec.Spec.Containers[0].Ports = []api.ContainerPort{{ + ContainerPort: 12345, + Protocol: api.ProtocolTCP, + }} + return spec + }() successCases := map[string]struct { opts JobValidationOptions @@ -179,6 +190,20 @@ func TestValidateJob(t *testing.T) { }, }, }, + "valid hostnet": { + opts: JobValidationOptions{RequirePrefixedLabels: true}, + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validHostNetPodTemplateSpec, + }, + }, + }, "valid NonIndexed completion mode": { opts: JobValidationOptions{RequirePrefixedLabels: true}, job: batch.Job{ @@ -1784,6 +1809,17 @@ func TestValidateCronJob(t *testing.T) { validManualSelector := getValidManualSelector() validPodTemplateSpec := getValidPodTemplateSpecForGenerated(getValidGeneratedSelector()) validPodTemplateSpec.Labels = map[string]string{} + validHostNetPodTemplateSpec := func() api.PodTemplateSpec { + spec := getValidPodTemplateSpecForGenerated(getValidGeneratedSelector()) + spec.Spec.SecurityContext = &api.PodSecurityContext{ + HostNetwork: true, + } + spec.Spec.Containers[0].Ports = []api.ContainerPort{{ + ContainerPort: 12345, + Protocol: api.ProtocolTCP, + }} + return spec + }() successCases := map[string]batch.CronJob{ "basic scheduled job": { @@ -1802,6 +1838,22 @@ func TestValidateCronJob(t *testing.T) { }, }, }, + "hostnet job": { + ObjectMeta: metav1.ObjectMeta{ + Name: "mycronjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.CronJobSpec{ + Schedule: "* * * * ?", + ConcurrencyPolicy: batch.AllowConcurrent, + JobTemplate: batch.JobTemplateSpec{ + Spec: batch.JobSpec{ + Template: validHostNetPodTemplateSpec, + }, + }, + }, + }, "non-standard scheduled": { ObjectMeta: metav1.ObjectMeta{ Name: "mycronjob", diff --git a/pkg/apis/core/v1/defaults.go b/pkg/apis/core/v1/defaults.go index b7ce699ef36..51337fe169c 100644 --- a/pkg/apis/core/v1/defaults.go +++ b/pkg/apis/core/v1/defaults.go @@ -199,6 +199,11 @@ func SetDefaults_Pod(obj *v1.Pod) { enableServiceLinks := v1.DefaultEnableServiceLinks obj.Spec.EnableServiceLinks = &enableServiceLinks } + + if obj.Spec.HostNetwork { + defaultHostNetworkPorts(&obj.Spec.Containers) + defaultHostNetworkPorts(&obj.Spec.InitContainers) + } } func SetDefaults_PodSpec(obj *v1.PodSpec) { // New fields added here will break upgrade tests: @@ -211,9 +216,11 @@ func SetDefaults_PodSpec(obj *v1.PodSpec) { if obj.RestartPolicy == "" { obj.RestartPolicy = v1.RestartPolicyAlways } - if obj.HostNetwork { - defaultHostNetworkPorts(&obj.Containers) - defaultHostNetworkPorts(&obj.InitContainers) + if utilfeature.DefaultFeatureGate.Enabled(features.DefaultHostNetworkHostPortsInPodTemplates) { + if obj.HostNetwork { + defaultHostNetworkPorts(&obj.Containers) + defaultHostNetworkPorts(&obj.InitContainers) + } } if obj.SecurityContext == nil { obj.SecurityContext = &v1.PodSecurityContext{} diff --git a/pkg/apis/core/v1/defaults_test.go b/pkg/apis/core/v1/defaults_test.go index 3059674a843..e5ff6404bc5 100644 --- a/pkg/apis/core/v1/defaults_test.go +++ b/pkg/apis/core/v1/defaults_test.go @@ -47,14 +47,12 @@ func TestWorkloadDefaults(t *testing.T) { t.Run("disabled_features", func(t *testing.T) { testWorkloadDefaults(t, false) }) } func testWorkloadDefaults(t *testing.T, featuresEnabled bool) { - features := utilfeature.DefaultFeatureGate.DeepCopy().GetAll() - for feature, featureSpec := range features { + allFeatures := utilfeature.DefaultFeatureGate.DeepCopy().GetAll() + for feature, featureSpec := range allFeatures { if !featureSpec.LockToDefault { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, feature, featuresEnabled)() } } - rc := &v1.ReplicationController{Spec: v1.ReplicationControllerSpec{Template: &v1.PodTemplateSpec{}}} - template := rc.Spec.Template // New defaults under PodTemplateSpec are only acceptable if they would not be applied when reading data from a previous release. // Forbidden: adding a new field `MyField *bool` and defaulting it to a non-nil value // Forbidden: defaulting an existing field `MyField *bool` when it was previously not defaulted @@ -177,11 +175,59 @@ func testWorkloadDefaults(t *testing.T, featuresEnabled bool) { ".Spec.Volumes[0].VolumeSource.ScaleIO.StorageMode": `"ThinProvisioned"`, ".Spec.Volumes[0].VolumeSource.Secret.DefaultMode": `420`, } - defaults := detectDefaults(t, rc, reflect.ValueOf(template)) - if !reflect.DeepEqual(expectedDefaults, defaults) { - t.Errorf("Defaults for PodTemplateSpec changed. This can cause spurious rollouts of workloads on API server upgrade.") - t.Logf(cmp.Diff(expectedDefaults, defaults)) - } + t.Run("empty PodTemplateSpec", func(t *testing.T) { + rc := &v1.ReplicationController{Spec: v1.ReplicationControllerSpec{Template: &v1.PodTemplateSpec{}}} + template := rc.Spec.Template + defaults := detectDefaults(t, rc, reflect.ValueOf(template)) + if !reflect.DeepEqual(expectedDefaults, defaults) { + t.Errorf("Defaults for PodTemplateSpec changed. This can cause spurious rollouts of workloads on API server upgrade.") + t.Logf(cmp.Diff(expectedDefaults, defaults)) + } + }) + t.Run("hostnet PodTemplateSpec with ports", func(t *testing.T) { + rc := &v1.ReplicationController{ + Spec: v1.ReplicationControllerSpec{ + Template: &v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + HostNetwork: true, + Containers: []v1.Container{{ + Ports: []v1.ContainerPort{{ + ContainerPort: 12345, + Protocol: v1.ProtocolTCP, + }}, + }}, + }, + }, + }, + } + template := rc.Spec.Template + defaults := detectDefaults(t, rc, reflect.ValueOf(template)) + expected := func() map[string]string { + // Set values that are known inputs + m := map[string]string{ + ".Spec.HostNetwork": "true", + ".Spec.Containers[0].Ports[0].ContainerPort": "12345", + } + if utilfeature.DefaultFeatureGate.Enabled(features.DefaultHostNetworkHostPortsInPodTemplates) { + m[".Spec.Containers"] = `[{"name":"","ports":[{"hostPort":12345,"containerPort":12345,"protocol":"TCP"}],"resources":{},"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"IfNotPresent"}]` + m[".Spec.Containers[0].Ports"] = `[{"hostPort":12345,"containerPort":12345,"protocol":"TCP"}]` + m[".Spec.Containers[0].Ports[0].HostPort"] = "12345" + } else { + m[".Spec.Containers"] = `[{"name":"","ports":[{"containerPort":12345,"protocol":"TCP"}],"resources":{},"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"IfNotPresent"}]` + m[".Spec.Containers[0].Ports"] = `[{"containerPort":12345,"protocol":"TCP"}]` + } + for k, v := range expectedDefaults { + if _, found := m[k]; !found { + m[k] = v + } + } + return m + }() + if !reflect.DeepEqual(expected, defaults) { + t.Errorf("Defaults for PodTemplateSpec changed. This can cause spurious rollouts of workloads on API server upgrade.") + t.Logf(cmp.Diff(expected, defaults)) + } + }) } // TestPodDefaults detects changes to defaults within PodSpec. @@ -330,6 +376,78 @@ func testPodDefaults(t *testing.T, featuresEnabled bool) { } } +func TestPodHostNetworkDefaults(t *testing.T) { + cases := []struct { + name string + gate bool + hostNet bool + expectPodDefault bool + expectPodSpecDefault bool + }{{ + name: "gate disabled, hostNetwork=false", + gate: false, + hostNet: false, + expectPodDefault: false, + expectPodSpecDefault: false, + }, { + name: "gate disabled, hostNetwork=true", + gate: false, + hostNet: true, + expectPodDefault: true, + expectPodSpecDefault: false, + }, { + name: "gate enabled, hostNetwork=false", + gate: true, + hostNet: false, + expectPodDefault: false, + expectPodSpecDefault: false, + }, { + name: "gate enabled, hostNetwork=true", + gate: true, + hostNet: true, + expectPodDefault: true, + expectPodSpecDefault: true, + }} + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DefaultHostNetworkHostPortsInPodTemplates, tc.gate)() + + const portNum = 12345 + spec := v1.PodSpec{ + HostNetwork: tc.hostNet, + Containers: []v1.Container{{ + Ports: []v1.ContainerPort{{ + ContainerPort: portNum, + Protocol: v1.ProtocolTCP, + // Note: HostPort is not set + }}, + }}, + } + + // Test Pod defaulting. + p := v1.Pod{Spec: *spec.DeepCopy()} + corev1.SetDefaults_Pod(&p) + if got := p.Spec.Containers[0].Ports[0].HostPort; tc.expectPodDefault && got == 0 { + t.Errorf("expected Pod HostPort to be defaulted, got %v", got) + } + if got := p.Spec.Containers[0].Ports[0].HostPort; !tc.expectPodDefault && got != 0 { + t.Errorf("expected Pod HostPort to be 0, got %v", got) + } + + // Test PodSpec defaulting. + s := spec.DeepCopy() + corev1.SetDefaults_PodSpec(s) + if got := s.Containers[0].Ports[0].HostPort; tc.expectPodSpecDefault && got == 0 { + t.Errorf("expected PodSpec HostPort to be defaulted, got %v", got) + } + if got := s.Containers[0].Ports[0].HostPort; !tc.expectPodSpecDefault && got != 0 { + t.Errorf("expected PodSpec HostPort to be 0, got %v", got) + } + }) + } +} + type testPath struct { path string value reflect.Value @@ -359,9 +477,12 @@ func detectDefaults(t *testing.T, obj runtime.Object, v reflect.Value) map[strin case visit.value.Kind() == reflect.Slice: if !visit.value.IsNil() { - // if we already have a value, we got defaulted + // if we already have a value, we either got defaulted or there + // was a fixed input - flag it and see if we can descend + // anyway. marshaled, _ := json.Marshal(defaultedV.Interface()) defaults[visit.path] = string(marshaled) + toVisit = append(toVisit, testPath{path: visit.path + "[0]", value: visit.value.Index(0)}) } else if visit.value.Type().Elem().Kind() == reflect.Struct { if strings.HasPrefix(visit.path, ".ObjectMeta.ManagedFields[") { break diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go index bb39e507abb..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,19 +3714,23 @@ 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, // and is called by ValidatePodCreate and ValidatePodUpdate. func validatePodMetadataAndSpec(pod *core.Pod, opts PodValidationOptions) field.ErrorList { - fldPath := field.NewPath("metadata") - allErrs := ValidateObjectMeta(&pod.ObjectMeta, true, ValidatePodName, fldPath) - allErrs = append(allErrs, ValidatePodSpecificAnnotations(pod.ObjectMeta.Annotations, &pod.Spec, fldPath.Child("annotations"), opts)...) - allErrs = append(allErrs, ValidatePodSpec(&pod.Spec, &pod.ObjectMeta, field.NewPath("spec"), opts)...) + metaPath := field.NewPath("metadata") + specPath := field.NewPath("spec") + + allErrs := ValidateObjectMeta(&pod.ObjectMeta, true, ValidatePodName, metaPath) + allErrs = append(allErrs, ValidatePodSpecificAnnotations(pod.ObjectMeta.Annotations, &pod.Spec, metaPath.Child("annotations"), opts)...) + allErrs = append(allErrs, ValidatePodSpec(&pod.Spec, &pod.ObjectMeta, specPath, opts)...) // we do additional validation only pertinent for pods and not pod templates // this was done to preserve backwards compatibility - specPath := field.NewPath("spec") if pod.Spec.ServiceAccountName == "" { for vi, volume := range pod.Spec.Volumes { @@ -3790,10 +3814,11 @@ 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"))...) - allErrs = append(allErrs, ValidatePodSecurityContext(spec.SecurityContext, spec, fldPath, fldPath.Child("securityContext"), opts)...) + allErrs = append(allErrs, validatePodSpecSecurityContext(spec.SecurityContext, spec, fldPath, fldPath.Child("securityContext"), opts)...) allErrs = append(allErrs, validateImagePullSecrets(spec.ImagePullSecrets, fldPath.Child("imagePullSecrets"))...) allErrs = append(allErrs, validateAffinity(spec.Affinity, opts, fldPath.Child("affinity"))...) allErrs = append(allErrs, validatePodDNSConfig(spec.DNSConfig, &spec.DNSPolicy, fldPath.Child("dnsConfig"), opts)...) @@ -4396,12 +4421,13 @@ func validateSysctls(sysctls []core.Sysctl, fldPath *field.Path) field.ErrorList return allErrs } -// ValidatePodSecurityContext test that the specified PodSecurityContext has valid data. -func ValidatePodSecurityContext(securityContext *core.PodSecurityContext, spec *core.PodSpec, specPath, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { +// 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). +func validatePodSpecSecurityContext(securityContext *core.PodSecurityContext, spec *core.PodSpec, specPath, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { allErrs := field.ErrorList{} if securityContext != nil { - allErrs = append(allErrs, validateHostNetwork(securityContext.HostNetwork, spec.Containers, specPath.Child("containers"))...) if securityContext.FSGroup != nil { for _, msg := range validation.IsValidGroupID(*securityContext.FSGroup) { allErrs = append(allErrs, field.Invalid(fldPath.Child("fsGroup"), *(securityContext.FSGroup), msg)) diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go index 5caae4e1354..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{ @@ -8895,7 +8910,6 @@ func TestValidatePodSpec(t *testing.T) { "bad supplementalGroups large than math.MaxInt32": { Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, SecurityContext: &core.PodSecurityContext{ - HostNetwork: false, SupplementalGroups: []int64{maxGroupID, 1234}, }, RestartPolicy: core.RestartPolicyAlways, @@ -8904,7 +8918,6 @@ func TestValidatePodSpec(t *testing.T) { "bad supplementalGroups less than 0": { Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, SecurityContext: &core.PodSecurityContext{ - HostNetwork: false, SupplementalGroups: []int64{minGroupID, 1234}, }, RestartPolicy: core.RestartPolicyAlways, @@ -8913,8 +8926,7 @@ func TestValidatePodSpec(t *testing.T) { "bad runAsUser large than math.MaxInt32": { Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, SecurityContext: &core.PodSecurityContext{ - HostNetwork: false, - RunAsUser: &maxUserID, + RunAsUser: &maxUserID, }, RestartPolicy: core.RestartPolicyAlways, DNSPolicy: core.DNSClusterFirst, @@ -8922,8 +8934,7 @@ func TestValidatePodSpec(t *testing.T) { "bad runAsUser less than 0": { Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, SecurityContext: &core.PodSecurityContext{ - HostNetwork: false, - RunAsUser: &minUserID, + RunAsUser: &minUserID, }, RestartPolicy: core.RestartPolicyAlways, DNSPolicy: core.DNSClusterFirst, @@ -8931,8 +8942,7 @@ func TestValidatePodSpec(t *testing.T) { "bad fsGroup large than math.MaxInt32": { Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, SecurityContext: &core.PodSecurityContext{ - HostNetwork: false, - FSGroup: &maxGroupID, + FSGroup: &maxGroupID, }, RestartPolicy: core.RestartPolicyAlways, DNSPolicy: core.DNSClusterFirst, @@ -8940,8 +8950,7 @@ func TestValidatePodSpec(t *testing.T) { "bad fsGroup less than 0": { Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, SecurityContext: &core.PodSecurityContext{ - HostNetwork: false, - FSGroup: &minGroupID, + FSGroup: &minGroupID, }, RestartPolicy: core.RestartPolicyAlways, DNSPolicy: core.DNSClusterFirst, @@ -9042,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) } } @@ -15381,6 +15393,30 @@ func TestValidateReplicationController(t *testing.T) { }, }, } + hostnetPodTemplate := core.PodTemplate{ + Template: core.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: validSelector, + }, + Spec: core.PodSpec{ + SecurityContext: &core.PodSecurityContext{ + HostNetwork: true, + }, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSClusterFirst, + Containers: []core.Container{{ + Name: "abc", + Image: "image", + ImagePullPolicy: "IfNotPresent", + TerminationMessagePolicy: "File", + Ports: []core.ContainerPort{{ + ContainerPort: 12345, + Protocol: core.ProtocolTCP, + }}, + }}, + }, + }, + } invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"} invalidPodTemplate := core.PodTemplate{ Template: core.PodTemplateSpec{ @@ -15412,8 +15448,14 @@ func TestValidateReplicationController(t *testing.T) { Selector: validSelector, Template: &readWriteVolumePodTemplate.Template, }, - }, - } + }, { + ObjectMeta: metav1.ObjectMeta{Name: "hostnet", Namespace: metav1.NamespaceDefault}, + Spec: core.ReplicationControllerSpec{ + Replicas: 1, + Selector: validSelector, + Template: &hostnetPodTemplate.Template, + }, + }} for _, successCase := range successCases { if errs := ValidateReplicationController(&successCase, PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success: %v", errs) diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index 362acbf173f..f4424491cef 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -212,6 +212,15 @@ const ( // Enables support for time zones in CronJobs. CronJobTimeZone featuregate.Feature = "CronJobTimeZone" + // owner: @thockin + // deprecated: v1.28 + // + // Changes when the default value of PodSpec.containers[].ports[].hostPort + // is assigned. The default is to only set a default value in Pods. + // Enabling this means a default will be assigned even to embeddedPodSpecs + // (e.g. in a Deployment), which is the historical default. + DefaultHostNetworkHostPortsInPodTemplates featuregate.Feature = "DefaultHostNetworkHostPortsInPodTemplates" + // owner: @andrewsykim // alpha: v1.22 // @@ -893,6 +902,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS CronJobTimeZone: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.29 + DefaultHostNetworkHostPortsInPodTemplates: {Default: false, PreRelease: featuregate.Deprecated}, + DisableCloudProviders: {Default: false, PreRelease: featuregate.Alpha}, DisableKubeletCloudCredentialProviders: {Default: false, PreRelease: featuregate.Alpha}, 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) }