Merge pull request #114930 from kannon92/add-new-labels

Add batch.kubernetes.io to labels created in the Job controller.
This commit is contained in:
Kubernetes Prow Robot 2023-03-14 17:44:13 -07:00 committed by GitHub
commit f3aebc85b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 831 additions and 470 deletions

View File

@ -22,6 +22,11 @@ import (
api "k8s.io/kubernetes/pkg/apis/core"
)
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.
//
@ -31,7 +36,15 @@ import (
// 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"
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

View File

@ -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
}

View File

@ -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,8 +303,12 @@ 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`: {
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
@ -260,7 +322,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
`spec.podFailurePolicy.rules[0].onExitCodes.values[1]: Duplicate value: 11`: {
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
@ -278,7 +343,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
`spec.podFailurePolicy.rules[0].onExitCodes.values: Too many: 256: must have at most 255 items`: {
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
@ -302,7 +370,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
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,
@ -324,7 +395,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
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,
@ -348,7 +422,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
`spec.podFailurePolicy.rules[0].onExitCodes.values[2]: Duplicate value: 13`: {
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
@ -366,7 +443,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
`spec.podFailurePolicy.rules[0].onExitCodes.values: Invalid value: []int32{19, 11}: must be ordered`: {
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
@ -384,7 +464,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
`spec.podFailurePolicy.rules[0].onExitCodes.values: Invalid value: []int32{}: at least one value is required`: {
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
@ -402,7 +485,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
`spec.podFailurePolicy.rules[0].action: Required value: valid values: ["Count" "FailJob" "Ignore"]`: {
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
@ -420,7 +506,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
`spec.podFailurePolicy.rules[0].onExitCodes.operator: Required value: valid values: ["In" "NotIn"]`: {
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
@ -438,7 +527,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
`spec.podFailurePolicy.rules[0]: Invalid value: specifying both OnExitCodes and OnPodConditions is not supported`: {
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
@ -463,7 +555,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
`spec.podFailurePolicy.rules[0].onExitCodes.values[1]: Invalid value: 0: must not be 0 for the In operator`: {
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
@ -481,7 +576,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
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`: {
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
@ -508,7 +606,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
`spec.podFailurePolicy.rules[0].action: Unsupported value: "UnknownAction": supported values: "Count", "FailJob", "Ignore"`: {
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
@ -527,7 +628,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
`spec.podFailurePolicy.rules[0].onExitCodes.operator: Unsupported value: "UnknownOperator": supported values: "In", "NotIn"`: {
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
@ -545,7 +649,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
`spec.podFailurePolicy.rules[0].onPodConditions[0].status: Required value: valid values: ["False" "True" "Unknown"]`: {
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
@ -564,7 +671,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
`spec.podFailurePolicy.rules[0].onPodConditions[0].status: Unsupported value: "UnknownStatus": supported values: "False", "True", "Unknown"`: {
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
@ -584,7 +694,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
`spec.podFailurePolicy.rules[0].onPodConditions[0].type: Invalid value: "": name part must be non-empty`: {
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
@ -603,7 +716,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
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]')`: {
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
@ -623,7 +739,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
`spec.template.spec.restartPolicy: Invalid value: "OnFailure": only "Never" is supported when podFailurePolicy is specified`: {
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
@ -642,7 +761,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
"spec.parallelism:must be greater than or equal to 0": {
job: batch.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
@ -654,7 +776,10 @@ func TestValidateJob(t *testing.T) {
Template: validPodTemplateSpecForGenerated,
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
"spec.backoffLimit:must be greater than or equal to 0": {
job: batch.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
@ -666,8 +791,10 @@ func TestValidateJob(t *testing.T) {
Template: validPodTemplateSpecForGenerated,
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
"spec.completions:must be greater than or equal to 0": {
job: batch.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
@ -679,7 +806,10 @@ func TestValidateJob(t *testing.T) {
Template: validPodTemplateSpecForGenerated,
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
"spec.activeDeadlineSeconds:must be greater than or equal to 0": {
job: batch.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
@ -691,7 +821,10 @@ func TestValidateJob(t *testing.T) {
Template: validPodTemplateSpecForGenerated,
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
"spec.selector:Required value": {
job: batch.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
@ -701,7 +834,10 @@ func TestValidateJob(t *testing.T) {
Template: validPodTemplateSpecForGenerated,
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
"spec.template.metadata.labels: Invalid value: map[string]string{\"y\":\"z\"}: `selector` does not match template `labels`": {
job: batch.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
@ -709,7 +845,7 @@ func TestValidateJob(t *testing.T) {
},
Spec: batch.JobSpec{
Selector: validManualSelector,
ManualSelector: pointer.Bool(true),
ManualSelector: pointer.BoolPtr(true),
Template: api.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"y": "z"},
@ -722,7 +858,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
"spec.template.metadata.labels: Invalid value: map[string]string{\"controller-uid\":\"4d5e6f\"}: `selector` does not match template `labels`": {
job: batch.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
@ -730,7 +869,7 @@ func TestValidateJob(t *testing.T) {
},
Spec: batch.JobSpec{
Selector: validManualSelector,
ManualSelector: pointer.Bool(true),
ManualSelector: pointer.BoolPtr(true),
Template: api.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"controller-uid": "4d5e6f"},
@ -743,7 +882,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
"spec.template.spec.restartPolicy: Required value": {
job: batch.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
@ -751,7 +893,7 @@ func TestValidateJob(t *testing.T) {
},
Spec: batch.JobSpec{
Selector: validManualSelector,
ManualSelector: pointer.Bool(true),
ManualSelector: pointer.BoolPtr(true),
Template: api.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: validManualSelector.MatchLabels,
@ -764,7 +906,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
"spec.template.spec.restartPolicy: Unsupported value": {
job: batch.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
@ -772,7 +917,7 @@ func TestValidateJob(t *testing.T) {
},
Spec: batch.JobSpec{
Selector: validManualSelector,
ManualSelector: pointer.Bool(true),
ManualSelector: pointer.BoolPtr(true),
Template: api.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: validManualSelector.MatchLabels,
@ -785,7 +930,10 @@ func TestValidateJob(t *testing.T) {
},
},
},
opts: JobValidationOptions{RequirePrefixedLabels: true},
},
"spec.ttlSecondsAfterFinished: must be greater than or equal to 0": {
job: batch.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "myjob",
Namespace: metav1.NamespaceDefault,
@ -797,7 +945,10 @@ func TestValidateJob(t *testing.T) {
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,
@ -809,7 +960,10 @@ func TestValidateJob(t *testing.T) {
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,
@ -819,11 +973,14 @@ func TestValidateJob(t *testing.T) {
Selector: validGeneratedSelector,
Template: validPodTemplateSpecForGenerated,
CompletionMode: completionModePtr(batch.IndexedCompletion),
Completions: pointer.Int32(2),
Parallelism: pointer.Int32(100001),
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,
@ -837,11 +994,106 @@ func TestValidateJob(t *testing.T) {
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 {

View File

@ -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"]
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["job-name"] = string(obj.ObjectMeta.Name)
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"]
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["controller-uid"] = string(obj.ObjectMeta.UID)
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

View File

@ -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,

View File

@ -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