Skip to content

Commit faa5c8c

Browse files
authored
Merge pull request kubernetes#99375 from ehashman/probe-kep-2238
Add Probe-level terminationGracePeriodSeconds
2 parents 251177e + 7df1259 commit faa5c8c

File tree

85 files changed

+17996
-17168
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+17996
-17168
lines changed

api/openapi-spec/swagger.json

+6-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/api/pod/util.go

+35
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,20 @@ func dropDisabledFields(
551551
})
552552
}
553553

554+
if !utilfeature.DefaultFeatureGate.Enabled(features.ProbeTerminationGracePeriod) && !probeGracePeriodInUse(oldPodSpec) {
555+
// Set pod-level terminationGracePeriodSeconds to nil if the feature is disabled and it is not used
556+
VisitContainers(podSpec, AllContainers, func(c *api.Container, containerType ContainerType) bool {
557+
if c.LivenessProbe != nil {
558+
c.LivenessProbe.TerminationGracePeriodSeconds = nil
559+
}
560+
if c.StartupProbe != nil {
561+
c.StartupProbe.TerminationGracePeriodSeconds = nil
562+
}
563+
// cannot be set for readiness probes
564+
return true
565+
})
566+
}
567+
554568
dropDisabledFSGroupFields(podSpec, oldPodSpec)
555569

