diff --git a/pkg/api/pod/util.go b/pkg/api/pod/util.go index 4a71f896274..feae040b4fb 100644 --- a/pkg/api/pod/util.go +++ b/pkg/api/pod/util.go @@ -244,6 +244,10 @@ func DropDisabledAlphaFields(podSpec *api.PodSpec) { } } + if !utilfeature.DefaultFeatureGate.Enabled(features.PodShareProcessNamespace) && podSpec.SecurityContext != nil { + podSpec.SecurityContext.ShareProcessNamespace = nil + } + for i := range podSpec.Containers { DropDisabledVolumeMountsAlphaFields(podSpec.Containers[i].VolumeMounts) } diff --git a/pkg/apis/core/types.go b/pkg/apis/core/types.go index cc5c7740d48..c3968ca6024 100644 --- a/pkg/apis/core/types.go +++ b/pkg/apis/core/types.go @@ -2644,6 +2644,15 @@ type PodSecurityContext struct { // +k8s:conversion-gen=false // +optional HostIPC bool + // Share a single process namespace between all of the containers in a pod. + // When this is set containers will be able to view and signal processes from other containers + // in the same pod, and the first process in each container will not be assigned PID 1. + // HostPID and ShareProcessNamespace cannot both be set. + // Optional: Default to false. + // This field is alpha-level and is honored only by servers that enable the PodShareProcessNamespace feature. + // +k8s:conversion-gen=false + // +optional + ShareProcessNamespace *bool // The SELinux context to be applied to all containers. // If unspecified, the container runtime will allocate a random SELinux context for each // container. May also be set in SecurityContext. If set in diff --git a/pkg/apis/core/v1/conversion.go b/pkg/apis/core/v1/conversion.go index 42d28609220..d888ebca445 100644 --- a/pkg/apis/core/v1/conversion.go +++ b/pkg/apis/core/v1/conversion.go @@ -384,6 +384,7 @@ func Convert_core_PodSpec_To_v1_PodSpec(in *core.PodSpec, out *v1.PodSpec, s con out.HostPID = in.SecurityContext.HostPID out.HostNetwork = in.SecurityContext.HostNetwork out.HostIPC = in.SecurityContext.HostIPC + out.ShareProcessNamespace = in.SecurityContext.ShareProcessNamespace } return nil @@ -408,6 +409,7 @@ func Convert_v1_PodSpec_To_core_PodSpec(in *v1.PodSpec, out *core.PodSpec, s con out.SecurityContext.HostNetwork = in.HostNetwork out.SecurityContext.HostPID = in.HostPID out.SecurityContext.HostIPC = in.HostIPC + out.SecurityContext.ShareProcessNamespace = in.ShareProcessNamespace return nil } diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go index 68b3d0056cb..f692018647d 100644 --- a/pkg/apis/core/validation/validation.go +++ b/pkg/apis/core/validation/validation.go @@ -3264,6 +3264,13 @@ func ValidatePodSecurityContext(securityContext *core.PodSecurityContext, spec * allErrs = append(allErrs, field.Invalid(fldPath.Child("supplementalGroups").Index(g), gid, msg)) } } + if securityContext.ShareProcessNamespace != nil { + if !utilfeature.DefaultFeatureGate.Enabled(features.PodShareProcessNamespace) { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("shareProcessNamespace"), "Process Namespace Sharing is disabled by PodShareProcessNamespace feature-gate")) + } else if securityContext.HostPID && *securityContext.ShareProcessNamespace { + allErrs = append(allErrs, field.Invalid(fldPath.Child("shareProcessNamespace"), *securityContext.ShareProcessNamespace, "ShareProcessNamespace and HostPID cannot both be enabled")) + } + } } return allErrs diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go index 460805569da..ce19b450e61 100644 --- a/pkg/apis/core/validation/validation_test.go +++ b/pkg/apis/core/validation/validation_test.go @@ -31,11 +31,13 @@ import ( "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" utilfeature "k8s.io/apiserver/pkg/util/feature" + utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing" "k8s.io/kubernetes/pkg/api/legacyscheme" _ "k8s.io/kubernetes/pkg/api/testapi" "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/apis/core/helper" "k8s.io/kubernetes/pkg/capabilities" + "k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/security/apparmor" ) @@ -5748,24 +5750,9 @@ func TestValidatePodSpec(t *testing.T) { minGroupID := int64(0) maxGroupID := int64(2147483647) - priorityEnabled := utilfeature.DefaultFeatureGate.Enabled("PodPriority") - defer func() { - var err error - // restoring the old value - if priorityEnabled { - err = utilfeature.DefaultFeatureGate.Set("PodPriority=true") - } else { - err = utilfeature.DefaultFeatureGate.Set("PodPriority=false") - } - if err != nil { - t.Errorf("Failed to restore feature gate for PodPriority: %v", err) - } - }() - err := utilfeature.DefaultFeatureGate.Set("PodPriority=true") - if err != nil { - t.Errorf("Failed to enable feature gate for PodPriority: %v", err) - return - } + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodPriority, true)() + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodShareProcessNamespace, true)() + successCases := []core.PodSpec{ { // Populate basic fields, leave defaults for most. Volumes: []core.Volume{{Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}}, @@ -5890,6 +5877,15 @@ func TestValidatePodSpec(t *testing.T) { DNSPolicy: core.DNSClusterFirst, PriorityClassName: "valid-name", }, + { // Populate ShareProcessNamespace + Volumes: []core.Volume{{Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}}, + Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSClusterFirst, + SecurityContext: &core.PodSecurityContext{ + ShareProcessNamespace: &[]bool{true}[0], + }, + }, } for i := range successCases { if errs := ValidatePodSpec(&successCases[i], field.NewPath("field")); len(errs) != 0 { @@ -6061,12 +6057,42 @@ func TestValidatePodSpec(t *testing.T) { DNSPolicy: core.DNSClusterFirst, PriorityClassName: "InvalidName", }, + "ShareProcessNamespace and HostPID both set": { + Volumes: []core.Volume{{Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}}, + Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSClusterFirst, + SecurityContext: &core.PodSecurityContext{ + HostPID: true, + ShareProcessNamespace: &[]bool{true}[0], + }, + }, } for k, v := range failureCases { if errs := ValidatePodSpec(&v, field.NewPath("field")); len(errs) == 0 { t.Errorf("expected failure for %q", k) } } + + // original value will be restored by previous defer + utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodShareProcessNamespace, false) + + featuregatedCases := map[string]core.PodSpec{ + "set ShareProcessNamespace": { + Volumes: []core.Volume{{Name: "vol", VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{}}}}, + Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSClusterFirst, + SecurityContext: &core.PodSecurityContext{ + ShareProcessNamespace: &[]bool{true}[0], + }, + }, + } + for k, v := range featuregatedCases { + if errs := ValidatePodSpec(&v, field.NewPath("field")); len(errs) == 0 { + t.Errorf("expected failure due to gated feature: %q", k) + } + } } func extendPodSpecwithTolerations(in core.PodSpec, tolerations []core.Toleration) core.PodSpec { diff --git a/staging/src/k8s.io/api/core/v1/types.go b/staging/src/k8s.io/api/core/v1/types.go index 36219b1a2c3..f0693400ed4 100644 --- a/staging/src/k8s.io/api/core/v1/types.go +++ b/staging/src/k8s.io/api/core/v1/types.go @@ -2861,6 +2861,15 @@ type PodSpec struct { // +k8s:conversion-gen=false // +optional HostIPC bool `json:"hostIPC,omitempty" protobuf:"varint,13,opt,name=hostIPC"` + // Share a single process namespace between all of the containers in a pod. + // When this is set containers will be able to view and signal processes from other containers + // in the same pod, and the first process in each container will not be assigned PID 1. + // HostPID and ShareProcessNamespace cannot both be set. + // Optional: Default to false. + // This field is alpha-level and is honored only by servers that enable the PodShareProcessNamespace feature. + // +k8s:conversion-gen=false + // +optional + ShareProcessNamespace *bool `json:"shareProcessNamespace,omitempty" protobuf:"varint,27,opt,name=shareProcessNamespace"` // SecurityContext holds pod-level security attributes and common container settings. // Optional: Defaults to empty. See type description for default values of each field. // +optional