diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go index 139d9bc29d7..b0a10f68039 100644 --- a/pkg/apis/core/validation/validation.go +++ b/pkg/apis/core/validation/validation.go @@ -2647,6 +2647,19 @@ const ( MaxDNSSearchListChars = 256 ) +func validateReadinessGates(readinessGates []core.PodReadinessGate, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if !utilfeature.DefaultFeatureGate.Enabled(features.PodReadinessGates) && len(readinessGates) > 0 { + return append(allErrs, field.Forbidden(fldPath, "PodReadinessGates is disabled by feature gate")) + } + for i, value := range readinessGates { + for _, msg := range validation.IsQualifiedName(string(value.ConditionType)) { + allErrs = append(allErrs, field.Invalid(fldPath.Index(i).Child("conditionType"), string(value.ConditionType), msg)) + } + } + return allErrs +} + func validatePodDNSConfig(dnsConfig *core.PodDNSConfig, dnsPolicy *core.DNSPolicy, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} @@ -2935,6 +2948,7 @@ func ValidatePodSpec(spec *core.PodSpec, fldPath *field.Path) field.ErrorList { allErrs = append(allErrs, validateImagePullSecrets(spec.ImagePullSecrets, fldPath.Child("imagePullSecrets"))...) allErrs = append(allErrs, validateAffinity(spec.Affinity, fldPath.Child("affinity"))...) allErrs = append(allErrs, validatePodDNSConfig(spec.DNSConfig, &spec.DNSPolicy, fldPath.Child("dnsConfig"))...) + allErrs = append(allErrs, validateReadinessGates(spec.ReadinessGates, fldPath.Child("readinessGates"))...) if len(spec.ServiceAccountName) > 0 { for _, msg := range ValidateServiceAccountName(spec.ServiceAccountName, false) { allErrs = append(allErrs, field.Invalid(fldPath.Child("serviceAccountName"), spec.ServiceAccountName, msg)) @@ -3485,6 +3499,7 @@ func ValidatePodStatusUpdate(newPod, oldPod *core.Pod) field.ErrorList { fldPath := field.NewPath("metadata") allErrs := ValidateObjectMetaUpdate(&newPod.ObjectMeta, &oldPod.ObjectMeta, fldPath) allErrs = append(allErrs, ValidatePodSpecificAnnotationUpdates(newPod, oldPod, fldPath.Child("annotations"))...) + allErrs = append(allErrs, validatePodConditions(newPod.Status.Conditions, fldPath.Child("conditions"))...) fldPath = field.NewPath("status") if newPod.Spec.NodeName != oldPod.Spec.NodeName { @@ -3508,6 +3523,21 @@ func ValidatePodStatusUpdate(newPod, oldPod *core.Pod) field.ErrorList { return allErrs } +// validatePodConditions tests if the custom pod conditions are valid. +func validatePodConditions(conditions []core.PodCondition, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + systemConditions := sets.NewString(string(core.PodScheduled), string(core.PodReady), string(core.PodInitialized)) + for i, condition := range conditions { + if systemConditions.Has(string(condition.Type)) { + continue + } + for _, msg := range validation.IsQualifiedName(string(condition.Type)) { + allErrs = append(allErrs, field.Invalid(fldPath.Index(i).Child("Type"), string(condition.Type), msg)) + } + } + return allErrs +} + // ValidatePodBinding tests if required fields in the pod binding are legal. func ValidatePodBinding(binding *core.Binding) field.ErrorList { allErrs := field.ErrorList{} diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go index 416f4cd4890..1cd0c51f0f6 100644 --- a/pkg/apis/core/validation/validation_test.go +++ b/pkg/apis/core/validation/validation_test.go @@ -5865,6 +5865,149 @@ func TestValidatePodDNSConfig(t *testing.T) { } } +func TestValidatePodReadinessGates(t *testing.T) { + podReadinessGatesEnabled := utilfeature.DefaultFeatureGate.Enabled(features.PodReadinessGates) + defer func() { + // Restoring the old value. + if err := utilfeature.DefaultFeatureGate.Set(fmt.Sprintf("%s=%v", features.PodReadinessGates, podReadinessGatesEnabled)); err != nil { + t.Errorf("Failed to restore PodReadinessGates feature gate: %v", err) + } + }() + if err := utilfeature.DefaultFeatureGate.Set(fmt.Sprintf("%s=true", features.PodReadinessGates)); err != nil { + t.Errorf("Failed to enable PodReadinessGates feature gate: %v", err) + } + + successCases := []struct { + desc string + readinessGates []core.PodReadinessGate + }{ + { + "no gate", + []core.PodReadinessGate{}, + }, + { + "one readiness gate", + []core.PodReadinessGate{ + { + ConditionType: core.PodConditionType("example.com/condition"), + }, + }, + }, + { + "two readiness gates", + []core.PodReadinessGate{ + { + ConditionType: core.PodConditionType("example.com/condition1"), + }, + { + ConditionType: core.PodConditionType("example.com/condition2"), + }, + }, + }, + } + for _, tc := range successCases { + if errs := validateReadinessGates(tc.readinessGates, field.NewPath("field")); len(errs) != 0 { + t.Errorf("expect tc %q to success: %v", tc.desc, errs) + } + } + + errorCases := []struct { + desc string + readinessGates []core.PodReadinessGate + }{ + { + "invalid condition type", + []core.PodReadinessGate{ + { + ConditionType: core.PodConditionType("invalid/condition/type"), + }, + }, + }, + } + for _, tc := range errorCases { + if errs := validateReadinessGates(tc.readinessGates, field.NewPath("field")); len(errs) == 0 { + t.Errorf("expected tc %q to fail", tc.desc) + } + } +} + +func TestValidatePodConditions(t *testing.T) { + successCases := []struct { + desc string + podConditions []core.PodCondition + }{ + { + "no condition", + []core.PodCondition{}, + }, + { + "one system condition", + []core.PodCondition{ + { + Type: core.PodReady, + Status: core.ConditionTrue, + }, + }, + }, + { + "one system condition and one custom condition", + []core.PodCondition{ + { + Type: core.PodReady, + Status: core.ConditionTrue, + }, + { + Type: core.PodConditionType("example.com/condition"), + Status: core.ConditionFalse, + }, + }, + }, + { + "two custom condition", + []core.PodCondition{ + { + Type: core.PodConditionType("foobar"), + Status: core.ConditionTrue, + }, + { + Type: core.PodConditionType("example.com/condition"), + Status: core.ConditionFalse, + }, + }, + }, + } + + for _, tc := range successCases { + if errs := validatePodConditions(tc.podConditions, field.NewPath("field")); len(errs) != 0 { + t.Errorf("expected tc %q to success, but got: %v", tc.desc, errs) + } + } + + errorCases := []struct { + desc string + podConditions []core.PodCondition + }{ + { + "one system condition and a invalid custom condition", + []core.PodCondition{ + { + Type: core.PodReady, + Status: core.ConditionStatus("True"), + }, + { + Type: core.PodConditionType("invalid/custom/condition"), + Status: core.ConditionStatus("True"), + }, + }, + }, + } + for _, tc := range errorCases { + if errs := validatePodConditions(tc.podConditions, field.NewPath("field")); len(errs) == 0 { + t.Errorf("expected tc %q to fail", tc.desc) + } + } +} + func TestValidatePodSpec(t *testing.T) { activeDeadlineSeconds := int64(30) activeDeadlineSecondsMax := int64(math.MaxInt32) diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index c152b9caa97..300c675653a 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -292,6 +292,12 @@ const ( // while making decisions. BalanceAttachedNodeVolumes utilfeature.Feature = "BalanceAttachedNodeVolumes" + // owner @freehan + // beta: v1.11 + // + // Support Pod Ready++ + PodReadinessGates utilfeature.Feature = "PodReadinessGates" + // owner: @lichuqiang // alpha: v1.11 // @@ -362,6 +368,7 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS VolumeSubpath: {Default: true, PreRelease: utilfeature.GA}, BalanceAttachedNodeVolumes: {Default: false, PreRelease: utilfeature.Alpha}, DynamicProvisioningScheduling: {Default: false, PreRelease: utilfeature.Alpha}, + PodReadinessGates: {Default: false, PreRelease: utilfeature.Beta}, VolumeSubpathEnvExpansion: {Default: false, PreRelease: utilfeature.Alpha}, KubeletPluginsWatcher: {Default: false, PreRelease: utilfeature.Alpha},