Sidecar: API changes
- Add SidecarContaienrs feature gate - Add ContainerRestartPolicy type - Add RestartPolicy field to the Container - Drop RestartPolicy field if the feature is disabled - Add validation for the SidecarContainers - Allow restartable init containaers to have a startup probe
This commit is contained in:
		 Gunju Kim
					Gunju Kim
				
			
				
					committed by
					
						 Sergey Kanzhelev
						Sergey Kanzhelev
					
				
			
			
				
	
			
			
			 Sergey Kanzhelev
						Sergey Kanzhelev
					
				
			
						parent
						
							c17601fa18
						
					
				
				
					commit
					5d26bcd468
				
			| @@ -510,6 +510,14 @@ func dropDisabledFields( | |||||||
| 			podSpec.EphemeralContainers[i].ResizePolicy = nil | 			podSpec.EphemeralContainers[i].ResizePolicy = nil | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if !utilfeature.DefaultFeatureGate.Enabled(features.SidecarContainers) && !restartableInitContainersInUse(oldPodSpec) { | ||||||
|  | 		// Drop the RestartPolicy field of init containers. | ||||||
|  | 		for i := range podSpec.InitContainers { | ||||||
|  | 			podSpec.InitContainers[i].RestartPolicy = nil | ||||||
|  | 		} | ||||||
|  | 		// For other types of containers, validateContainers will handle them. | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // dropDisabledPodStatusFields removes disabled fields from the pod status | // dropDisabledPodStatusFields removes disabled fields from the pod status | ||||||
| @@ -778,6 +786,23 @@ func schedulingGatesInUse(podSpec *api.PodSpec) bool { | |||||||
| 	return len(podSpec.SchedulingGates) != 0 | 	return len(podSpec.SchedulingGates) != 0 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // restartableInitContainersInUse returns true if the pod spec is non-nil and | ||||||
|  | // it has any init container with ContainerRestartPolicyAlways. | ||||||
|  | func restartableInitContainersInUse(podSpec *api.PodSpec) bool { | ||||||
|  | 	if podSpec == nil { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	var inUse bool | ||||||
|  | 	VisitContainers(podSpec, InitContainers, func(c *api.Container, containerType ContainerType) bool { | ||||||
|  | 		if c.RestartPolicy != nil && *c.RestartPolicy == api.ContainerRestartPolicyAlways { | ||||||
|  | 			inUse = true | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 		return true | ||||||
|  | 	}) | ||||||
|  | 	return inUse | ||||||
|  | } | ||||||
|  |  | ||||||
| func hasInvalidLabelValueInAffinitySelector(spec *api.PodSpec) bool { | func hasInvalidLabelValueInAffinitySelector(spec *api.PodSpec) bool { | ||||||
| 	if spec.Affinity != nil { | 	if spec.Affinity != nil { | ||||||
| 		if spec.Affinity.PodAffinity != nil { | 		if spec.Affinity.PodAffinity != nil { | ||||||
|   | |||||||
| @@ -2052,6 +2052,109 @@ func TestDropInPlacePodVerticalScaling(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestDropSidecarContainers(t *testing.T) { | ||||||
|  | 	containerRestartPolicyAlways := api.ContainerRestartPolicyAlways | ||||||
|  |  | ||||||
|  | 	podWithSidecarContainers := func() *api.Pod { | ||||||
|  | 		return &api.Pod{ | ||||||
|  | 			Spec: api.PodSpec{ | ||||||
|  | 				InitContainers: []api.Container{ | ||||||
|  | 					{ | ||||||
|  | 						Name:          "c1", | ||||||
|  | 						Image:         "image", | ||||||
|  | 						RestartPolicy: &containerRestartPolicyAlways, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	podWithoutSidecarContainers := func() *api.Pod { | ||||||
|  | 		return &api.Pod{ | ||||||
|  | 			Spec: api.PodSpec{ | ||||||
|  | 				InitContainers: []api.Container{ | ||||||
|  | 					{ | ||||||
|  | 						Name:  "c1", | ||||||
|  | 						Image: "image", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	podInfo := []struct { | ||||||
|  | 		description         string | ||||||
|  | 		hasSidecarContainer bool | ||||||
|  | 		pod                 func() *api.Pod | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			description:         "has a sidecar container", | ||||||
|  | 			hasSidecarContainer: true, | ||||||
|  | 			pod:                 podWithSidecarContainers, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description:         "does not have a sidecar container", | ||||||
|  | 			hasSidecarContainer: false, | ||||||
|  | 			pod:                 podWithoutSidecarContainers, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description:         "is nil", | ||||||
|  | 			hasSidecarContainer: false, | ||||||
|  | 			pod:                 func() *api.Pod { return nil }, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, enabled := range []bool{true, false} { | ||||||
|  | 		for _, oldPodInfo := range podInfo { | ||||||
|  | 			for _, newPodInfo := range podInfo { | ||||||
|  | 				oldPodHasSidecarContainer, oldPod := oldPodInfo.hasSidecarContainer, oldPodInfo.pod() | ||||||
|  | 				newPodHasSidecarContainer, newPod := newPodInfo.hasSidecarContainer, newPodInfo.pod() | ||||||
|  | 				if newPod == nil { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				t.Run(fmt.Sprintf("feature enabled=%v, old pod %v, new pod %v", enabled, oldPodInfo.description, newPodInfo.description), func(t *testing.T) { | ||||||
|  | 					defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SidecarContainers, enabled)() | ||||||
|  |  | ||||||
|  | 					var oldPodSpec *api.PodSpec | ||||||
|  | 					if oldPod != nil { | ||||||
|  | 						oldPodSpec = &oldPod.Spec | ||||||
|  | 					} | ||||||
|  | 					dropDisabledFields(&newPod.Spec, nil, oldPodSpec, nil) | ||||||
|  |  | ||||||
|  | 					// old pod should never be changed | ||||||
|  | 					if !reflect.DeepEqual(oldPod, oldPodInfo.pod()) { | ||||||
|  | 						t.Errorf("old pod changed: %v", cmp.Diff(oldPod, oldPodInfo.pod())) | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					switch { | ||||||
|  | 					case enabled || oldPodHasSidecarContainer: | ||||||
|  | 						// new pod shouldn't change if feature enabled or if old pod has | ||||||
|  | 						// any sidecar container | ||||||
|  | 						if !reflect.DeepEqual(newPod, newPodInfo.pod()) { | ||||||
|  | 							t.Errorf("new pod changed: %v", cmp.Diff(newPod, newPodInfo.pod())) | ||||||
|  | 						} | ||||||
|  | 					case newPodHasSidecarContainer: | ||||||
|  | 						// new pod should be changed | ||||||
|  | 						if reflect.DeepEqual(newPod, newPodInfo.pod()) { | ||||||
|  | 							t.Errorf("new pod was not changed") | ||||||
|  | 						} | ||||||
|  | 						// new pod should not have any sidecar container | ||||||
|  | 						if !reflect.DeepEqual(newPod, podWithoutSidecarContainers()) { | ||||||
|  | 							t.Errorf("new pod has a sidecar container: %v", cmp.Diff(newPod, podWithoutSidecarContainers())) | ||||||
|  | 						} | ||||||
|  | 					default: | ||||||
|  | 						// new pod should not need to be changed | ||||||
|  | 						if !reflect.DeepEqual(newPod, newPodInfo.pod()) { | ||||||
|  | 							t.Errorf("new pod changed: %v", cmp.Diff(newPod, newPodInfo.pod())) | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				}) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func TestMarkPodProposedForResize(t *testing.T) { | func TestMarkPodProposedForResize(t *testing.T) { | ||||||
| 	testCases := []struct { | 	testCases := []struct { | ||||||
| 		desc        string | 		desc        string | ||||||
|   | |||||||
| @@ -2279,6 +2279,24 @@ type Container struct { | |||||||
| 	// +featureGate=InPlacePodVerticalScaling | 	// +featureGate=InPlacePodVerticalScaling | ||||||
| 	// +optional | 	// +optional | ||||||
| 	ResizePolicy []ContainerResizePolicy | 	ResizePolicy []ContainerResizePolicy | ||||||
|  | 	// RestartPolicy defines the restart behavior of individual containers in a pod. | ||||||
|  | 	// This field may only be set for init containers, and the only allowed value is "Always". | ||||||
|  | 	// For non-init containers or when this field is not specified, | ||||||
|  | 	// the restart behavior is defined by the Pod's restart policy and the container type. | ||||||
|  | 	// Setting the RestartPolicy as "Always" for the init container will have the following effect: | ||||||
|  | 	// this init container will be continually restarted on | ||||||
|  | 	// exit until all regular containers have terminated. Once all regular | ||||||
|  | 	// containers have completed, all init containers with restartPolicy "Always" | ||||||
|  | 	// will be shut down. This lifecycle differs from normal init containers and | ||||||
|  | 	// is often referred to as a "sidecar" container. Although this init | ||||||
|  | 	// container still starts in the init container sequence, it does not wait | ||||||
|  | 	// for the container to complete before proceeding to the next init | ||||||
|  | 	// container. Instead, the next init container starts immediately after this | ||||||
|  | 	// init container is started, or after any startupProbe has successfully | ||||||
|  | 	// completed. | ||||||
|  | 	// +featureGate=SidecarContainers | ||||||
|  | 	// +optional | ||||||
|  | 	RestartPolicy *ContainerRestartPolicy | ||||||
| 	// +optional | 	// +optional | ||||||
| 	VolumeMounts []VolumeMount | 	VolumeMounts []VolumeMount | ||||||
| 	// volumeDevices is the list of block devices to be used by the container. | 	// volumeDevices is the list of block devices to be used by the container. | ||||||
| @@ -2597,6 +2615,14 @@ const ( | |||||||
| 	RestartPolicyNever     RestartPolicy = "Never" | 	RestartPolicyNever     RestartPolicy = "Never" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // ContainerRestartPolicy is the restart policy for a single container. | ||||||
|  | // This may only be set for init containers and only allowed value is "Always". | ||||||
|  | type ContainerRestartPolicy string | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	ContainerRestartPolicyAlways ContainerRestartPolicy = "Always" | ||||||
|  | ) | ||||||
|  |  | ||||||
| // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object | ||||||
|  |  | ||||||
| // PodList is a list of Pods. | // PodList is a list of Pods. | ||||||
| @@ -3505,6 +3531,13 @@ type EphemeralContainerCommon struct { | |||||||
| 	// +featureGate=InPlacePodVerticalScaling | 	// +featureGate=InPlacePodVerticalScaling | ||||||
| 	// +optional | 	// +optional | ||||||
| 	ResizePolicy []ContainerResizePolicy | 	ResizePolicy []ContainerResizePolicy | ||||||
|  | 	// Restart policy for the container to manage the restart behavior of each | ||||||
|  | 	// container within a pod. | ||||||
|  | 	// This may only be set for init containers. You cannot set this field on | ||||||
|  | 	// ephemeral containers. | ||||||
|  | 	// +featureGate=SidecarContainers | ||||||
|  | 	// +optional | ||||||
|  | 	RestartPolicy *ContainerRestartPolicy | ||||||
| 	// Pod volumes to mount into the container's filesystem. Subpath mounts are not allowed for ephemeral containers. | 	// Pod volumes to mount into the container's filesystem. Subpath mounts are not allowed for ephemeral containers. | ||||||
| 	// +optional | 	// +optional | ||||||
| 	VolumeMounts []VolumeMount | 	VolumeMounts []VolumeMount | ||||||
|   | |||||||
| @@ -2835,6 +2835,23 @@ func validateProbe(probe *core.Probe, fldPath *field.Path) field.ErrorList { | |||||||
| 	return allErrs | 	return allErrs | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func validateInitContainerRestartPolicy(restartPolicy *core.ContainerRestartPolicy, fldPath *field.Path) field.ErrorList { | ||||||
|  | 	var allErrors field.ErrorList | ||||||
|  |  | ||||||
|  | 	if restartPolicy == nil { | ||||||
|  | 		return allErrors | ||||||
|  | 	} | ||||||
|  | 	switch *restartPolicy { | ||||||
|  | 	case core.ContainerRestartPolicyAlways: | ||||||
|  | 		break | ||||||
|  | 	default: | ||||||
|  | 		validValues := []string{string(core.ContainerRestartPolicyAlways)} | ||||||
|  | 		allErrors = append(allErrors, field.NotSupported(fldPath, *restartPolicy, validValues)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return allErrors | ||||||
|  | } | ||||||
|  |  | ||||||
| type commonHandler struct { | type commonHandler struct { | ||||||
| 	Exec      *core.ExecAction | 	Exec      *core.ExecAction | ||||||
| 	HTTPGet   *core.HTTPGetAction | 	HTTPGet   *core.HTTPGetAction | ||||||
| @@ -3165,6 +3182,13 @@ func validateInitContainers(containers []core.Container, regularContainers []cor | |||||||
| 		// Apply the validation common to all container types | 		// Apply the validation common to all container types | ||||||
| 		allErrs = append(allErrs, validateContainerCommon(&ctr, volumes, podClaimNames, idxPath, opts)...) | 		allErrs = append(allErrs, validateContainerCommon(&ctr, volumes, podClaimNames, idxPath, opts)...) | ||||||
|  |  | ||||||
|  | 		restartAlways := false | ||||||
|  | 		// Apply the validation specific to init containers | ||||||
|  | 		if ctr.RestartPolicy != nil { | ||||||
|  | 			allErrs = append(allErrs, validateInitContainerRestartPolicy(ctr.RestartPolicy, idxPath.Child("restartPolicy"))...) | ||||||
|  | 			restartAlways = *ctr.RestartPolicy == core.ContainerRestartPolicyAlways | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		// Names must be unique within regular and init containers. Collisions with ephemeral containers | 		// Names must be unique within regular and init containers. Collisions with ephemeral containers | ||||||
| 		// will be detected by validateEphemeralContainers(). | 		// will be detected by validateEphemeralContainers(). | ||||||
| 		if allNames.Has(ctr.Name) { | 		if allNames.Has(ctr.Name) { | ||||||
| @@ -3176,6 +3200,26 @@ func validateInitContainers(containers []core.Container, regularContainers []cor | |||||||
| 		// Check for port conflicts in init containers individually since init containers run one-by-one. | 		// Check for port conflicts in init containers individually since init containers run one-by-one. | ||||||
| 		allErrs = append(allErrs, checkHostPortConflicts([]core.Container{ctr}, fldPath)...) | 		allErrs = append(allErrs, checkHostPortConflicts([]core.Container{ctr}, fldPath)...) | ||||||
|  |  | ||||||
|  | 		switch { | ||||||
|  | 		case restartAlways: | ||||||
|  | 			// TODO: Allow restartable init containers to have a lifecycle hook. | ||||||
|  | 			if ctr.Lifecycle != nil { | ||||||
|  | 				allErrs = append(allErrs, field.Forbidden(idxPath.Child("lifecycle"), "may not be set for init containers")) | ||||||
|  | 			} | ||||||
|  | 			// TODO: Allow restartable init containers to have a liveness probe. | ||||||
|  | 			if ctr.LivenessProbe != nil { | ||||||
|  | 				allErrs = append(allErrs, field.Forbidden(idxPath.Child("livenessProbe"), "may not be set for init containers")) | ||||||
|  | 			} | ||||||
|  | 			// TODO: Allow restartable init containers to have a readiness probe. | ||||||
|  | 			if ctr.ReadinessProbe != nil { | ||||||
|  | 				allErrs = append(allErrs, field.Forbidden(idxPath.Child("readinessProbe"), "may not be set for init containers")) | ||||||
|  | 			} | ||||||
|  | 			allErrs = append(allErrs, validateProbe(ctr.StartupProbe, idxPath.Child("startupProbe"))...) | ||||||
|  | 			if ctr.StartupProbe != nil && ctr.StartupProbe.SuccessThreshold != 1 { | ||||||
|  | 				allErrs = append(allErrs, field.Invalid(idxPath.Child("startupProbe", "successThreshold"), ctr.StartupProbe.SuccessThreshold, "must be 1")) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 		default: | ||||||
| 			// These fields are disallowed for init containers. | 			// These fields are disallowed for init containers. | ||||||
| 			if ctr.Lifecycle != nil { | 			if ctr.Lifecycle != nil { | ||||||
| 				allErrs = append(allErrs, field.Forbidden(idxPath.Child("lifecycle"), "may not be set for init containers")) | 				allErrs = append(allErrs, field.Forbidden(idxPath.Child("lifecycle"), "may not be set for init containers")) | ||||||
| @@ -3189,6 +3233,8 @@ func validateInitContainers(containers []core.Container, regularContainers []cor | |||||||
| 			if ctr.StartupProbe != nil { | 			if ctr.StartupProbe != nil { | ||||||
| 				allErrs = append(allErrs, field.Forbidden(idxPath.Child("startupProbe"), "may not be set for init containers")) | 				allErrs = append(allErrs, field.Forbidden(idxPath.Child("startupProbe"), "may not be set for init containers")) | ||||||
| 			} | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		if len(ctr.ResizePolicy) > 0 { | 		if len(ctr.ResizePolicy) > 0 { | ||||||
| 			allErrs = append(allErrs, field.Invalid(idxPath.Child("resizePolicy"), ctr.ResizePolicy, "must not be set for init containers")) | 			allErrs = append(allErrs, field.Invalid(idxPath.Child("resizePolicy"), ctr.ResizePolicy, "must not be set for init containers")) | ||||||
| 		} | 		} | ||||||
| @@ -3311,6 +3357,11 @@ func validateContainers(containers []core.Container, volumes map[string]core.Vol | |||||||
| 		if ctr.StartupProbe != nil && ctr.StartupProbe.SuccessThreshold != 1 { | 		if ctr.StartupProbe != nil && ctr.StartupProbe.SuccessThreshold != 1 { | ||||||
| 			allErrs = append(allErrs, field.Invalid(path.Child("startupProbe", "successThreshold"), ctr.StartupProbe.SuccessThreshold, "must be 1")) | 			allErrs = append(allErrs, field.Invalid(path.Child("startupProbe", "successThreshold"), ctr.StartupProbe.SuccessThreshold, "must be 1")) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		// These fields are disallowed for regular containers | ||||||
|  | 		if ctr.RestartPolicy != nil { | ||||||
|  | 			allErrs = append(allErrs, field.Forbidden(path.Child("restartPolicy"), "may not be set for non-init containers")) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Port conflicts are checked across all containers | 	// Port conflicts are checked across all containers | ||||||
|   | |||||||
| @@ -54,6 +54,14 @@ const ( | |||||||
| 	envVarNameErrMsg        = "a valid environment variable name must consist of" | 	envVarNameErrMsg        = "a valid environment variable name must consist of" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	containerRestartPolicyAlways    = core.ContainerRestartPolicyAlways | ||||||
|  | 	containerRestartPolicyOnFailure = core.ContainerRestartPolicy("OnFailure") | ||||||
|  | 	containerRestartPolicyNever     = core.ContainerRestartPolicy("Never") | ||||||
|  | 	containerRestartPolicyInvalid   = core.ContainerRestartPolicy("invalid") | ||||||
|  | 	containerRestartPolicyEmpty     = core.ContainerRestartPolicy("") | ||||||
|  | ) | ||||||
|  |  | ||||||
| type topologyPair struct { | type topologyPair struct { | ||||||
| 	key   string | 	key   string | ||||||
| 	value string | 	value string | ||||||
| @@ -7129,6 +7137,71 @@ func TestValidateEphemeralContainers(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 		}}, | 		}}, | ||||||
| 		field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].resizePolicy"}}, | 		field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].resizePolicy"}}, | ||||||
|  | 	}, { | ||||||
|  | 		"Forbidden RestartPolicy: Always", | ||||||
|  | 		line(), | ||||||
|  | 		[]core.EphemeralContainer{{ | ||||||
|  | 			EphemeralContainerCommon: core.EphemeralContainerCommon{ | ||||||
|  | 				Name:                     "foo", | ||||||
|  | 				Image:                    "image", | ||||||
|  | 				ImagePullPolicy:          "IfNotPresent", | ||||||
|  | 				TerminationMessagePolicy: "File", | ||||||
|  | 				RestartPolicy:            &containerRestartPolicyAlways, | ||||||
|  | 			}, | ||||||
|  | 		}}, | ||||||
|  | 		field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].restartPolicy"}}, | ||||||
|  | 	}, { | ||||||
|  | 		"Forbidden RestartPolicy: OnFailure", | ||||||
|  | 		line(), | ||||||
|  | 		[]core.EphemeralContainer{{ | ||||||
|  | 			EphemeralContainerCommon: core.EphemeralContainerCommon{ | ||||||
|  | 				Name:                     "foo", | ||||||
|  | 				Image:                    "image", | ||||||
|  | 				ImagePullPolicy:          "IfNotPresent", | ||||||
|  | 				TerminationMessagePolicy: "File", | ||||||
|  | 				RestartPolicy:            &containerRestartPolicyOnFailure, | ||||||
|  | 			}, | ||||||
|  | 		}}, | ||||||
|  | 		field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].restartPolicy"}}, | ||||||
|  | 	}, { | ||||||
|  | 		"Forbidden RestartPolicy: Never", | ||||||
|  | 		line(), | ||||||
|  | 		[]core.EphemeralContainer{{ | ||||||
|  | 			EphemeralContainerCommon: core.EphemeralContainerCommon{ | ||||||
|  | 				Name:                     "foo", | ||||||
|  | 				Image:                    "image", | ||||||
|  | 				ImagePullPolicy:          "IfNotPresent", | ||||||
|  | 				TerminationMessagePolicy: "File", | ||||||
|  | 				RestartPolicy:            &containerRestartPolicyNever, | ||||||
|  | 			}, | ||||||
|  | 		}}, | ||||||
|  | 		field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].restartPolicy"}}, | ||||||
|  | 	}, { | ||||||
|  | 		"Forbidden RestartPolicy: invalid", | ||||||
|  | 		line(), | ||||||
|  | 		[]core.EphemeralContainer{{ | ||||||
|  | 			EphemeralContainerCommon: core.EphemeralContainerCommon{ | ||||||
|  | 				Name:                     "foo", | ||||||
|  | 				Image:                    "image", | ||||||
|  | 				ImagePullPolicy:          "IfNotPresent", | ||||||
|  | 				TerminationMessagePolicy: "File", | ||||||
|  | 				RestartPolicy:            &containerRestartPolicyInvalid, | ||||||
|  | 			}, | ||||||
|  | 		}}, | ||||||
|  | 		field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].restartPolicy"}}, | ||||||
|  | 	}, { | ||||||
|  | 		"Forbidden RestartPolicy: empty", | ||||||
|  | 		line(), | ||||||
|  | 		[]core.EphemeralContainer{{ | ||||||
|  | 			EphemeralContainerCommon: core.EphemeralContainerCommon{ | ||||||
|  | 				Name:                     "foo", | ||||||
|  | 				Image:                    "image", | ||||||
|  | 				ImagePullPolicy:          "IfNotPresent", | ||||||
|  | 				TerminationMessagePolicy: "File", | ||||||
|  | 				RestartPolicy:            &containerRestartPolicyEmpty, | ||||||
|  | 			}, | ||||||
|  | 		}}, | ||||||
|  | 		field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].restartPolicy"}}, | ||||||
| 	}, | 	}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -7986,6 +8059,61 @@ func TestValidateContainers(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 		}}, | 		}}, | ||||||
| 		field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "containers[0].resizePolicy"}}, | 		field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "containers[0].resizePolicy"}}, | ||||||
|  | 	}, { | ||||||
|  | 		"Forbidden RestartPolicy: Always", | ||||||
|  | 		line(), | ||||||
|  | 		[]core.Container{{ | ||||||
|  | 			Name:                     "foo", | ||||||
|  | 			Image:                    "image", | ||||||
|  | 			ImagePullPolicy:          "IfNotPresent", | ||||||
|  | 			TerminationMessagePolicy: "File", | ||||||
|  | 			RestartPolicy:            &containerRestartPolicyAlways, | ||||||
|  | 		}}, | ||||||
|  | 		field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "containers[0].restartPolicy"}}, | ||||||
|  | 	}, { | ||||||
|  | 		"Forbidden RestartPolicy: OnFailure", | ||||||
|  | 		line(), | ||||||
|  | 		[]core.Container{{ | ||||||
|  | 			Name:                     "foo", | ||||||
|  | 			Image:                    "image", | ||||||
|  | 			ImagePullPolicy:          "IfNotPresent", | ||||||
|  | 			TerminationMessagePolicy: "File", | ||||||
|  | 			RestartPolicy:            &containerRestartPolicyOnFailure, | ||||||
|  | 		}}, | ||||||
|  | 		field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "containers[0].restartPolicy"}}, | ||||||
|  | 	}, { | ||||||
|  | 		"Forbidden RestartPolicy: Never", | ||||||
|  | 		line(), | ||||||
|  | 		[]core.Container{{ | ||||||
|  | 			Name:                     "foo", | ||||||
|  | 			Image:                    "image", | ||||||
|  | 			ImagePullPolicy:          "IfNotPresent", | ||||||
|  | 			TerminationMessagePolicy: "File", | ||||||
|  | 			RestartPolicy:            &containerRestartPolicyNever, | ||||||
|  | 		}}, | ||||||
|  | 		field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "containers[0].restartPolicy"}}, | ||||||
|  | 	}, { | ||||||
|  | 		"Forbidden RestartPolicy: invalid", | ||||||
|  | 		line(), | ||||||
|  | 		[]core.Container{{ | ||||||
|  | 			Name:                     "foo", | ||||||
|  | 			Image:                    "image", | ||||||
|  | 			ImagePullPolicy:          "IfNotPresent", | ||||||
|  | 			TerminationMessagePolicy: "File", | ||||||
|  | 			RestartPolicy:            &containerRestartPolicyInvalid, | ||||||
|  | 		}}, | ||||||
|  | 		field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "containers[0].restartPolicy"}}, | ||||||
|  | 	}, { | ||||||
|  | 		"Forbidden RestartPolicy: empty", | ||||||
|  | 		line(), | ||||||
|  | 		[]core.Container{{ | ||||||
|  | 			Name:                     "foo", | ||||||
|  | 			Image:                    "image", | ||||||
|  | 			ImagePullPolicy:          "IfNotPresent", | ||||||
|  | 			TerminationMessagePolicy: "File", | ||||||
|  | 			RestartPolicy:            &containerRestartPolicyEmpty, | ||||||
|  | 		}}, | ||||||
|  | 		field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "containers[0].restartPolicy"}}, | ||||||
| 	}, | 	}, | ||||||
| 	} | 	} | ||||||
| 	for _, tc := range errorCases { | 	for _, tc := range errorCases { | ||||||
| @@ -8035,6 +8163,18 @@ func TestValidateInitContainers(t *testing.T) { | |||||||
| 		}, | 		}, | ||||||
| 		ImagePullPolicy:          "IfNotPresent", | 		ImagePullPolicy:          "IfNotPresent", | ||||||
| 		TerminationMessagePolicy: "File", | 		TerminationMessagePolicy: "File", | ||||||
|  | 	}, { | ||||||
|  | 		Name:                     "container-3-restart-always-with-startup-probe", | ||||||
|  | 		Image:                    "image", | ||||||
|  | 		ImagePullPolicy:          "IfNotPresent", | ||||||
|  | 		TerminationMessagePolicy: "File", | ||||||
|  | 		RestartPolicy:            &containerRestartPolicyAlways, | ||||||
|  | 		StartupProbe: &core.Probe{ | ||||||
|  | 			ProbeHandler: core.ProbeHandler{ | ||||||
|  | 				TCPSocket: &core.TCPSocketAction{Port: intstr.FromInt(80)}, | ||||||
|  | 			}, | ||||||
|  | 			SuccessThreshold: 1, | ||||||
|  | 		}, | ||||||
| 	}, | 	}, | ||||||
| 	} | 	} | ||||||
| 	if errs := validateInitContainers(successCase, containers, volumeDevices, nil, field.NewPath("field"), PodValidationOptions{}); len(errs) != 0 { | 	if errs := validateInitContainers(successCase, containers, volumeDevices, nil, field.NewPath("field"), PodValidationOptions{}); len(errs) != 0 { | ||||||
| @@ -8191,6 +8331,67 @@ func TestValidateInitContainers(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 		}}, | 		}}, | ||||||
| 		field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "initContainers[0].startupProbe", BadValue: ""}}, | 		field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "initContainers[0].startupProbe", BadValue: ""}}, | ||||||
|  | 	}, { | ||||||
|  | 		"Not supported RestartPolicy: OnFailure", | ||||||
|  | 		line(), | ||||||
|  | 		[]core.Container{{ | ||||||
|  | 			Name:                     "init", | ||||||
|  | 			Image:                    "image", | ||||||
|  | 			ImagePullPolicy:          "IfNotPresent", | ||||||
|  | 			TerminationMessagePolicy: "File", | ||||||
|  | 			RestartPolicy:            &containerRestartPolicyOnFailure, | ||||||
|  | 		}}, | ||||||
|  | 		field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "initContainers[0].restartPolicy", BadValue: containerRestartPolicyOnFailure}}, | ||||||
|  | 	}, { | ||||||
|  | 		"Not supported RestartPolicy: Never", | ||||||
|  | 		line(), | ||||||
|  | 		[]core.Container{{ | ||||||
|  | 			Name:                     "init", | ||||||
|  | 			Image:                    "image", | ||||||
|  | 			ImagePullPolicy:          "IfNotPresent", | ||||||
|  | 			TerminationMessagePolicy: "File", | ||||||
|  | 			RestartPolicy:            &containerRestartPolicyNever, | ||||||
|  | 		}}, | ||||||
|  | 		field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "initContainers[0].restartPolicy", BadValue: containerRestartPolicyNever}}, | ||||||
|  | 	}, { | ||||||
|  | 		"Not supported RestartPolicy: invalid", | ||||||
|  | 		line(), | ||||||
|  | 		[]core.Container{{ | ||||||
|  | 			Name:                     "init", | ||||||
|  | 			Image:                    "image", | ||||||
|  | 			ImagePullPolicy:          "IfNotPresent", | ||||||
|  | 			TerminationMessagePolicy: "File", | ||||||
|  | 			RestartPolicy:            &containerRestartPolicyInvalid, | ||||||
|  | 		}}, | ||||||
|  | 		field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "initContainers[0].restartPolicy", BadValue: containerRestartPolicyInvalid}}, | ||||||
|  | 	}, { | ||||||
|  | 		"Not supported RestartPolicy: empty", | ||||||
|  | 		line(), | ||||||
|  | 		[]core.Container{{ | ||||||
|  | 			Name:                     "init", | ||||||
|  | 			Image:                    "image", | ||||||
|  | 			ImagePullPolicy:          "IfNotPresent", | ||||||
|  | 			TerminationMessagePolicy: "File", | ||||||
|  | 			RestartPolicy:            &containerRestartPolicyEmpty, | ||||||
|  | 		}}, | ||||||
|  | 		field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "initContainers[0].restartPolicy", BadValue: containerRestartPolicyEmpty}}, | ||||||
|  | 	}, { | ||||||
|  | 		"invalid startup probe in restartable container, successThreshold != 1", | ||||||
|  | 		line(), | ||||||
|  | 		[]core.Container{{ | ||||||
|  | 			Name:                     "restartable-init", | ||||||
|  | 			Image:                    "image", | ||||||
|  | 			ImagePullPolicy:          "IfNotPresent", | ||||||
|  | 			TerminationMessagePolicy: "File", | ||||||
|  | 			RestartPolicy:            &containerRestartPolicyAlways, | ||||||
|  | 			StartupProbe: &core.Probe{ | ||||||
|  | 				ProbeHandler: core.ProbeHandler{ | ||||||
|  | 					TCPSocket: &core.TCPSocketAction{Port: intstr.FromInt(80)}, | ||||||
|  | 				}, | ||||||
|  | 				SuccessThreshold: 2, | ||||||
|  | 			}, | ||||||
|  | 		}}, | ||||||
|  | 		field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "initContainers[0].startupProbe.successThreshold", BadValue: int32(2)}}, | ||||||
| 	}, | 	}, | ||||||
| 	} | 	} | ||||||
| 	for _, tc := range errorCases { | 	for _, tc := range errorCases { | ||||||
| @@ -19323,6 +19524,7 @@ func TestValidateOSFields(t *testing.T) { | |||||||
| 		"Containers[*].Resources", | 		"Containers[*].Resources", | ||||||
| 		"Containers[*].ResizePolicy[*].RestartPolicy", | 		"Containers[*].ResizePolicy[*].RestartPolicy", | ||||||
| 		"Containers[*].ResizePolicy[*].ResourceName", | 		"Containers[*].ResizePolicy[*].ResourceName", | ||||||
|  | 		"Containers[*].RestartPolicy", | ||||||
| 		"Containers[*].SecurityContext.RunAsNonRoot", | 		"Containers[*].SecurityContext.RunAsNonRoot", | ||||||
| 		"Containers[*].Stdin", | 		"Containers[*].Stdin", | ||||||
| 		"Containers[*].StdinOnce", | 		"Containers[*].StdinOnce", | ||||||
| @@ -19349,6 +19551,7 @@ func TestValidateOSFields(t *testing.T) { | |||||||
| 		"EphemeralContainers[*].EphemeralContainerCommon.Resources", | 		"EphemeralContainers[*].EphemeralContainerCommon.Resources", | ||||||
| 		"EphemeralContainers[*].EphemeralContainerCommon.ResizePolicy[*].RestartPolicy", | 		"EphemeralContainers[*].EphemeralContainerCommon.ResizePolicy[*].RestartPolicy", | ||||||
| 		"EphemeralContainers[*].EphemeralContainerCommon.ResizePolicy[*].ResourceName", | 		"EphemeralContainers[*].EphemeralContainerCommon.ResizePolicy[*].ResourceName", | ||||||
|  | 		"EphemeralContainers[*].EphemeralContainerCommon.RestartPolicy", | ||||||
| 		"EphemeralContainers[*].EphemeralContainerCommon.Stdin", | 		"EphemeralContainers[*].EphemeralContainerCommon.Stdin", | ||||||
| 		"EphemeralContainers[*].EphemeralContainerCommon.StdinOnce", | 		"EphemeralContainers[*].EphemeralContainerCommon.StdinOnce", | ||||||
| 		"EphemeralContainers[*].EphemeralContainerCommon.TTY", | 		"EphemeralContainers[*].EphemeralContainerCommon.TTY", | ||||||
| @@ -19377,6 +19580,7 @@ func TestValidateOSFields(t *testing.T) { | |||||||
| 		"InitContainers[*].Resources", | 		"InitContainers[*].Resources", | ||||||
| 		"InitContainers[*].ResizePolicy[*].RestartPolicy", | 		"InitContainers[*].ResizePolicy[*].RestartPolicy", | ||||||
| 		"InitContainers[*].ResizePolicy[*].ResourceName", | 		"InitContainers[*].ResizePolicy[*].ResourceName", | ||||||
|  | 		"InitContainers[*].RestartPolicy", | ||||||
| 		"InitContainers[*].Stdin", | 		"InitContainers[*].Stdin", | ||||||
| 		"InitContainers[*].StdinOnce", | 		"InitContainers[*].StdinOnce", | ||||||
| 		"InitContainers[*].TTY", | 		"InitContainers[*].TTY", | ||||||
|   | |||||||
| @@ -714,6 +714,15 @@ const ( | |||||||
| 	// Subdivide the NodePort range for dynamic and static port allocation. | 	// Subdivide the NodePort range for dynamic and static port allocation. | ||||||
| 	ServiceNodePortStaticSubrange featuregate.Feature = "ServiceNodePortStaticSubrange" | 	ServiceNodePortStaticSubrange featuregate.Feature = "ServiceNodePortStaticSubrange" | ||||||
|  |  | ||||||
|  | 	// owner: @gjkim42 @SergeyKanzhelev @matthyx @tzneal | ||||||
|  | 	// kep: http://kep.k8s.io/753 | ||||||
|  | 	// alpha: v1.28 | ||||||
|  | 	// | ||||||
|  | 	// Introduces sidecar containers, a new type of init container that starts | ||||||
|  | 	// before other containers but remains running for the full duration of the | ||||||
|  | 	// pod's lifecycle and will not block pod termination. | ||||||
|  | 	SidecarContainers featuregate.Feature = "SidecarContainers" | ||||||
|  |  | ||||||
| 	// owner: @derekwaynecarr | 	// owner: @derekwaynecarr | ||||||
| 	// alpha: v1.20 | 	// alpha: v1.20 | ||||||
| 	// beta: v1.22 | 	// beta: v1.22 | ||||||
| @@ -1037,6 +1046,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS | |||||||
|  |  | ||||||
| 	ServiceNodePortStaticSubrange: {Default: true, PreRelease: featuregate.Beta}, | 	ServiceNodePortStaticSubrange: {Default: true, PreRelease: featuregate.Beta}, | ||||||
|  |  | ||||||
|  | 	SidecarContainers: {Default: false, PreRelease: featuregate.Alpha}, | ||||||
|  |  | ||||||
| 	SizeMemoryBackedVolumes: {Default: true, PreRelease: featuregate.Beta}, | 	SizeMemoryBackedVolumes: {Default: true, PreRelease: featuregate.Beta}, | ||||||
|  |  | ||||||
| 	StableLoadBalancerNodeSet: {Default: true, PreRelease: featuregate.Beta}, | 	StableLoadBalancerNodeSet: {Default: true, PreRelease: featuregate.Beta}, | ||||||
|   | |||||||
| @@ -2446,6 +2446,24 @@ type Container struct { | |||||||
| 	// +optional | 	// +optional | ||||||
| 	// +listType=atomic | 	// +listType=atomic | ||||||
| 	ResizePolicy []ContainerResizePolicy `json:"resizePolicy,omitempty" protobuf:"bytes,23,rep,name=resizePolicy"` | 	ResizePolicy []ContainerResizePolicy `json:"resizePolicy,omitempty" protobuf:"bytes,23,rep,name=resizePolicy"` | ||||||
|  | 	// RestartPolicy defines the restart behavior of individual containers in a pod. | ||||||
|  | 	// This field may only be set for init containers, and the only allowed value is "Always". | ||||||
|  | 	// For non-init containers or when this field is not specified, | ||||||
|  | 	// the restart behavior is defined by the Pod's restart policy and the container type. | ||||||
|  | 	// Setting the RestartPolicy as "Always" for the init container will have the following effect: | ||||||
|  | 	// this init container will be continually restarted on | ||||||
|  | 	// exit until all regular containers have terminated. Once all regular | ||||||
|  | 	// containers have completed, all init containers with restartPolicy "Always" | ||||||
|  | 	// will be shut down. This lifecycle differs from normal init containers and | ||||||
|  | 	// is often referred to as a "sidecar" container. Although this init | ||||||
|  | 	// container still starts in the init container sequence, it does not wait | ||||||
|  | 	// for the container to complete before proceeding to the next init | ||||||
|  | 	// container. Instead, the next init container starts immediately after this | ||||||
|  | 	// init container is started, or after any startupProbe has successfully | ||||||
|  | 	// completed. | ||||||
|  | 	// +featureGate=SidecarContainers | ||||||
|  | 	// +optional | ||||||
|  | 	RestartPolicy *ContainerRestartPolicy `json:"restartPolicy,omitempty" protobuf:"bytes,24,opt,name=restartPolicy,casttype=ContainerRestartPolicy"` | ||||||
| 	// Pod volumes to mount into the container's filesystem. | 	// Pod volumes to mount into the container's filesystem. | ||||||
| 	// Cannot be updated. | 	// Cannot be updated. | ||||||
| 	// +optional | 	// +optional | ||||||
| @@ -2842,6 +2860,14 @@ const ( | |||||||
| 	RestartPolicyNever     RestartPolicy = "Never" | 	RestartPolicyNever     RestartPolicy = "Never" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // ContainerRestartPolicy is the restart policy for a single container. | ||||||
|  | // This may only be set for init containers and only allowed value is "Always". | ||||||
|  | type ContainerRestartPolicy string | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	ContainerRestartPolicyAlways ContainerRestartPolicy = "Always" | ||||||
|  | ) | ||||||
|  |  | ||||||
| // DNSPolicy defines how a pod's DNS will be configured. | // DNSPolicy defines how a pod's DNS will be configured. | ||||||
| // +enum | // +enum | ||||||
| type DNSPolicy string | type DNSPolicy string | ||||||
| @@ -3976,6 +4002,13 @@ type EphemeralContainerCommon struct { | |||||||
| 	// +optional | 	// +optional | ||||||
| 	// +listType=atomic | 	// +listType=atomic | ||||||
| 	ResizePolicy []ContainerResizePolicy `json:"resizePolicy,omitempty" protobuf:"bytes,23,rep,name=resizePolicy"` | 	ResizePolicy []ContainerResizePolicy `json:"resizePolicy,omitempty" protobuf:"bytes,23,rep,name=resizePolicy"` | ||||||
|  | 	// Restart policy for the container to manage the restart behavior of each | ||||||
|  | 	// container within a pod. | ||||||
|  | 	// This may only be set for init containers. You cannot set this field on | ||||||
|  | 	// ephemeral containers. | ||||||
|  | 	// +featureGate=SidecarContainers | ||||||
|  | 	// +optional | ||||||
|  | 	RestartPolicy *ContainerRestartPolicy `json:"restartPolicy,omitempty" protobuf:"bytes,24,opt,name=restartPolicy,casttype=ContainerRestartPolicy"` | ||||||
| 	// Pod volumes to mount into the container's filesystem. Subpath mounts are not allowed for ephemeral containers. | 	// Pod volumes to mount into the container's filesystem. Subpath mounts are not allowed for ephemeral containers. | ||||||
| 	// Cannot be updated. | 	// Cannot be updated. | ||||||
| 	// +optional | 	// +optional | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user