556570
if !utilfeature.DefaultFeatureGate.Enabled(features.PodOverhead) && !overheadInUse(oldPodSpec) {
@@ -811,6 +825,27 @@ func subpathExprInUse(podSpec *api.PodSpec) bool {
811825
return inUse
812826
}
813827

828+
// probeGracePeriodInUse returns true if the pod spec is non-nil and has a probe that makes use
829+
// of the probe-level terminationGracePeriodSeconds feature
830+
func probeGracePeriodInUse(podSpec *api.PodSpec) bool {
831+
if podSpec == nil {
832+
return false
833+
}
834+
835+
var inUse bool
836+
VisitContainers(podSpec, AllContainers, func(c *api.Container, containerType ContainerType) bool {
837+
// cannot be set for readiness probes
838+
if (c.LivenessProbe != nil && c.LivenessProbe.TerminationGracePeriodSeconds != nil) ||
839+
(c.StartupProbe != nil && c.StartupProbe.TerminationGracePeriodSeconds != nil) {
840+
inUse = true
841+
return false
842+
}
843+
return true
844+
})
845+
846+
return inUse
847+
}
848+
814849
// csiInUse returns true if any pod's spec include inline CSI volumes.
815850
func csiInUse(podSpec *api.PodSpec) bool {
816851
if podSpec == nil {

pkg/api/pod/util_test.go

+97
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,103 @@ func TestDropSubPathExpr(t *testing.T) {
11401140
}
11411141
}
11421142

1143+
func TestDropProbeGracePeriod(t *testing.T) {
1144+
gracePeriod := int64(10)
1145+
probe := api.Probe{TerminationGracePeriodSeconds: &gracePeriod}
1146+
podWithProbeGracePeriod := func() *api.Pod {
1147+
return &api.Pod{
1148+
Spec: api.PodSpec{
1149+
RestartPolicy: api.RestartPolicyNever,
1150+
Containers: []api.Container{{Name: "container1", Image: "testimage", LivenessProbe: &probe, StartupProbe: &probe}},
1151+
},
1152+
}
1153+
}
1154+
podWithoutProbeGracePeriod := func() *api.Pod {
1155+
p := podWithProbeGracePeriod()
1156+
p.Spec.Containers[0].LivenessProbe.TerminationGracePeriodSeconds = nil
1157+
p.Spec.Containers[0].StartupProbe.TerminationGracePeriodSeconds = nil
1158+
return p
1159+
}
1160+
1161+
podInfo := []struct {
1162+
description string
1163+
hasGracePeriod bool
1164+
pod func() *api.Pod
1165+
}{
1166+
{
1167+
description: "has probe-level terminationGracePeriod",
1168+
hasGracePeriod: true,
1169+
pod: podWithProbeGracePeriod,
1170+
},
1171+
{
1172+
description: "does not have probe-level terminationGracePeriod",
1173+
hasGracePeriod: false,
1174+
pod: podWithoutProbeGracePeriod,
1175+
},
1176+
{
1177+
description: "only has liveness probe-level terminationGracePeriod",
1178+
hasGracePeriod: true,
1179+
pod: func() *api.Pod {
1180+
p := podWithProbeGracePeriod()
1181+
p.Spec.Containers[0].StartupProbe.TerminationGracePeriodSeconds = nil
1182+
return p
1183+
},
1184+
},
1185+
{
1186+
description: "is nil",
1187+
hasGracePeriod: false,
1188+
pod: func() *api.Pod { return nil },
1189+
},
1190+
}
1191+
1192+
enabled := true
1193+
for _, oldPodInfo := range podInfo {
1194+
for _, newPodInfo := range podInfo {
1195+
oldPodHasGracePeriod, oldPod := oldPodInfo.hasGracePeriod, oldPodInfo.pod()
1196+
newPodHasGracePeriod, newPod := newPodInfo.hasGracePeriod, newPodInfo.pod()
1197+
if newPod == nil {
1198+
continue
1199+
}
1200+
1201+
t.Run(fmt.Sprintf("feature enabled=%v, old pod %v, new pod %v", enabled, oldPodInfo.description, newPodInfo.description), func(t *testing.T) {
1202+
1203+
var oldPodSpec *api.PodSpec
1204+
if oldPod != nil {
1205+
oldPodSpec = &oldPod.Spec
1206+
}
1207+
dropDisabledFields(&newPod.Spec, nil, oldPodSpec, nil)
1208+
1209+
// old pod should never be changed
1210+
if !reflect.DeepEqual(oldPod, oldPodInfo.pod()) {
1211+
t.Errorf("old pod changed: %v", diff.ObjectReflectDiff(oldPod, oldPodInfo.pod()))
1212+
}
1213+
1214+
switch {
1215+
case enabled || oldPodHasGracePeriod:
1216+
// new pod should not be changed if the feature is enabled, or if the old pod had subpaths
1217+
if !reflect.DeepEqual(newPod, newPodInfo.pod()) {
1218+
t.Errorf("new pod changed: %v", diff.ObjectReflectDiff(newPod, newPodInfo.pod()))
1219+
}
1220+
case newPodHasGracePeriod:
1221+
// new pod should be changed
1222+
if reflect.DeepEqual(newPod, newPodInfo.pod()) {
1223+
t.Errorf("new pod was not changed")
1224+
}
1225+
// new pod should not have subpaths
1226+
if !reflect.DeepEqual(newPod, podWithoutProbeGracePeriod()) {
1227+
t.Errorf("new pod had probe-level terminationGracePeriod: %v", diff.ObjectReflectDiff(newPod, podWithoutProbeGracePeriod()))
1228+
}
1229+
default:
1230+
// new pod should not need to be changed
1231+
if !reflect.DeepEqual(newPod, newPodInfo.pod()) {
1232+
t.Errorf("new pod changed: %v", diff.ObjectReflectDiff(newPod, newPodInfo.pod()))
1233+
}
1234+
}
1235+
})
1236+
}
1237+
}
1238+
}
1239+
11431240
// helper creates a podStatus with list of PodIPs
11441241
func makePodStatus(podIPs []api.PodIP) *api.PodStatus {
11451242
return &api.PodStatus{

pkg/apis/core/types.go

+13-1
Original file line numberDiff line numberDiff line change
@@ -2020,6 +2020,17 @@ type Probe struct {
20202020
// Minimum consecutive failures for the probe to be considered failed after having succeeded.
20212021
// +optional
20222022
FailureThreshold int32
2023+
// Optional duration in seconds the pod needs to terminate gracefully upon probe failure.
2024+
// The grace period is the duration in seconds after the processes running in the pod are sent
2025+
// a termination signal and the time when the processes are forcibly halted with a kill signal.
2026+
// Set this value longer than the expected cleanup time for your process.
2027+
// If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this
2028+
// value overrides the value provided by the pod spec.
2029+
// Value must be non-negative integer. The value zero indicates stop immediately via
2030+
// the kill signal (no opportunity to shut down).
2031+
// This is an alpha field and requires enabling ProbeTerminationGracePeriod feature gate.
2032+
// +optional
2033+
TerminationGracePeriodSeconds *int64
20232034
}
20242035

20252036
// PullPolicy describes a policy for if/when to pull a container image
@@ -2719,7 +2730,8 @@ type PodSpec struct {
27192730
// +optional
27202731
RestartPolicy RestartPolicy
27212732
// Optional duration in seconds the pod needs to terminate gracefully. May be decreased in delete request.
2722-
// Value must be non-negative integer. The value zero indicates delete immediately.
2733+
// Value must be non-negative integer. The value zero indicates stop immediately via the kill
2734+
// signal (no opportunity to shut down).
27232735
// If this value is nil, the default grace period will be used instead.
27242736
// The grace period is the duration in seconds after the processes running in the pod are sent
27252737
// a termination signal and the time when the processes are forcibly halted with a kill signal.

pkg/apis/core/v1/zz_generated.conversion.go

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/apis/core/validation/validation.go

+5
Original file line numberDiff line numberDiff line change
@@ -2859,6 +2859,11 @@ func validateContainers(containers []core.Container, isInitContainers bool, volu
28592859
allErrs = append(allErrs, validateLifecycle(ctr.Lifecycle, idxPath.Child("lifecycle"))...)
28602860
}
28612861
allErrs = append(allErrs, validateProbe(ctr.LivenessProbe, idxPath.Child("livenessProbe"))...)
2862+
// Readiness-specific validation
2863+
if ctr.ReadinessProbe != nil && ctr.ReadinessProbe.TerminationGracePeriodSeconds != nil {
2864+
allErrs = append(allErrs, field.Invalid(idxPath.Child("readinessProbe", "terminationGracePeriodSeconds"), ctr.ReadinessProbe.TerminationGracePeriodSeconds, "must not be set for readinessProbes"))
2865+
}
2866+
allErrs = append(allErrs, validateProbe(ctr.StartupProbe, idxPath.Child("startupProbe"))...)
28622867
// Liveness-specific validation
28632868
if ctr.LivenessProbe != nil && ctr.LivenessProbe.SuccessThreshold != 1 {
28642869
allErrs = append(allErrs, field.Invalid(idxPath.Child("livenessProbe", "successThreshold"), ctr.LivenessProbe.SuccessThreshold, "must be 1"))

pkg/apis/core/validation/validation_test.go

+14
Original file line numberDiff line numberDiff line change
@@ -6284,6 +6284,20 @@ func TestValidateContainers(t *testing.T) {
62846284
TerminationMessagePolicy: "File",
62856285
},
62866286
},
6287+
"invalid readiness probe, terminationGracePeriodSeconds set.": {
6288+
{
6289+
Name: "life-123",
6290+
Image: "image",
6291+
ReadinessProbe: &core.Probe{
6292+
Handler: core.Handler{
6293+
TCPSocket: &core.TCPSocketAction{},
6294+
},
6295+
TerminationGracePeriodSeconds: utilpointer.Int64Ptr(10),
6296+
},
6297+
ImagePullPolicy: "IfNotPresent",
6298+
TerminationMessagePolicy: "File",
6299+
},
6300+
},
62876301
"invalid liveness probe, no tcp socket port.": {
62886302
{
62896303
Name: "life-123",

pkg/apis/core/zz_generated.deepcopy.go

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/features/kube_features.go

+7
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,12 @@ const (
696696
// Enables topology aware hints for EndpointSlices
697697
TopologyAwareHints featuregate.Feature = "TopologyAwareHints"
698698

699+
// owner: @ehashman
700+
// alpha: v1.21
701+
//
702+
// Allows user to override pod-level terminationGracePeriod for probes
703+
ProbeTerminationGracePeriod featuregate.Feature = "ProbeTerminationGracePeriod"
704+
699705
// owner: @ahg-g
700706
// alpha: v1.21
701707
//
@@ -850,6 +856,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
850856
MixedProtocolLBService: {Default: false, PreRelease: featuregate.Alpha},
851857
VolumeCapacityPriority: {Default: false, PreRelease: featuregate.Alpha},
852858
PreferNominatedNode: {Default: false, PreRelease: featuregate.Alpha},
859+
ProbeTerminationGracePeriod: {Default: false, PreRelease: featuregate.Alpha},
853860
RunAsGroup: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.22
854861
PodDeletionCost: {Default: false, PreRelease: featuregate.Alpha},
855862
TopologyAwareHints: {Default: false, PreRelease: featuregate.Alpha},

pkg/kubelet/kuberuntime/kuberuntime_container.go

+16-3
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ func (m *kubeGenericRuntimeManager) startContainer(podSandboxID string, podSandb
227227
msg, handlerErr := m.runner.Run(kubeContainerID, pod, container, container.Lifecycle.PostStart)
228228
if handlerErr != nil {
229229
m.recordContainerEvent(pod, container, kubeContainerID.ID, v1.EventTypeWarning, events.FailedPostStartHook, msg)
230-
if err := m.killContainer(pod, kubeContainerID, container.Name, "FailedPostStartHook", nil); err != nil {
230+
if err := m.killContainer(pod, kubeContainerID, container.Name, "FailedPostStartHook", reasonFailedPostStartHook, nil); err != nil {
231231
klog.ErrorS(fmt.Errorf("%s: %v", ErrPostStartHook, handlerErr), "Failed to kill container", "pod", klog.KObj(pod),
232232
"podUID", pod.UID, "containerName", container.Name, "containerID", kubeContainerID.String())
233233
}
@@ -596,7 +596,7 @@ func (m *kubeGenericRuntimeManager) restoreSpecsFromContainerLabels(containerID
596596
// killContainer kills a container through the following steps:
597597
// * Run the pre-stop lifecycle hooks (if applicable).
598598
// * Stop the container.
599-
func (m *kubeGenericRuntimeManager) killContainer(pod *v1.Pod, containerID kubecontainer.ContainerID, containerName string, message string, gracePeriodOverride *int64) error {
599+
func (m *kubeGenericRuntimeManager) killContainer(pod *v1.Pod, containerID kubecontainer.ContainerID, containerName string, message string, reason containerKillReason, gracePeriodOverride *int64) error {
600600
var containerSpec *v1.Container
601601
if pod != nil {
602602
if containerSpec = kubecontainer.GetContainerSpec(pod, containerName); containerSpec == nil {
@@ -619,6 +619,19 @@ func (m *kubeGenericRuntimeManager) killContainer(pod *v1.Pod, containerID kubec
619619
gracePeriod = *pod.DeletionGracePeriodSeconds
620620
case pod.Spec.TerminationGracePeriodSeconds != nil:
621621
gracePeriod = *pod.Spec.TerminationGracePeriodSeconds
622+
623+
if utilfeature.DefaultFeatureGate.Enabled(features.ProbeTerminationGracePeriod) {
624+
switch reason {
625+
case reasonStartupProbe:
626+
if containerSpec.StartupProbe != nil && containerSpec.StartupProbe.TerminationGracePeriodSeconds != nil {
627+
gracePeriod = *containerSpec.StartupProbe.TerminationGracePeriodSeconds
628+
}
629+
case reasonLivenessProbe:
630+
if containerSpec.LivenessProbe != nil && containerSpec.LivenessProbe.TerminationGracePeriodSeconds != nil {
631+
gracePeriod = *containerSpec.LivenessProbe.TerminationGracePeriodSeconds
632+
}
633+
}
634+
}
622635
}
623636

624637
if len(message) == 0 {
@@ -672,7 +685,7 @@ func (m *kubeGenericRuntimeManager) killContainersWithSyncResult(pod *v1.Pod, ru
672685
defer wg.Done()
673686

674687
killContainerResult := kubecontainer.NewSyncResult(kubecontainer.KillContainer, container.Name)
675-
if err := m.killContainer(pod, container.ID, container.Name, "", gracePeriodOverride); err != nil {
688+
if err := m.killContainer(pod, container.ID, container.Name, "", reasonUnknown, gracePeriodOverride); err != nil {
676689
killContainerResult.Fail(kubecontainer.ErrKillContainer, err.Error())
677690
klog.ErrorS(err, "Kill container failed", "pod", klog.KObj(pod), "podUID", pod.UID,
678691
"containerName", container.Name, "containerID", container.ID)

pkg/kubelet/kuberuntime/kuberuntime_container_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func TestKillContainer(t *testing.T) {
120120
}
121121

122122
for _, test := range tests {
123-
err := m.killContainer(test.pod, test.containerID, test.containerName, test.reason, &test.gracePeriodOverride)
123+
err := m.killContainer(test.pod, test.containerID, test.containerName, test.reason, "", &test.gracePeriodOverride)
124124
if test.succeed != (err == nil) {
125125
t.Errorf("%s: expected %v, got %v (%v)", test.caseName, test.succeed, (err == nil), err)
126126
}
@@ -290,7 +290,7 @@ func TestLifeCycleHook(t *testing.T) {
290290
// Configured and works as expected
291291
t.Run("PreStop-CMDExec", func(t *testing.T) {
292292
testPod.Spec.Containers[0].Lifecycle = cmdLifeCycle
293-
m.killContainer(testPod, cID, "foo", "testKill", &gracePeriod)
293+
m.killContainer(testPod, cID, "foo", "testKill", "", &gracePeriod)
294294
if fakeRunner.Cmd[0] != cmdLifeCycle.PreStop.Exec.Command[0] {
295295
t.Errorf("CMD Prestop hook was not invoked")
296296
}
@@ -300,7 +300,7 @@ func TestLifeCycleHook(t *testing.T) {
300300
t.Run("PreStop-HTTPGet", func(t *testing.T) {
301301
defer func() { fakeHTTP.url = "" }()
302302
testPod.Spec.Containers[0].Lifecycle = httpLifeCycle
303-
m.killContainer(testPod, cID, "foo", "testKill", &gracePeriod)
303+
m.killContainer(testPod, cID, "foo", "testKill", "", &gracePeriod)
304304

305305
if !strings.Contains(fakeHTTP.url, httpLifeCycle.PreStop.HTTPGet.Host) {
306306
t.Errorf("HTTP Prestop hook was not invoked")
@@ -314,7 +314,7 @@ func TestLifeCycleHook(t *testing.T) {
314314
testPod.DeletionGracePeriodSeconds = &gracePeriodLocal
315315
testPod.Spec.TerminationGracePeriodSeconds = &gracePeriodLocal
316316

317-
m.killContainer(testPod, cID, "foo", "testKill", &gracePeriodLocal)
317+
m.killContainer(testPod, cID, "foo", "testKill", "", &gracePeriodLocal)
318318

319319
if strings.Contains(fakeHTTP.url, httpLifeCycle.PreStop.HTTPGet.Host) {
320320
t.Errorf("HTTP Should not execute when gracePeriod is 0")

pkg/kubelet/kuberuntime/kuberuntime_gc.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ func (cgc *containerGC) removeOldestN(containers []containerGCInfo, toRemove int
137137
ID: containers[i].id,
138138
}
139139
message := "Container is in unknown state, try killing it before removal"
140-
if err := cgc.manager.killContainer(nil, id, containers[i].name, message, nil); err != nil {
140+
if err := cgc.manager.killContainer(nil, id, containers[i].name, message, reasonUnknown, nil); err != nil {
141141
klog.Errorf("Failed to stop container %q: %v", containers[i].id, err)
142142
continue
143143
}

0 commit comments

Comments
 (0)