diff --git a/pkg/apis/batch/types.go b/pkg/apis/batch/types.go index e1154e025ab..dbddde490a1 100644 --- a/pkg/apis/batch/types.go +++ b/pkg/apis/batch/types.go @@ -22,16 +22,29 @@ import ( api "k8s.io/kubernetes/pkg/apis/core" ) -// JobTrackingFinalizer is a finalizer for Job's pods. It prevents them from -// being deleted before being accounted in the Job status. -// -// Additionally, the apiserver and job controller use this string as a Job -// annotation, to mark Jobs that are being tracked using pod finalizers. -// However, this behavior is deprecated in kubernetes 1.26. This means that, in -// 1.27+, one release after JobTrackingWithFinalizers graduates to GA, the -// apiserver and job controller will ignore this annotation and they will -// always track jobs using finalizers. -const JobTrackingFinalizer = "batch.kubernetes.io/job-tracking" +const ( + // Unprefixed labels are reserved for end-users + // so we will add a batch.kubernetes.io to designate these labels as official Kubernetes labels. + // See https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#label-selector-and-annotation-conventions + labelPrefix = "batch.kubernetes.io/" + // JobTrackingFinalizer is a finalizer for Job's pods. It prevents them from + // being deleted before being accounted in the Job status. + // + // Additionally, the apiserver and job controller use this string as a Job + // annotation, to mark Jobs that are being tracked using pod finalizers. + // However, this behavior is deprecated in kubernetes 1.26. This means that, in + // 1.27+, one release after JobTrackingWithFinalizers graduates to GA, the + // apiserver and job controller will ignore this annotation and they will + // always track jobs using finalizers. + JobTrackingFinalizer = labelPrefix + "job-tracking" + // LegacyJobName and LegacyControllerUid are legacy labels that were set using unprefixed labels. + LegacyJobNameLabel = "job-name" + LegacyControllerUidLabel = "controller-uid" + // JobName is a user friendly way to refer to jobs and is set in the labels for jobs. + JobNameLabel = labelPrefix + LegacyJobNameLabel + // Controller UID is used for selectors and labels for jobs + ControllerUidLabel = labelPrefix + LegacyControllerUidLabel +) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/batch/validation/validation.go b/pkg/apis/batch/validation/validation.go index b45bbd7d7e2..e8815c3842d 100644 --- a/pkg/apis/batch/validation/validation.go +++ b/pkg/apis/batch/validation/validation.go @@ -70,12 +70,12 @@ var ( string(v1.ConditionUnknown)) ) -// ValidateGeneratedSelector validates that the generated selector on a controller object match the controller object +// validateGeneratedSelector validates that the generated selector on a controller object match the controller object // metadata, and the labels on the pod template are as generated. // // TODO: generalize for other controller objects that will follow the same pattern, such as ReplicaSet and DaemonSet, and // move to new location. Replace batch.Job with an interface. -func ValidateGeneratedSelector(obj *batch.Job) field.ErrorList { +func validateGeneratedSelector(obj *batch.Job, validateBatchLabels bool) field.ErrorList { allErrs := field.ErrorList{} if obj.Spec.ManualSelector != nil && *obj.Spec.ManualSelector { return allErrs @@ -100,12 +100,20 @@ func ValidateGeneratedSelector(obj *batch.Job) field.ErrorList { // have placed certain labels on the pod, but this could have failed if // the user added coflicting labels. Validate that the expected // generated ones are there. - - allErrs = append(allErrs, apivalidation.ValidateHasLabel(obj.Spec.Template.ObjectMeta, field.NewPath("spec").Child("template").Child("metadata"), "controller-uid", string(obj.UID))...) - allErrs = append(allErrs, apivalidation.ValidateHasLabel(obj.Spec.Template.ObjectMeta, field.NewPath("spec").Child("template").Child("metadata"), "job-name", string(obj.Name))...) + allErrs = append(allErrs, apivalidation.ValidateHasLabel(obj.Spec.Template.ObjectMeta, field.NewPath("spec").Child("template").Child("metadata"), batch.LegacyControllerUidLabel, string(obj.UID))...) + allErrs = append(allErrs, apivalidation.ValidateHasLabel(obj.Spec.Template.ObjectMeta, field.NewPath("spec").Child("template").Child("metadata"), batch.LegacyJobNameLabel, string(obj.Name))...) expectedLabels := make(map[string]string) - expectedLabels["controller-uid"] = string(obj.UID) - expectedLabels["job-name"] = string(obj.Name) + if validateBatchLabels { + allErrs = append(allErrs, apivalidation.ValidateHasLabel(obj.Spec.Template.ObjectMeta, field.NewPath("spec").Child("template").Child("metadata"), batch.ControllerUidLabel, string(obj.UID))...) + allErrs = append(allErrs, apivalidation.ValidateHasLabel(obj.Spec.Template.ObjectMeta, field.NewPath("spec").Child("template").Child("metadata"), batch.JobNameLabel, string(obj.Name))...) + expectedLabels[batch.ControllerUidLabel] = string(obj.UID) + expectedLabels[batch.JobNameLabel] = string(obj.Name) + } + // Labels created by the Kubernetes project should have a Kubernetes prefix. + // These labels are set due to legacy reasons. + + expectedLabels[batch.LegacyControllerUidLabel] = string(obj.UID) + expectedLabels[batch.LegacyJobNameLabel] = string(obj.Name) // Whether manually or automatically generated, the selector of the job must match the pods it will produce. if selector, err := metav1.LabelSelectorAsSelector(obj.Spec.Selector); err == nil { if !selector.Matches(labels.Set(expectedLabels)) { @@ -120,7 +128,7 @@ func ValidateGeneratedSelector(obj *batch.Job) field.ErrorList { func ValidateJob(job *batch.Job, opts JobValidationOptions) field.ErrorList { // Jobs and rcs have the same name validation allErrs := apivalidation.ValidateObjectMeta(&job.ObjectMeta, true, apivalidation.ValidateReplicationControllerName, field.NewPath("metadata")) - allErrs = append(allErrs, ValidateGeneratedSelector(job)...) + allErrs = append(allErrs, validateGeneratedSelector(job, opts.RequirePrefixedLabels)...) allErrs = append(allErrs, ValidateJobSpec(&job.Spec, field.NewPath("spec"), opts.PodValidationOptions)...) if !opts.AllowTrackingAnnotation && hasJobTrackingAnnotation(job) { allErrs = append(allErrs, field.Forbidden(field.NewPath("metadata").Child("annotations").Key(batch.JobTrackingFinalizer), "cannot add this annotation")) @@ -596,4 +604,6 @@ type JobValidationOptions struct { AllowMutableSchedulingDirectives bool // Allow elastic indexed jobs AllowElasticIndexedJobs bool + // Require Job to have the label on batch.kubernetes.io/job-name and batch.kubernetes.io/controller-uid + RequirePrefixedLabels bool } diff --git a/pkg/apis/batch/validation/validation_test.go b/pkg/apis/batch/validation/validation_test.go index 7f38d6bfadb..af5828d284e 100644 --- a/pkg/apis/batch/validation/validation_test.go +++ b/pkg/apis/batch/validation/validation_test.go @@ -69,7 +69,7 @@ func getValidPodTemplateSpecForManual(selector *metav1.LabelSelector) api.PodTem func getValidGeneratedSelector() *metav1.LabelSelector { return &metav1.LabelSelector{ - MatchLabels: map[string]string{"controller-uid": "1a2b3c", "job-name": "myjob"}, + MatchLabels: map[string]string{batch.ControllerUidLabel: "1a2b3c", batch.LegacyControllerUidLabel: "1a2b3c", batch.JobNameLabel: "myjob", batch.LegacyJobNameLabel: "myjob"}, } } @@ -105,6 +105,7 @@ func TestValidateJob(t *testing.T) { job batch.Job }{ "valid pod failure policy": { + opts: JobValidationOptions{RequirePrefixedLabels: true}, job: batch.Job{ ObjectMeta: validJobObjectMeta, Spec: batch.JobSpec{ @@ -159,6 +160,7 @@ func TestValidateJob(t *testing.T) { }, }, "valid manual selector": { + opts: JobValidationOptions{RequirePrefixedLabels: true}, job: batch.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "myjob", @@ -174,6 +176,7 @@ func TestValidateJob(t *testing.T) { }, }, "valid generated selector": { + opts: JobValidationOptions{RequirePrefixedLabels: true}, job: batch.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "myjob", @@ -187,6 +190,7 @@ func TestValidateJob(t *testing.T) { }, }, "valid NonIndexed completion mode": { + opts: JobValidationOptions{RequirePrefixedLabels: true}, job: batch.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "myjob", @@ -201,6 +205,7 @@ func TestValidateJob(t *testing.T) { }, }, "valid Indexed completion mode": { + opts: JobValidationOptions{RequirePrefixedLabels: true}, job: batch.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "myjob", @@ -219,6 +224,7 @@ func TestValidateJob(t *testing.T) { "valid job tracking annotation": { opts: JobValidationOptions{ AllowTrackingAnnotation: true, + RequirePrefixedLabels: true, }, job: batch.Job{ ObjectMeta: metav1.ObjectMeta{ @@ -235,6 +241,58 @@ func TestValidateJob(t *testing.T) { }, }, }, + "valid batch labels": { + opts: JobValidationOptions{ + AllowTrackingAnnotation: true, + RequirePrefixedLabels: true, + }, + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + Annotations: map[string]string{ + batch.JobTrackingFinalizer: "", + }, + }, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGenerated, + }, + }, + }, + "do not allow new batch labels": { + opts: JobValidationOptions{ + AllowTrackingAnnotation: true, + RequirePrefixedLabels: false, + }, + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + Annotations: map[string]string{ + batch.JobTrackingFinalizer: "", + }, + }, + Spec: batch.JobSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{batch.LegacyControllerUidLabel: "1a2b3c"}, + }, + Template: api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{batch.LegacyControllerUidLabel: "1a2b3c", batch.LegacyJobNameLabel: "myjob"}, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyOnFailure, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + InitContainers: []api.Container{{Name: "def", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + }, + }, + }, + }, + }, } for k, v := range successCases { t.Run(k, func(t *testing.T) { @@ -245,603 +303,797 @@ func TestValidateJob(t *testing.T) { } negative := int32(-1) negative64 := int64(-1) - errorCases := map[string]batch.Job{ + errorCases := map[string]struct { + opts JobValidationOptions + job batch.Job + }{ `spec.podFailurePolicy.rules[0]: Invalid value: specifying one of OnExitCodes and OnPodConditions is required`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGeneratedRestartPolicyNever, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: []batch.PodFailurePolicyRule{ - { - Action: batch.PodFailurePolicyActionFailJob, - }, - }, - }, - }, - }, - `spec.podFailurePolicy.rules[0].onExitCodes.values[1]: Duplicate value: 11`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGeneratedRestartPolicyNever, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: []batch.PodFailurePolicyRule{ - { - Action: batch.PodFailurePolicyActionFailJob, - OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ - Operator: batch.PodFailurePolicyOnExitCodesOpIn, - Values: []int32{11, 11}, + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGeneratedRestartPolicyNever, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: []batch.PodFailurePolicyRule{ + { + Action: batch.PodFailurePolicyActionFailJob, }, }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, + }, + `spec.podFailurePolicy.rules[0].onExitCodes.values[1]: Duplicate value: 11`: { + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGeneratedRestartPolicyNever, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: []batch.PodFailurePolicyRule{ + { + Action: batch.PodFailurePolicyActionFailJob, + OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ + Operator: batch.PodFailurePolicyOnExitCodesOpIn, + Values: []int32{11, 11}, + }, + }, + }, + }, + }, + }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, `spec.podFailurePolicy.rules[0].onExitCodes.values: Too many: 256: must have at most 255 items`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGeneratedRestartPolicyNever, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: []batch.PodFailurePolicyRule{ - { - Action: batch.PodFailurePolicyActionFailJob, - OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ - Operator: batch.PodFailurePolicyOnExitCodesOpIn, - Values: func() (values []int32) { - tooManyValues := make([]int32, maxPodFailurePolicyOnExitCodesValues+1) - for i := range tooManyValues { - tooManyValues[i] = int32(i) + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGeneratedRestartPolicyNever, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: []batch.PodFailurePolicyRule{ + { + Action: batch.PodFailurePolicyActionFailJob, + OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ + Operator: batch.PodFailurePolicyOnExitCodesOpIn, + Values: func() (values []int32) { + tooManyValues := make([]int32, maxPodFailurePolicyOnExitCodesValues+1) + for i := range tooManyValues { + tooManyValues[i] = int32(i) + } + return tooManyValues + }(), + }, + }, + }, + }, + }, + }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, + }, + `spec.podFailurePolicy.rules: Too many: 21: must have at most 20 items`: { + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGeneratedRestartPolicyNever, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: func() []batch.PodFailurePolicyRule { + tooManyRules := make([]batch.PodFailurePolicyRule, maxPodFailurePolicyRules+1) + for i := range tooManyRules { + tooManyRules[i] = batch.PodFailurePolicyRule{ + Action: batch.PodFailurePolicyActionFailJob, + OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ + Operator: batch.PodFailurePolicyOnExitCodesOpIn, + Values: []int32{int32(i + 1)}, + }, + } + } + return tooManyRules + }(), + }, + }, + }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, + }, + `spec.podFailurePolicy.rules[0].onPodConditions: Too many: 21: must have at most 20 items`: { + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGeneratedRestartPolicyNever, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: []batch.PodFailurePolicyRule{ + { + Action: batch.PodFailurePolicyActionFailJob, + OnPodConditions: func() []batch.PodFailurePolicyOnPodConditionsPattern { + tooManyPatterns := make([]batch.PodFailurePolicyOnPodConditionsPattern, maxPodFailurePolicyOnPodConditionsPatterns+1) + for i := range tooManyPatterns { + tooManyPatterns[i] = batch.PodFailurePolicyOnPodConditionsPattern{ + Type: api.PodConditionType(fmt.Sprintf("CustomType_%d", i)), + Status: api.ConditionTrue, + } } - return tooManyValues + return tooManyPatterns }(), }, }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, - `spec.podFailurePolicy.rules: Too many: 21: must have at most 20 items`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGeneratedRestartPolicyNever, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: func() []batch.PodFailurePolicyRule { - tooManyRules := make([]batch.PodFailurePolicyRule, maxPodFailurePolicyRules+1) - for i := range tooManyRules { - tooManyRules[i] = batch.PodFailurePolicyRule{ + `spec.podFailurePolicy.rules[0].onExitCodes.values[2]: Duplicate value: 13`: { + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGeneratedRestartPolicyNever, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: []batch.PodFailurePolicyRule{ + { Action: batch.PodFailurePolicyActionFailJob, OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ Operator: batch.PodFailurePolicyOnExitCodesOpIn, - Values: []int32{int32(i + 1)}, + Values: []int32{12, 13, 13, 13}, }, - } - } - return tooManyRules - }(), - }, - }, - }, - `spec.podFailurePolicy.rules[0].onPodConditions: Too many: 21: must have at most 20 items`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGeneratedRestartPolicyNever, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: []batch.PodFailurePolicyRule{ - { - Action: batch.PodFailurePolicyActionFailJob, - OnPodConditions: func() []batch.PodFailurePolicyOnPodConditionsPattern { - tooManyPatterns := make([]batch.PodFailurePolicyOnPodConditionsPattern, maxPodFailurePolicyOnPodConditionsPatterns+1) - for i := range tooManyPatterns { - tooManyPatterns[i] = batch.PodFailurePolicyOnPodConditionsPattern{ - Type: api.PodConditionType(fmt.Sprintf("CustomType_%d", i)), - Status: api.ConditionTrue, - } - } - return tooManyPatterns - }(), - }, - }, - }, - }, - }, - `spec.podFailurePolicy.rules[0].onExitCodes.values[2]: Duplicate value: 13`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGeneratedRestartPolicyNever, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: []batch.PodFailurePolicyRule{ - { - Action: batch.PodFailurePolicyActionFailJob, - OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ - Operator: batch.PodFailurePolicyOnExitCodesOpIn, - Values: []int32{12, 13, 13, 13}, }, }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, `spec.podFailurePolicy.rules[0].onExitCodes.values: Invalid value: []int32{19, 11}: must be ordered`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGeneratedRestartPolicyNever, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: []batch.PodFailurePolicyRule{ - { - Action: batch.PodFailurePolicyActionFailJob, - OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ - Operator: batch.PodFailurePolicyOnExitCodesOpIn, - Values: []int32{19, 11}, + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGeneratedRestartPolicyNever, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: []batch.PodFailurePolicyRule{ + { + Action: batch.PodFailurePolicyActionFailJob, + OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ + Operator: batch.PodFailurePolicyOnExitCodesOpIn, + Values: []int32{19, 11}, + }, }, }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, `spec.podFailurePolicy.rules[0].onExitCodes.values: Invalid value: []int32{}: at least one value is required`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGeneratedRestartPolicyNever, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: []batch.PodFailurePolicyRule{ - { - Action: batch.PodFailurePolicyActionFailJob, - OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ - Operator: batch.PodFailurePolicyOnExitCodesOpIn, - Values: []int32{}, + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGeneratedRestartPolicyNever, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: []batch.PodFailurePolicyRule{ + { + Action: batch.PodFailurePolicyActionFailJob, + OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ + Operator: batch.PodFailurePolicyOnExitCodesOpIn, + Values: []int32{}, + }, }, }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, `spec.podFailurePolicy.rules[0].action: Required value: valid values: ["Count" "FailJob" "Ignore"]`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGeneratedRestartPolicyNever, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: []batch.PodFailurePolicyRule{ - { - Action: "", - OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ - Operator: batch.PodFailurePolicyOnExitCodesOpIn, - Values: []int32{1, 2, 3}, + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGeneratedRestartPolicyNever, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: []batch.PodFailurePolicyRule{ + { + Action: "", + OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ + Operator: batch.PodFailurePolicyOnExitCodesOpIn, + Values: []int32{1, 2, 3}, + }, }, }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, `spec.podFailurePolicy.rules[0].onExitCodes.operator: Required value: valid values: ["In" "NotIn"]`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGeneratedRestartPolicyNever, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: []batch.PodFailurePolicyRule{ - { - Action: batch.PodFailurePolicyActionFailJob, - OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ - Operator: "", - Values: []int32{1, 2, 3}, + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGeneratedRestartPolicyNever, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: []batch.PodFailurePolicyRule{ + { + Action: batch.PodFailurePolicyActionFailJob, + OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ + Operator: "", + Values: []int32{1, 2, 3}, + }, }, }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, `spec.podFailurePolicy.rules[0]: Invalid value: specifying both OnExitCodes and OnPodConditions is not supported`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGeneratedRestartPolicyNever, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: []batch.PodFailurePolicyRule{ - { - Action: batch.PodFailurePolicyActionFailJob, - OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ - ContainerName: pointer.String("abc"), - Operator: batch.PodFailurePolicyOnExitCodesOpIn, - Values: []int32{1, 2, 3}, - }, - OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{ - { - Type: api.DisruptionTarget, - Status: api.ConditionTrue, + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGeneratedRestartPolicyNever, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: []batch.PodFailurePolicyRule{ + { + Action: batch.PodFailurePolicyActionFailJob, + OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ + ContainerName: pointer.String("abc"), + Operator: batch.PodFailurePolicyOnExitCodesOpIn, + Values: []int32{1, 2, 3}, + }, + OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{ + { + Type: api.DisruptionTarget, + Status: api.ConditionTrue, + }, }, }, }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, `spec.podFailurePolicy.rules[0].onExitCodes.values[1]: Invalid value: 0: must not be 0 for the In operator`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGeneratedRestartPolicyNever, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: []batch.PodFailurePolicyRule{ - { - Action: batch.PodFailurePolicyActionIgnore, - OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ - Operator: batch.PodFailurePolicyOnExitCodesOpIn, - Values: []int32{1, 0, 2}, + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGeneratedRestartPolicyNever, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: []batch.PodFailurePolicyRule{ + { + Action: batch.PodFailurePolicyActionIgnore, + OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ + Operator: batch.PodFailurePolicyOnExitCodesOpIn, + Values: []int32{1, 0, 2}, + }, }, }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, `spec.podFailurePolicy.rules[1].onExitCodes.containerName: Invalid value: "xyz": must be one of the container or initContainer names in the pod template`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGeneratedRestartPolicyNever, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: []batch.PodFailurePolicyRule{ - { - Action: batch.PodFailurePolicyActionIgnore, - OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ - ContainerName: pointer.String("abc"), - Operator: batch.PodFailurePolicyOnExitCodesOpIn, - Values: []int32{1, 2, 3}, + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGeneratedRestartPolicyNever, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: []batch.PodFailurePolicyRule{ + { + Action: batch.PodFailurePolicyActionIgnore, + OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ + ContainerName: pointer.String("abc"), + Operator: batch.PodFailurePolicyOnExitCodesOpIn, + Values: []int32{1, 2, 3}, + }, }, - }, - { - Action: batch.PodFailurePolicyActionFailJob, - OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ - ContainerName: pointer.String("xyz"), - Operator: batch.PodFailurePolicyOnExitCodesOpIn, - Values: []int32{5, 6, 7}, + { + Action: batch.PodFailurePolicyActionFailJob, + OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ + ContainerName: pointer.String("xyz"), + Operator: batch.PodFailurePolicyOnExitCodesOpIn, + Values: []int32{5, 6, 7}, + }, }, }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, `spec.podFailurePolicy.rules[0].action: Unsupported value: "UnknownAction": supported values: "Count", "FailJob", "Ignore"`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGeneratedRestartPolicyNever, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: []batch.PodFailurePolicyRule{ - { - Action: "UnknownAction", - OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ - ContainerName: pointer.String("abc"), - Operator: batch.PodFailurePolicyOnExitCodesOpIn, - Values: []int32{1, 2, 3}, + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGeneratedRestartPolicyNever, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: []batch.PodFailurePolicyRule{ + { + Action: "UnknownAction", + OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ + ContainerName: pointer.String("abc"), + Operator: batch.PodFailurePolicyOnExitCodesOpIn, + Values: []int32{1, 2, 3}, + }, }, }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, `spec.podFailurePolicy.rules[0].onExitCodes.operator: Unsupported value: "UnknownOperator": supported values: "In", "NotIn"`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGeneratedRestartPolicyNever, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: []batch.PodFailurePolicyRule{ - { - Action: batch.PodFailurePolicyActionIgnore, - OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ - Operator: "UnknownOperator", - Values: []int32{1, 2, 3}, + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGeneratedRestartPolicyNever, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: []batch.PodFailurePolicyRule{ + { + Action: batch.PodFailurePolicyActionIgnore, + OnExitCodes: &batch.PodFailurePolicyOnExitCodesRequirement{ + Operator: "UnknownOperator", + Values: []int32{1, 2, 3}, + }, }, }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, `spec.podFailurePolicy.rules[0].onPodConditions[0].status: Required value: valid values: ["False" "True" "Unknown"]`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGeneratedRestartPolicyNever, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: []batch.PodFailurePolicyRule{ - { - Action: batch.PodFailurePolicyActionIgnore, - OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{ - { - Type: api.DisruptionTarget, + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGeneratedRestartPolicyNever, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: []batch.PodFailurePolicyRule{ + { + Action: batch.PodFailurePolicyActionIgnore, + OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{ + { + Type: api.DisruptionTarget, + }, }, }, }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, `spec.podFailurePolicy.rules[0].onPodConditions[0].status: Unsupported value: "UnknownStatus": supported values: "False", "True", "Unknown"`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGeneratedRestartPolicyNever, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: []batch.PodFailurePolicyRule{ - { - Action: batch.PodFailurePolicyActionIgnore, - OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{ - { - Type: api.DisruptionTarget, - Status: "UnknownStatus", + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGeneratedRestartPolicyNever, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: []batch.PodFailurePolicyRule{ + { + Action: batch.PodFailurePolicyActionIgnore, + OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{ + { + Type: api.DisruptionTarget, + Status: "UnknownStatus", + }, }, }, }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, `spec.podFailurePolicy.rules[0].onPodConditions[0].type: Invalid value: "": name part must be non-empty`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGeneratedRestartPolicyNever, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: []batch.PodFailurePolicyRule{ - { - Action: batch.PodFailurePolicyActionIgnore, - OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{ - { - Status: api.ConditionTrue, + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGeneratedRestartPolicyNever, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: []batch.PodFailurePolicyRule{ + { + Action: batch.PodFailurePolicyActionIgnore, + OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{ + { + Status: api.ConditionTrue, + }, }, }, }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, `spec.podFailurePolicy.rules[0].onPodConditions[0].type: Invalid value: "Invalid Condition Type": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGeneratedRestartPolicyNever, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: []batch.PodFailurePolicyRule{ - { - Action: batch.PodFailurePolicyActionIgnore, - OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{ - { - Type: api.PodConditionType("Invalid Condition Type"), - Status: api.ConditionTrue, + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGeneratedRestartPolicyNever, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: []batch.PodFailurePolicyRule{ + { + Action: batch.PodFailurePolicyActionIgnore, + OnPodConditions: []batch.PodFailurePolicyOnPodConditionsPattern{ + { + Type: api.PodConditionType("Invalid Condition Type"), + Status: api.ConditionTrue, + }, }, }, }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, `spec.template.spec.restartPolicy: Invalid value: "OnFailure": only "Never" is supported when podFailurePolicy is specified`: { - ObjectMeta: validJobObjectMeta, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: api.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: validGeneratedSelector.MatchLabels, + job: batch.Job{ + ObjectMeta: validJobObjectMeta, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: validGeneratedSelector.MatchLabels, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyOnFailure, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + }, }, - Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicyOnFailure, - DNSPolicy: api.DNSClusterFirst, - Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + PodFailurePolicy: &batch.PodFailurePolicy{ + Rules: []batch.PodFailurePolicyRule{}, }, }, - PodFailurePolicy: &batch.PodFailurePolicy{ - Rules: []batch.PodFailurePolicyRule{}, - }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, "spec.parallelism:must be greater than or equal to 0": { - ObjectMeta: metav1.ObjectMeta{ - Name: "myjob", - Namespace: metav1.NamespaceDefault, - UID: types.UID("1a2b3c"), - }, - Spec: batch.JobSpec{ - Parallelism: &negative, - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGenerated, + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + Parallelism: &negative, + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGenerated, + }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, "spec.backoffLimit:must be greater than or equal to 0": { - ObjectMeta: metav1.ObjectMeta{ - Name: "myjob", - Namespace: metav1.NamespaceDefault, - UID: types.UID("1a2b3c"), - }, - Spec: batch.JobSpec{ - BackoffLimit: pointer.Int32(-1), - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGenerated, + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + BackoffLimit: pointer.Int32(-1), + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGenerated, + }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, - "spec.completions:must be greater than or equal to 0": { - ObjectMeta: metav1.ObjectMeta{ - Name: "myjob", - Namespace: metav1.NamespaceDefault, - UID: types.UID("1a2b3c"), - }, - Spec: batch.JobSpec{ - Completions: &negative, - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGenerated, + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + Completions: &negative, + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGenerated, + }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, "spec.activeDeadlineSeconds:must be greater than or equal to 0": { - ObjectMeta: metav1.ObjectMeta{ - Name: "myjob", - Namespace: metav1.NamespaceDefault, - UID: types.UID("1a2b3c"), - }, - Spec: batch.JobSpec{ - ActiveDeadlineSeconds: &negative64, - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGenerated, + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + ActiveDeadlineSeconds: &negative64, + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGenerated, + }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, "spec.selector:Required value": { - ObjectMeta: metav1.ObjectMeta{ - Name: "myjob", - Namespace: metav1.NamespaceDefault, - UID: types.UID("1a2b3c"), - }, - Spec: batch.JobSpec{ - Template: validPodTemplateSpecForGenerated, + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + Template: validPodTemplateSpecForGenerated, + }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, "spec.template.metadata.labels: Invalid value: map[string]string{\"y\":\"z\"}: `selector` does not match template `labels`": { - ObjectMeta: metav1.ObjectMeta{ - Name: "myjob", - Namespace: metav1.NamespaceDefault, - UID: types.UID("1a2b3c"), - }, - Spec: batch.JobSpec{ - Selector: validManualSelector, - ManualSelector: pointer.Bool(true), - Template: api.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{"y": "z"}, - }, - Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicyOnFailure, - DNSPolicy: api.DNSClusterFirst, - Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + Selector: validManualSelector, + ManualSelector: pointer.BoolPtr(true), + Template: api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"y": "z"}, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyOnFailure, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, "spec.template.metadata.labels: Invalid value: map[string]string{\"controller-uid\":\"4d5e6f\"}: `selector` does not match template `labels`": { - ObjectMeta: metav1.ObjectMeta{ - Name: "myjob", - Namespace: metav1.NamespaceDefault, - UID: types.UID("1a2b3c"), - }, - Spec: batch.JobSpec{ - Selector: validManualSelector, - ManualSelector: pointer.Bool(true), - Template: api.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{"controller-uid": "4d5e6f"}, - }, - Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicyOnFailure, - DNSPolicy: api.DNSClusterFirst, - Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + Selector: validManualSelector, + ManualSelector: pointer.BoolPtr(true), + Template: api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"controller-uid": "4d5e6f"}, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyOnFailure, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, "spec.template.spec.restartPolicy: Required value": { - ObjectMeta: metav1.ObjectMeta{ - Name: "myjob", - Namespace: metav1.NamespaceDefault, - UID: types.UID("1a2b3c"), - }, - Spec: batch.JobSpec{ - Selector: validManualSelector, - ManualSelector: pointer.Bool(true), - Template: api.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: validManualSelector.MatchLabels, - }, - Spec: api.PodSpec{ - RestartPolicy: api.RestartPolicyAlways, - DNSPolicy: api.DNSClusterFirst, - Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + Selector: validManualSelector, + ManualSelector: pointer.BoolPtr(true), + Template: api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: validManualSelector.MatchLabels, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, "spec.template.spec.restartPolicy: Unsupported value": { - ObjectMeta: metav1.ObjectMeta{ - Name: "myjob", - Namespace: metav1.NamespaceDefault, - UID: types.UID("1a2b3c"), - }, - Spec: batch.JobSpec{ - Selector: validManualSelector, - ManualSelector: pointer.Bool(true), - Template: api.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: validManualSelector.MatchLabels, - }, - Spec: api.PodSpec{ - RestartPolicy: "Invalid", - DNSPolicy: api.DNSClusterFirst, - Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + Selector: validManualSelector, + ManualSelector: pointer.BoolPtr(true), + Template: api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: validManualSelector.MatchLabels, + }, + Spec: api.PodSpec{ + RestartPolicy: "Invalid", + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + }, }, }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, "spec.ttlSecondsAfterFinished: must be greater than or equal to 0": { - ObjectMeta: metav1.ObjectMeta{ - Name: "myjob", - Namespace: metav1.NamespaceDefault, - UID: types.UID("1a2b3c"), - }, - Spec: batch.JobSpec{ - TTLSecondsAfterFinished: &negative, - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGenerated, - }, - }, - "spec.completions: Required value: when completion mode is Indexed": { - ObjectMeta: metav1.ObjectMeta{ - Name: "myjob", - Namespace: metav1.NamespaceDefault, - UID: types.UID("1a2b3c"), - }, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGenerated, - CompletionMode: completionModePtr(batch.IndexedCompletion), - }, - }, - "spec.parallelism: must be less than or equal to 100000 when completion mode is Indexed": { - ObjectMeta: metav1.ObjectMeta{ - Name: "myjob", - Namespace: metav1.NamespaceDefault, - UID: types.UID("1a2b3c"), - }, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGenerated, - CompletionMode: completionModePtr(batch.IndexedCompletion), - Completions: pointer.Int32(2), - Parallelism: pointer.Int32(100001), - }, - }, - "metadata.annotations[batch.kubernetes.io/job-tracking]: cannot add this annotation": { - ObjectMeta: metav1.ObjectMeta{ - Name: "myjob", - Namespace: metav1.NamespaceDefault, - UID: types.UID("1a2b3c"), - Annotations: map[string]string{ - batch.JobTrackingFinalizer: "", + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + TTLSecondsAfterFinished: &negative, + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGenerated, }, }, - Spec: batch.JobSpec{ - Selector: validGeneratedSelector, - Template: validPodTemplateSpecForGenerated, + opts: JobValidationOptions{RequirePrefixedLabels: true}, + }, + "spec.completions: Required value: when completion mode is Indexed": { + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGenerated, + CompletionMode: completionModePtr(batch.IndexedCompletion), + }, }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, + }, + "spec.parallelism: must be less than or equal to 100000 when completion mode is Indexed": { + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGenerated, + CompletionMode: completionModePtr(batch.IndexedCompletion), + Completions: pointer.Int32Ptr(2), + Parallelism: pointer.Int32Ptr(100001), + }, + }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, + }, + "metadata.annotations[batch.kubernetes.io/job-tracking]: cannot add this annotation": { + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + Annotations: map[string]string{ + batch.JobTrackingFinalizer: "", + }, + }, + Spec: batch.JobSpec{ + Selector: validGeneratedSelector, + Template: validPodTemplateSpecForGenerated, + }, + }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, + }, + "spec.template.metadata.labels[controller-uid]: Required value: must be '1a2b3c'": { + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{batch.LegacyControllerUidLabel: "1a2b3c"}, + }, + Template: api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{batch.LegacyJobNameLabel: "myjob"}, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyOnFailure, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + InitContainers: []api.Container{{Name: "def", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + }, + }, + }, + }, + opts: JobValidationOptions{}, + }, + "metadata.uid: Required value": { + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + }, + Spec: batch.JobSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{batch.LegacyControllerUidLabel: "test"}, + }, + Template: api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{batch.LegacyJobNameLabel: "myjob"}, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyOnFailure, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + InitContainers: []api.Container{{Name: "def", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + }, + }, + }, + }, + opts: JobValidationOptions{}, + }, + "spec.selector: Invalid value: v1.LabelSelector{MatchLabels:map[string]string{\"a\":\"b\"}, MatchExpressions:[]v1.LabelSelectorRequirement(nil)}: `selector` not auto-generated": { + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"a": "b"}, + }, + Template: validPodTemplateSpecForGenerated, + }, + }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, + }, + "spec.template.metadata.labels[batch.kubernetes.io/controller-uid]: Required value: must be '1a2b3c'": { + job: batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + Namespace: metav1.NamespaceDefault, + UID: types.UID("1a2b3c"), + }, + Spec: batch.JobSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{batch.ControllerUidLabel: "1a2b3c"}, + }, + Template: api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{batch.JobNameLabel: "myjob", batch.LegacyControllerUidLabel: "1a2b3c", batch.LegacyJobNameLabel: "myjob"}, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyOnFailure, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + InitContainers: []api.Container{{Name: "def", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + }, + }, + }, + }, + opts: JobValidationOptions{RequirePrefixedLabels: true}, }, } for k, v := range errorCases { t.Run(k, func(t *testing.T) { - errs := ValidateJob(&v, JobValidationOptions{}) + errs := ValidateJob(&v.job, v.opts) if len(errs) == 0 { t.Errorf("expected failure for %s", k) } else { diff --git a/pkg/registry/batch/job/strategy.go b/pkg/registry/batch/job/strategy.go index 3b2d102a0b4..6d12407d51c 100644 --- a/pkg/registry/batch/job/strategy.go +++ b/pkg/registry/batch/job/strategy.go @@ -170,6 +170,7 @@ func validationOptionsForJob(newJob, oldJob *batch.Job) batchvalidation.JobValid PodValidationOptions: pod.GetValidationOptionsFromPodTemplate(newPodTemplate, oldPodTemplate), AllowTrackingAnnotation: true, AllowElasticIndexedJobs: utilfeature.DefaultFeatureGate.Enabled(features.ElasticIndexedJob), + RequirePrefixedLabels: true, } if oldJob != nil { opts.AllowInvalidLabelValueInSelector = opts.AllowInvalidLabelValueInSelector || metav1validation.LabelSelectorHasInvalidLabelValue(oldJob.Spec.Selector) @@ -184,6 +185,12 @@ func validationOptionsForJob(newJob, oldJob *batch.Job) batchvalidation.JobValid suspended := oldJob.Spec.Suspend != nil && *oldJob.Spec.Suspend notStarted := oldJob.Status.StartTime == nil opts.AllowMutableSchedulingDirectives = suspended && notStarted + + // Validation should not fail jobs if they don't have the new labels. + // This can be removed once we have high confidence that both labels exist (1.30 at least) + _, hadJobName := oldJob.Spec.Template.Labels[batch.JobNameLabel] + _, hadControllerUid := oldJob.Spec.Template.Labels[batch.ControllerUidLabel] + opts.RequirePrefixedLabels = hadJobName && hadControllerUid } return opts } @@ -209,21 +216,28 @@ func generateSelector(obj *batch.Job) { // The job-name label is unique except in cases that are expected to be // quite uncommon, and is more user friendly than uid. So, we add it as // a label. - _, found := obj.Spec.Template.Labels["job-name"] - if found { - // User asked us to automatically generate a selector, but set manual labels. - // If there is a conflict, we will reject in validation. - } else { - obj.Spec.Template.Labels["job-name"] = string(obj.ObjectMeta.Name) + jobNameLabels := []string{batch.LegacyJobNameLabel, batch.JobNameLabel} + for _, value := range jobNameLabels { + _, found := obj.Spec.Template.Labels[value] + if found { + // User asked us to automatically generate a selector, but set manual labels. + // If there is a conflict, we will reject in validation. + } else { + obj.Spec.Template.Labels[value] = string(obj.ObjectMeta.Name) + } } + // The controller-uid label makes the pods that belong to this job // only match this job. - _, found = obj.Spec.Template.Labels["controller-uid"] - if found { - // User asked us to automatically generate a selector, but set manual labels. - // If there is a conflict, we will reject in validation. - } else { - obj.Spec.Template.Labels["controller-uid"] = string(obj.ObjectMeta.UID) + controllerUidLabels := []string{batch.LegacyControllerUidLabel, batch.ControllerUidLabel} + for _, value := range controllerUidLabels { + _, found := obj.Spec.Template.Labels[value] + if found { + // User asked us to automatically generate a selector, but set manual labels. + // If there is a conflict, we will reject in validation. + } else { + obj.Spec.Template.Labels[value] = string(obj.ObjectMeta.UID) + } } // Select the controller-uid label. This is sufficient for uniqueness. if obj.Spec.Selector == nil { @@ -232,8 +246,9 @@ func generateSelector(obj *batch.Job) { if obj.Spec.Selector.MatchLabels == nil { obj.Spec.Selector.MatchLabels = make(map[string]string) } - if _, found := obj.Spec.Selector.MatchLabels["controller-uid"]; !found { - obj.Spec.Selector.MatchLabels["controller-uid"] = string(obj.ObjectMeta.UID) + + if _, found := obj.Spec.Selector.MatchLabels[batch.ControllerUidLabel]; !found { + obj.Spec.Selector.MatchLabels[batch.ControllerUidLabel] = string(obj.ObjectMeta.UID) } // If the user specified matchLabel controller-uid=$WRONGUID, then it should fail // in validation, either because the selector does not match the pod template diff --git a/pkg/registry/batch/job/strategy_test.go b/pkg/registry/batch/job/strategy_test.go index d10816d0301..1278ecd6d08 100644 --- a/pkg/registry/batch/job/strategy_test.go +++ b/pkg/registry/batch/job/strategy_test.go @@ -670,6 +670,66 @@ func TestJobStrategy_ValidateUpdate(t *testing.T) { job.Annotations["hello"] = "world" }, }, + "old job has no batch.kubernetes.io labels": { + job: &batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + UID: "test", + Namespace: metav1.NamespaceDefault, + ResourceVersion: "10", + Annotations: map[string]string{"hello": "world"}, + }, + Spec: batch.JobSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{batch.LegacyControllerUidLabel: "test"}, + }, + Parallelism: pointer.Int32(4), + Template: api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{batch.LegacyJobNameLabel: "myjob", batch.LegacyControllerUidLabel: "test"}, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyOnFailure, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + }, + }, + }, + }, + update: func(job *batch.Job) { + job.Annotations["hello"] = "world" + }, + }, + "old job has all labels": { + job: &batch.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myjob", + UID: "test", + Namespace: metav1.NamespaceDefault, + ResourceVersion: "10", + Annotations: map[string]string{"foo": "bar"}, + }, + Spec: batch.JobSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{batch.ControllerUidLabel: "test"}, + }, + Parallelism: pointer.Int32(4), + Template: api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{batch.LegacyJobNameLabel: "myjob", batch.JobNameLabel: "myjob", batch.LegacyControllerUidLabel: "test", batch.ControllerUidLabel: "test"}, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyOnFailure, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, + }, + }, + }, + }, + update: func(job *batch.Job) { + job.Annotations["hello"] = "world" + }, + }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { @@ -872,7 +932,8 @@ func TestJobStrategy_Validate(t *testing.T) { validSelector := &metav1.LabelSelector{ MatchLabels: map[string]string{"a": "b"}, } - + validLabels := map[string]string{batch.LegacyJobNameLabel: "myjob2", batch.JobNameLabel: "myjob2", batch.LegacyControllerUidLabel: string(theUID), batch.ControllerUidLabel: string(theUID)} + labelsWithNonBatch := map[string]string{"a": "b", batch.LegacyJobNameLabel: "myjob2", batch.JobNameLabel: "myjob2", batch.LegacyControllerUidLabel: string(theUID), batch.ControllerUidLabel: string(theUID)} validPodSpec := api.PodSpec{ RestartPolicy: api.RestartPolicyOnFailure, DNSPolicy: api.DNSClusterFirst, @@ -903,7 +964,7 @@ func TestJobStrategy_Validate(t *testing.T) { wantJob: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ - Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"controller-uid": string(theUID)}}, + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{batch.ControllerUidLabel: string(theUID)}}, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector.MatchLabels, @@ -924,10 +985,10 @@ func TestJobStrategy_Validate(t *testing.T) { wantJob: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ - Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"controller-uid": string(theUID)}}, + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{batch.ControllerUidLabel: string(theUID)}}, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{"job-name": "myjob2", "controller-uid": string(theUID)}, + Labels: validLabels, }, Spec: validPodSpec, }}, @@ -940,7 +1001,7 @@ func TestJobStrategy_Validate(t *testing.T) { Selector: nil, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{"a": "b", "job-name": "myjob2", "controller-uid": string(theUID)}, + Labels: labelsWithNonBatch, }, Spec: validPodSpec, }}, @@ -948,10 +1009,10 @@ func TestJobStrategy_Validate(t *testing.T) { wantJob: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ - Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"controller-uid": string(theUID)}}, + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{batch.ControllerUidLabel: string(theUID)}}, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{"a": "b", "job-name": "myjob2", "controller-uid": string(theUID)}, + Labels: labelsWithNonBatch, }, Spec: validPodSpec, }}, @@ -1007,10 +1068,10 @@ func TestJobStrategy_Validate(t *testing.T) { wantJob: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ - Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"controller-uid": string(theUID)}}, + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{batch.ControllerUidLabel: string(theUID)}}, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{"a": "b", "job-name": "myjob2", "controller-uid": string(theUID)}, + Labels: labelsWithNonBatch, }, Spec: validPodSpec, }, @@ -1042,10 +1103,10 @@ func TestJobStrategy_Validate(t *testing.T) { wantJob: &batch.Job{ ObjectMeta: validObjectMeta, Spec: batch.JobSpec{ - Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"controller-uid": string(theUID)}}, + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{batch.ControllerUidLabel: string(theUID)}}, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{"a": "b", "job-name": "myjob2", "controller-uid": string(theUID)}, + Labels: labelsWithNonBatch, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyOnFailure, diff --git a/staging/src/k8s.io/api/batch/v1/types.go b/staging/src/k8s.io/api/batch/v1/types.go index 54b450aeb56..346676b0951 100644 --- a/staging/src/k8s.io/api/batch/v1/types.go +++ b/staging/src/k8s.io/api/batch/v1/types.go @@ -23,8 +23,11 @@ import ( ) const ( - JobCompletionIndexAnnotation = "batch.kubernetes.io/job-completion-index" + // All Kubernetes labels need to be prefixed with Kubernetes to distinguish them from end-user labels + // More info: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#label-selector-and-annotation-conventions + labelPrefix = "batch.kubernetes.io/" + JobCompletionIndexAnnotation = labelPrefix + "job-completion-index" // JobTrackingFinalizer is a finalizer for Job's pods. It prevents them from // being deleted before being accounted in the Job status. // @@ -34,7 +37,14 @@ const ( // 1.27+, one release after JobTrackingWithFinalizers graduates to GA, the // apiserver and job controller will ignore this annotation and they will // always track jobs using finalizers. - JobTrackingFinalizer = "batch.kubernetes.io/job-tracking" + JobTrackingFinalizer = labelPrefix + "job-tracking" + // The Job labels will use batch.kubernetes.io as a prefix for all labels + // Historically the job controller uses unprefixed labels for job-name and controller-uid and + // Kubernetes continutes to recognize those unprefixed labels for consistency. + JobNameLabel = labelPrefix + "job-name" + // ControllerUid is used to programatically get pods corresponding to a Job. + // There is a corresponding label without the batch.kubernetes.io that we support for legacy reasons. + ControllerUidLabel = labelPrefix + "controller-uid" ) // +genclient