diff --git a/pkg/api/pod/testing/make.go b/pkg/api/pod/testing/make.go index e338bcb024a..552d795ff78 100644 --- a/pkg/api/pod/testing/make.go +++ b/pkg/api/pod/testing/make.go @@ -270,6 +270,12 @@ func SetContainerImage(image string) TweakContainer { } } +func SetContainerLifecycle(lifecycle api.Lifecycle) TweakContainer { + return func(cnr *api.Container) { + cnr.Lifecycle = &lifecycle + } +} + func MakeResourceRequirements(requests, limits map[string]string) api.ResourceRequirements { rr := api.ResourceRequirements{Requests: api.ResourceList{}, Limits: api.ResourceList{}} for k, v := range requests { diff --git a/pkg/api/pod/util.go b/pkg/api/pod/util.go index 062272c844a..c23714d0c95 100644 --- a/pkg/api/pod/util.go +++ b/pkg/api/pod/util.go @@ -678,6 +678,51 @@ func dropDisabledFields( dropPodLifecycleSleepAction(podSpec, oldPodSpec) dropImageVolumes(podSpec, oldPodSpec) dropSELinuxChangePolicy(podSpec, oldPodSpec) + dropContainerStopSignals(podSpec, oldPodSpec) +} + +func dropContainerStopSignals(podSpec, oldPodSpec *api.PodSpec) { + if utilfeature.DefaultFeatureGate.Enabled(features.ContainerStopSignals) || containerStopSignalsInUse(oldPodSpec) { + return + } + + wipeLifecycle := func(ctr *api.Container) { + if ctr.Lifecycle == nil { + return + } + if ctr.Lifecycle.StopSignal != nil { + ctr.Lifecycle.StopSignal = nil + if *ctr.Lifecycle == (api.Lifecycle{}) { + ctr.Lifecycle = nil + } + } + } + + VisitContainers(podSpec, AllContainers, func(c *api.Container, containerType ContainerType) bool { + if c.Lifecycle == nil { + return true + } + wipeLifecycle(c) + return true + }) +} + +func containerStopSignalsInUse(podSpec *api.PodSpec) bool { + if podSpec == nil { + return false + } + var inUse bool + VisitContainers(podSpec, AllContainers, func(c *api.Container, containerType ContainerType) bool { + if c.Lifecycle == nil { + return true + } + if c.Lifecycle.StopSignal != nil { + inUse = true + return false + } + return true + }) + return inUse } func dropDisabledPodLevelResources(podSpec, oldPodSpec *api.PodSpec) { @@ -713,7 +758,7 @@ func dropPodLifecycleSleepAction(podSpec, oldPodSpec *api.PodSpec) { continue } adjustLifecycle(podSpec.Containers[i].Lifecycle) - if podSpec.Containers[i].Lifecycle.PreStop == nil && podSpec.Containers[i].Lifecycle.PostStart == nil { + if podSpec.Containers[i].Lifecycle.PreStop == nil && podSpec.Containers[i].Lifecycle.PostStart == nil && podSpec.Containers[i].Lifecycle.StopSignal == nil { podSpec.Containers[i].Lifecycle = nil } } @@ -723,7 +768,7 @@ func dropPodLifecycleSleepAction(podSpec, oldPodSpec *api.PodSpec) { continue } adjustLifecycle(podSpec.InitContainers[i].Lifecycle) - if podSpec.InitContainers[i].Lifecycle.PreStop == nil && podSpec.InitContainers[i].Lifecycle.PostStart == nil { + if podSpec.InitContainers[i].Lifecycle.PreStop == nil && podSpec.InitContainers[i].Lifecycle.PostStart == nil && podSpec.InitContainers[i].Lifecycle.StopSignal == nil { podSpec.InitContainers[i].Lifecycle = nil } } @@ -733,7 +778,7 @@ func dropPodLifecycleSleepAction(podSpec, oldPodSpec *api.PodSpec) { continue } adjustLifecycle(podSpec.EphemeralContainers[i].Lifecycle) - if podSpec.EphemeralContainers[i].Lifecycle.PreStop == nil && podSpec.EphemeralContainers[i].Lifecycle.PostStart == nil { + if podSpec.EphemeralContainers[i].Lifecycle.PreStop == nil && podSpec.EphemeralContainers[i].Lifecycle.PostStart == nil && podSpec.EphemeralContainers[i].Lifecycle.StopSignal == nil { podSpec.EphemeralContainers[i].Lifecycle = nil } } diff --git a/pkg/api/pod/util_test.go b/pkg/api/pod/util_test.go index acb6305a02e..8f8de2fc8b3 100644 --- a/pkg/api/pod/util_test.go +++ b/pkg/api/pod/util_test.go @@ -3436,6 +3436,148 @@ func TestDropPodLifecycleSleepAction(t *testing.T) { } } +func TestDropContainerStopSignals(t *testing.T) { + makeContainer := func(lifecycle *api.Lifecycle) api.Container { + container := api.Container{Name: "foo"} + if lifecycle != nil { + container.Lifecycle = lifecycle + } + return container + } + + makeEphemeralContainer := func(lifecycle *api.Lifecycle) api.EphemeralContainer { + container := api.EphemeralContainer{ + EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "foo"}, + } + if lifecycle != nil { + container.Lifecycle = lifecycle + } + return container + } + + makePod := func(os api.OSName, containers []api.Container, initContainers []api.Container, ephemeralContainers []api.EphemeralContainer) *api.PodSpec { + return &api.PodSpec{ + OS: &api.PodOS{Name: os}, + Containers: containers, + InitContainers: initContainers, + EphemeralContainers: ephemeralContainers, + } + } + + testCases := []struct { + featuregateEnabled bool + oldLifecycle *api.Lifecycle + newLifecycle *api.Lifecycle + expectedLifecycle *api.Lifecycle + }{ + // feature gate is turned on and stopsignal is not in use - Lifecycle stays nil + { + featuregateEnabled: true, + oldLifecycle: nil, + newLifecycle: nil, + expectedLifecycle: nil, + }, + // feature gate is turned off and StopSignal is in use - StopSignal is not dropped + { + featuregateEnabled: false, + oldLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM)}, + newLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM)}, + expectedLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM)}, + }, + // feature gate is turned off and StopSignal is not in use - Entire lifecycle is dropped + { + featuregateEnabled: false, + oldLifecycle: &api.Lifecycle{StopSignal: nil}, + newLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM)}, + expectedLifecycle: nil, + }, + // feature gate is turned on and StopSignal is in use - StopSignal is not dropped + { + featuregateEnabled: true, + oldLifecycle: &api.Lifecycle{StopSignal: nil}, + newLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM)}, + expectedLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM)}, + }, + // feature gate is turned off and PreStop is in use - StopSignal alone is dropped + { + featuregateEnabled: false, + oldLifecycle: &api.Lifecycle{StopSignal: nil, PreStop: &api.LifecycleHandler{ + Exec: &api.ExecAction{Command: []string{"foo"}}, + }}, + newLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM), PreStop: &api.LifecycleHandler{ + Exec: &api.ExecAction{Command: []string{"foo"}}, + }}, + expectedLifecycle: &api.Lifecycle{StopSignal: nil, PreStop: &api.LifecycleHandler{ + Exec: &api.ExecAction{Command: []string{"foo"}}, + }}, + }, + // feature gate is turned on and PreStop is in use - StopSignal is not dropped + { + featuregateEnabled: true, + oldLifecycle: &api.Lifecycle{StopSignal: nil, PreStop: &api.LifecycleHandler{ + Exec: &api.ExecAction{Command: []string{"foo"}}, + }}, + newLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM), PreStop: &api.LifecycleHandler{ + Exec: &api.ExecAction{Command: []string{"foo"}}, + }}, + expectedLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM), PreStop: &api.LifecycleHandler{ + Exec: &api.ExecAction{Command: []string{"foo"}}, + }}, + }, + // feature gate is turned off and PreStop and StopSignal are in use - nothing is dropped + { + featuregateEnabled: true, + oldLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM), PreStop: &api.LifecycleHandler{ + Exec: &api.ExecAction{Command: []string{"foo"}}, + }}, + newLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM), PreStop: &api.LifecycleHandler{ + Exec: &api.ExecAction{Command: []string{"foo"}}, + }}, + expectedLifecycle: &api.Lifecycle{StopSignal: ptr.To(api.SIGTERM), PreStop: &api.LifecycleHandler{ + Exec: &api.ExecAction{Command: []string{"foo"}}, + }}, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ContainerStopSignals, tc.featuregateEnabled) + // Containers + { + oldPod := makePod(api.Linux, []api.Container{makeContainer(tc.oldLifecycle.DeepCopy())}, nil, nil) + newPod := makePod(api.Linux, []api.Container{makeContainer(tc.newLifecycle.DeepCopy())}, nil, nil) + expectedPod := makePod(api.Linux, []api.Container{makeContainer(tc.expectedLifecycle.DeepCopy())}, nil, nil) + dropDisabledFields(newPod, nil, oldPod, nil) + + if diff := cmp.Diff(expectedPod, newPod); diff != "" { + t.Fatalf("Unexpected modification to new pod; diff (-got +want)\n%s", diff) + } + } + // InitContainers + { + oldPod := makePod(api.Linux, nil, []api.Container{makeContainer(tc.oldLifecycle.DeepCopy())}, nil) + newPod := makePod(api.Linux, nil, []api.Container{makeContainer(tc.newLifecycle.DeepCopy())}, nil) + expectPod := makePod(api.Linux, nil, []api.Container{makeContainer(tc.expectedLifecycle.DeepCopy())}, nil) + dropDisabledFields(newPod, nil, oldPod, nil) + if diff := cmp.Diff(expectPod, newPod); diff != "" { + t.Fatalf("Unexpected modification to new pod; diff (-got +want)\n%s", diff) + } + } + // EphemeralContainers + { + oldPod := makePod(api.Linux, nil, nil, []api.EphemeralContainer{makeEphemeralContainer(tc.oldLifecycle.DeepCopy())}) + newPod := makePod(api.Linux, nil, nil, []api.EphemeralContainer{makeEphemeralContainer(tc.newLifecycle.DeepCopy())}) + expectPod := makePod(api.Linux, nil, nil, []api.EphemeralContainer{makeEphemeralContainer(tc.expectedLifecycle.DeepCopy())}) + dropDisabledFields(newPod, nil, oldPod, nil) + if diff := cmp.Diff(expectPod, newPod); diff != "" { + t.Fatalf("Unexpected modification to new pod; diff (-got +want)\n%s", diff) + } + } + + }) + } +} + func TestDropSupplementalGroupsPolicy(t *testing.T) { supplementalGroupsPolicyMerge := api.SupplementalGroupsPolicyMerge podWithSupplementalGroupsPolicy := func() *api.Pod { diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go index e00e4daf663..fa96a723244 100644 --- a/pkg/apis/core/validation/validation.go +++ b/pkg/apis/core/validation/validation.go @@ -3343,7 +3343,48 @@ func validateHandler(handler commonHandler, gracePeriod *int64, fldPath *field.P return allErrors } -func validateLifecycle(lifecycle *core.Lifecycle, gracePeriod *int64, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { +var supportedStopSignalsLinux = sets.New( + core.SIGABRT, core.SIGALRM, core.SIGBUS, core.SIGCHLD, + core.SIGCLD, core.SIGCONT, core.SIGFPE, core.SIGHUP, + core.SIGILL, core.SIGINT, core.SIGIO, core.SIGIOT, + core.SIGKILL, core.SIGPIPE, core.SIGPOLL, core.SIGPROF, + core.SIGPWR, core.SIGQUIT, core.SIGSEGV, core.SIGSTKFLT, + core.SIGSTOP, core.SIGSYS, core.SIGTERM, core.SIGTRAP, + core.SIGTSTP, core.SIGTTIN, core.SIGTTOU, core.SIGURG, + core.SIGUSR1, core.SIGUSR2, core.SIGVTALRM, core.SIGWINCH, + core.SIGXCPU, core.SIGXFSZ, core.SIGRTMIN, core.SIGRTMINPLUS1, + core.SIGRTMINPLUS2, core.SIGRTMINPLUS3, core.SIGRTMINPLUS4, + core.SIGRTMINPLUS5, core.SIGRTMINPLUS6, core.SIGRTMINPLUS7, + core.SIGRTMINPLUS8, core.SIGRTMINPLUS9, core.SIGRTMINPLUS10, + core.SIGRTMINPLUS11, core.SIGRTMINPLUS12, core.SIGRTMINPLUS13, + core.SIGRTMINPLUS14, core.SIGRTMINPLUS15, core.SIGRTMAXMINUS14, + core.SIGRTMAXMINUS13, core.SIGRTMAXMINUS12, core.SIGRTMAXMINUS11, + core.SIGRTMAXMINUS10, core.SIGRTMAXMINUS9, core.SIGRTMAXMINUS8, + core.SIGRTMAXMINUS7, core.SIGRTMAXMINUS6, core.SIGRTMAXMINUS5, + core.SIGRTMAXMINUS4, core.SIGRTMAXMINUS3, core.SIGRTMAXMINUS2, + core.SIGRTMAXMINUS1, core.SIGRTMAX) + +var supportedStopSignalsWindows = sets.New(core.SIGKILL, core.SIGTERM) + +func validateStopSignal(stopSignal *core.Signal, fldPath *field.Path, os *core.PodOS) field.ErrorList { + allErrors := field.ErrorList{} + + if os == nil { + allErrors = append(allErrors, field.Forbidden(fldPath, "may not be set for containers with empty `spec.os.name`")) + } else if os.Name == core.Windows { + if !supportedStopSignalsWindows.Has(*stopSignal) { + allErrors = append(allErrors, field.NotSupported(fldPath, stopSignal, sets.List(supportedStopSignalsWindows))) + } + } else if os.Name == core.Linux { + if !supportedStopSignalsLinux.Has(*stopSignal) { + allErrors = append(allErrors, field.NotSupported(fldPath, stopSignal, sets.List(supportedStopSignalsLinux))) + } + } + + return allErrors +} + +func validateLifecycle(lifecycle *core.Lifecycle, gracePeriod *int64, fldPath *field.Path, opts PodValidationOptions, os *core.PodOS) field.ErrorList { allErrs := field.ErrorList{} if lifecycle.PostStart != nil { allErrs = append(allErrs, validateHandler(handlerFromLifecycle(lifecycle.PostStart), gracePeriod, fldPath.Child("postStart"), opts)...) @@ -3351,6 +3392,9 @@ func validateLifecycle(lifecycle *core.Lifecycle, gracePeriod *int64, fldPath *f if lifecycle.PreStop != nil { allErrs = append(allErrs, validateHandler(handlerFromLifecycle(lifecycle.PreStop), gracePeriod, fldPath.Child("preStop"), opts)...) } + if lifecycle.StopSignal != nil { + allErrs = append(allErrs, validateStopSignal(lifecycle.StopSignal, fldPath.Child("stopSignal"), os)...) + } return allErrs } @@ -3494,7 +3538,7 @@ func validateFieldAllowList(value interface{}, allowedFields map[string]bool, er } // validateInitContainers is called by pod spec and template validation to validate the list of init containers -func validateInitContainers(containers []core.Container, regularContainers []core.Container, volumes map[string]core.VolumeSource, podClaimNames sets.Set[string], gracePeriod *int64, fldPath *field.Path, opts PodValidationOptions, podRestartPolicy *core.RestartPolicy, hostUsers bool) field.ErrorList { +func validateInitContainers(containers []core.Container, os *core.PodOS, regularContainers []core.Container, volumes map[string]core.VolumeSource, podClaimNames sets.Set[string], gracePeriod *int64, fldPath *field.Path, opts PodValidationOptions, podRestartPolicy *core.RestartPolicy, hostUsers bool) field.ErrorList { var allErrs field.ErrorList allNames := sets.Set[string]{} @@ -3528,7 +3572,7 @@ func validateInitContainers(containers []core.Container, regularContainers []cor switch { case restartAlways: if ctr.Lifecycle != nil { - allErrs = append(allErrs, validateLifecycle(ctr.Lifecycle, gracePeriod, idxPath.Child("lifecycle"), opts)...) + allErrs = append(allErrs, validateLifecycle(ctr.Lifecycle, gracePeriod, idxPath.Child("lifecycle"), opts, os)...) } allErrs = append(allErrs, validateLivenessProbe(ctr.LivenessProbe, gracePeriod, idxPath.Child("livenessProbe"), opts)...) allErrs = append(allErrs, validateReadinessProbe(ctr.ReadinessProbe, gracePeriod, idxPath.Child("readinessProbe"), opts)...) @@ -3632,7 +3676,7 @@ func validateHostUsers(spec *core.PodSpec, fldPath *field.Path) field.ErrorList } // validateContainers is called by pod spec and template validation to validate the list of regular containers. -func validateContainers(containers []core.Container, volumes map[string]core.VolumeSource, podClaimNames sets.Set[string], gracePeriod *int64, fldPath *field.Path, opts PodValidationOptions, podRestartPolicy *core.RestartPolicy, hostUsers bool) field.ErrorList { +func validateContainers(containers []core.Container, os *core.PodOS, volumes map[string]core.VolumeSource, podClaimNames sets.Set[string], gracePeriod *int64, fldPath *field.Path, opts PodValidationOptions, podRestartPolicy *core.RestartPolicy, hostUsers bool) field.ErrorList { allErrs := field.ErrorList{} if len(containers) == 0 { @@ -3660,7 +3704,7 @@ func validateContainers(containers []core.Container, volumes map[string]core.Vol // Regular init container and ephemeral container validation will return // field.Forbidden() for these paths. if ctr.Lifecycle != nil { - allErrs = append(allErrs, validateLifecycle(ctr.Lifecycle, gracePeriod, path.Child("lifecycle"), opts)...) + allErrs = append(allErrs, validateLifecycle(ctr.Lifecycle, gracePeriod, path.Child("lifecycle"), opts, os)...) } allErrs = append(allErrs, validateLivenessProbe(ctr.LivenessProbe, gracePeriod, path.Child("livenessProbe"), opts)...) allErrs = append(allErrs, validateReadinessProbe(ctr.ReadinessProbe, gracePeriod, path.Child("readinessProbe"), opts)...) @@ -4207,8 +4251,8 @@ func ValidatePodSpec(spec *core.PodSpec, podMeta *metav1.ObjectMeta, fldPath *fi allErrs = append(allErrs, vErrs...) podClaimNames := gatherPodResourceClaimNames(spec.ResourceClaims) allErrs = append(allErrs, validatePodResourceClaims(podMeta, spec.ResourceClaims, fldPath.Child("resourceClaims"))...) - allErrs = append(allErrs, validateContainers(spec.Containers, vols, podClaimNames, gracePeriod, fldPath.Child("containers"), opts, &spec.RestartPolicy, hostUsers)...) - allErrs = append(allErrs, validateInitContainers(spec.InitContainers, spec.Containers, vols, podClaimNames, gracePeriod, fldPath.Child("initContainers"), opts, &spec.RestartPolicy, hostUsers)...) + allErrs = append(allErrs, validateContainers(spec.Containers, spec.OS, vols, podClaimNames, gracePeriod, fldPath.Child("containers"), opts, &spec.RestartPolicy, hostUsers)...) + allErrs = append(allErrs, validateInitContainers(spec.InitContainers, spec.OS, spec.Containers, vols, podClaimNames, gracePeriod, fldPath.Child("initContainers"), opts, &spec.RestartPolicy, hostUsers)...) allErrs = append(allErrs, validateEphemeralContainers(spec.EphemeralContainers, spec.Containers, spec.InitContainers, vols, podClaimNames, fldPath.Child("ephemeralContainers"), opts, &spec.RestartPolicy, hostUsers)...) if opts.PodLevelResourcesEnabled { diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go index 4e7a65aa862..7a24ceb2608 100644 --- a/pkg/apis/core/validation/validation_test.go +++ b/pkg/apis/core/validation/validation_test.go @@ -8372,6 +8372,7 @@ func TestValidateLinuxPodSecurityContext(t *testing.T) { func TestValidateContainers(t *testing.T) { volumeDevices := make(map[string]core.VolumeSource) + podOS := &core.PodOS{Name: core.OSName(v1.Linux)} capabilities.ResetForTest() capabilities.Initialize(capabilities.Capabilities{ AllowPrivileged: true, @@ -8568,11 +8569,19 @@ func TestValidateContainers(t *testing.T) { {ResourceName: "memory", RestartPolicy: "NotRequired"}, {ResourceName: "cpu", RestartPolicy: "RestartContainer"}, }, + }, { + Name: "container-with-stopsignal-lifecycle", + Image: "image", + ImagePullPolicy: "IfNotPresent", + TerminationMessagePolicy: "File", + Lifecycle: &core.Lifecycle{ + StopSignal: ptr.To(core.SIGTERM), + }, }, } var PodRestartPolicy core.RestartPolicy = "Always" - if errs := validateContainers(successCase, volumeDevices, nil, defaultGracePeriod, field.NewPath("field"), PodValidationOptions{}, &PodRestartPolicy, noUserNamespace); len(errs) != 0 { + if errs := validateContainers(successCase, podOS, volumeDevices, nil, defaultGracePeriod, field.NewPath("field"), PodValidationOptions{}, &PodRestartPolicy, noUserNamespace); len(errs) != 0 { t.Errorf("expected success: %v", errs) } @@ -9187,7 +9196,7 @@ func TestValidateContainers(t *testing.T) { for _, tc := range errorCases { t.Run(tc.title+"__@L"+tc.line, func(t *testing.T) { - errs := validateContainers(tc.containers, volumeDevices, nil, defaultGracePeriod, field.NewPath("containers"), PodValidationOptions{}, &PodRestartPolicy, noUserNamespace) + errs := validateContainers(tc.containers, podOS, volumeDevices, nil, defaultGracePeriod, field.NewPath("containers"), PodValidationOptions{}, &PodRestartPolicy, noUserNamespace) if len(errs) == 0 { t.Fatal("expected error but received none") } @@ -9202,6 +9211,7 @@ func TestValidateContainers(t *testing.T) { func TestValidateInitContainers(t *testing.T) { volumeDevices := make(map[string]core.VolumeSource) + podOS := &core.PodOS{Name: core.OSName(v1.Linux)} capabilities.ResetForTest() capabilities.Initialize(capabilities.Capabilities{ AllowPrivileged: true, @@ -9285,7 +9295,7 @@ func TestValidateInitContainers(t *testing.T) { }, } var PodRestartPolicy core.RestartPolicy = "Never" - if errs := validateInitContainers(successCase, containers, volumeDevices, nil, defaultGracePeriod, field.NewPath("field"), PodValidationOptions{AllowSidecarResizePolicy: true}, &PodRestartPolicy, noUserNamespace); len(errs) != 0 { + if errs := validateInitContainers(successCase, podOS, containers, volumeDevices, nil, defaultGracePeriod, field.NewPath("field"), PodValidationOptions{AllowSidecarResizePolicy: true}, &PodRestartPolicy, noUserNamespace); len(errs) != 0 { t.Errorf("expected success: %v", errs) } @@ -9679,7 +9689,7 @@ func TestValidateInitContainers(t *testing.T) { for _, tc := range errorCases { t.Run(tc.title+"__@L"+tc.line, func(t *testing.T) { - errs := validateInitContainers(tc.initContainers, containers, volumeDevices, nil, defaultGracePeriod, field.NewPath("initContainers"), PodValidationOptions{}, &PodRestartPolicy, noUserNamespace) + errs := validateInitContainers(tc.initContainers, podOS, containers, volumeDevices, nil, defaultGracePeriod, field.NewPath("initContainers"), PodValidationOptions{}, &PodRestartPolicy, noUserNamespace) if len(errs) == 0 { t.Fatal("expected error but received none") } @@ -11051,6 +11061,22 @@ func TestValidatePod(t *testing.T) { }, }), ), + "Pod with valid StopSignal and valid OS": *podtest.MakePod("test-pod", + podtest.SetOS(core.Linux), + podtest.SetContainers(podtest.MakeContainer( + "test-container", + podtest.SetContainerImage("image"), + podtest.SetContainerLifecycle(core.Lifecycle{StopSignal: ptr.To(core.SIGTERM)}), + )), + ), + "Pod with valid StopSignal and valid OS (Windows)": *podtest.MakePod("test-pod", + podtest.SetOS(core.Windows), + podtest.SetContainers(podtest.MakeContainer( + "test-container", + podtest.SetContainerImage("image"), + podtest.SetContainerLifecycle(core.Lifecycle{StopSignal: ptr.To(core.SIGTERM)}), + )), + ), } for k, v := range successCases { @@ -12390,6 +12416,27 @@ func TestValidatePod(t *testing.T) { }), ), }, + "Pod with a StopSignal without `spec.os.name`": { + expectedError: "spec.containers[0].lifecycle.stopSignal: Forbidden: may not be set for containers with empty `spec.os.name`", + spec: *podtest.MakePod("test-pod", + podtest.SetContainers(podtest.MakeContainer( + "test-container", + podtest.SetContainerImage("image"), + podtest.SetContainerLifecycle(core.Lifecycle{StopSignal: ptr.To(core.SIGTERM)}), + )), + ), + }, + "Pod with a StopSignal not supported by OS": { + expectedError: "spec.containers[0].lifecycle.stopSignal: Unsupported value: \"SIGHUP\": supported values: \"SIGKILL\", \"SIGTERM\"", + spec: *podtest.MakePod("test-pod", + podtest.SetOS(core.Windows), + podtest.SetContainers(podtest.MakeContainer( + "test-container", + podtest.SetContainerImage("image"), + podtest.SetContainerLifecycle(core.Lifecycle{StopSignal: ptr.To(core.SIGHUP)}), + )), + ), + }, } for k, v := range errorCases { @@ -26968,3 +27015,58 @@ func TestValidateNodeSwapStatus(t *testing.T) { }) } } + +func TestValidateStopSignal(t *testing.T) { + fldPath := field.NewPath("root") + sigkill := core.SIGKILL + sigterm := core.SIGTERM + sighup := core.SIGHUP + linux := core.PodOS{Name: core.Linux} + windows := core.PodOS{Name: core.Windows} + + testCases := []struct { + name string + stopsignal *core.Signal + os *core.PodOS + expectErr field.ErrorList + }{ + { + name: "Empty spec.os.name", + stopsignal: &sigterm, + os: nil, + expectErr: field.ErrorList{field.Forbidden(fldPath, "may not be set for containers with empty `spec.os.name`")}, + }, + { + name: "Invalid signal passed to windows pod", + stopsignal: &sighup, + os: &windows, + expectErr: field.ErrorList{field.NotSupported(fldPath, &sighup, sets.List(supportedStopSignalsWindows))}, + }, + { + name: "Valid signal passed to windows pod", + stopsignal: &sigkill, + os: &windows, + }, + { + name: "Valid signal passed to linux pod", + stopsignal: &sighup, + os: &linux, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + errs := validateStopSignal(tc.stopsignal, fldPath, tc.os) + + if len(tc.expectErr) > 0 && len(errs) == 0 { + t.Errorf("Unexpected success") + } else if len(tc.expectErr) == 0 && len(errs) != 0 { + t.Errorf("Unexpected error(s): %v", errs) + } else if len(tc.expectErr) > 0 { + if tc.expectErr[0].Error() != errs[0].Error() { + t.Errorf("Unexpected error(s): %v", errs) + } + } + }) + } +}