Support for the Job managedBy field (alpha) (#123273)

* support for the managed-by label in Job

* Use managedBy field instead of managed-by label

* Additional review remarks

* Review remarks 2

* review remarks 3

* Skip cleanup of finalizers for job with custom managedBy

* Drop the performance optimization

* imrpove logs
This commit is contained in:
Michał Woźniak
2024-03-05 18:25:15 +01:00
committed by GitHub
parent a81411594a
commit e568a77a93
32 changed files with 2445 additions and 177 deletions

View File

@@ -17,6 +17,7 @@ limitations under the License.
package validation
import (
"errors"
_ "time/tzdata"
"fmt"
@@ -33,6 +34,7 @@ import (
api "k8s.io/kubernetes/pkg/apis/core"
corevalidation "k8s.io/kubernetes/pkg/apis/core/validation"
"k8s.io/utils/pointer"
"k8s.io/utils/ptr"
)
var (
@@ -380,6 +382,17 @@ func TestValidateJob(t *testing.T) {
},
},
},
"valid managedBy field": {
opts: JobValidationOptions{RequirePrefixedLabels: true},
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
Template: validPodTemplateSpecForGenerated,
ManagedBy: ptr.To("example.com/foo"),
},
},
},
}
for k, v := range successCases {
t.Run(k, func(t *testing.T) {
@@ -394,6 +407,28 @@ func TestValidateJob(t *testing.T) {
opts JobValidationOptions
job batch.Job
}{
`spec.managedBy: Too long: may not be longer than 63`: {
opts: JobValidationOptions{RequirePrefixedLabels: true},
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
Template: validPodTemplateSpecForGenerated,
ManagedBy: ptr.To("example.com/" + strings.Repeat("x", 60)),
},
},
},
`spec.managedBy: Invalid value: "invalid custom controller name": must be a domain-prefixed path (such as "acme.io/foo")`: {
opts: JobValidationOptions{RequirePrefixedLabels: true},
job: batch.Job{
ObjectMeta: validJobObjectMeta,
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
Template: validPodTemplateSpecForGenerated,
ManagedBy: ptr.To("invalid custom controller name"),
},
},
},
`spec.podFailurePolicy.rules[0]: Invalid value: specifying one of OnExitCodes and OnPodConditions is required`: {
job: batch.Job{
ObjectMeta: validJobObjectMeta,
@@ -1349,6 +1384,39 @@ func TestValidateJobUpdate(t *testing.T) {
job.Spec.ManualSelector = pointer.Bool(true)
},
},
"invalid attempt to set managedBy field": {
old: batch.Job{
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
Template: validPodTemplateSpecForGenerated,
},
},
update: func(job *batch.Job) {
job.Spec.ManagedBy = ptr.To("example.com/custom-controller")
},
err: &field.Error{
Type: field.ErrorTypeInvalid,
Field: "spec.managedBy",
},
},
"invalid update of the managedBy field": {
old: batch.Job{
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
Spec: batch.JobSpec{
Selector: validGeneratedSelector,
Template: validPodTemplateSpecForGenerated,
ManagedBy: ptr.To("example.com/custom-controller1"),
},
},
update: func(job *batch.Job) {
job.Spec.ManagedBy = ptr.To("example.com/custom-controller2")
},
err: &field.Error{
Type: field.ErrorTypeInvalid,
Field: "spec.managedBy",
},
},
"immutable completions for non-indexed jobs": {
old: batch.Job{
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
@@ -2014,6 +2082,8 @@ func TestValidateJobUpdate(t *testing.T) {
func TestValidateJobUpdateStatus(t *testing.T) {
cases := map[string]struct {
opts JobStatusValidationOptions
old batch.Job
update batch.Job
wantErrs field.ErrorList
@@ -2141,7 +2211,7 @@ func TestValidateJobUpdateStatus(t *testing.T) {
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
errs := ValidateJobUpdateStatus(&tc.update, &tc.old)
errs := ValidateJobUpdateStatus(&tc.update, &tc.old, tc.opts)
if diff := cmp.Diff(tc.wantErrs, errs, ignoreErrValueDetail); diff != "" {
t.Errorf("Unexpected errors (-want,+got):\n%s", diff)
}
@@ -3587,3 +3657,161 @@ func TestTimeZones(t *testing.T) {
}
}
}
func TestValidateIndexesString(t *testing.T) {
testCases := map[string]struct {
indexesString string
completions int32
wantError error
}{
"empty is valid": {
indexesString: "",
completions: 6,
},
"single number is valid": {
indexesString: "1",
completions: 6,
},
"single interval is valid": {
indexesString: "1-3",
completions: 6,
},
"mixed intervals valid": {
indexesString: "0,1-3,5,7-10",
completions: 12,
},
"invalid due to extra space": {
indexesString: "0,1-3, 5",
completions: 6,
wantError: errors.New(`cannot convert string to integer for index: " 5"`),
},
"invalid due to too large index": {
indexesString: "0,1-3,5",
completions: 5,
wantError: errors.New(`too large index: "5"`),
},
"invalid due to non-increasing order of intervals": {
indexesString: "1-3,0,5",
completions: 6,
wantError: errors.New(`non-increasing order, previous: 3, current: 0`),
},
"invalid due to non-increasing order between intervals": {
indexesString: "0,0,5",
completions: 6,
wantError: errors.New(`non-increasing order, previous: 0, current: 0`),
},
"invalid due to non-increasing order within interval": {
indexesString: "0,1-1,5",
completions: 6,
wantError: errors.New(`non-increasing order, previous: 1, current: 1`),
},
"invalid due to starting with '-'": {
indexesString: "-1,0",
completions: 6,
wantError: errors.New(`cannot convert string to integer for index: ""`),
},
"invalid due to ending with '-'": {
indexesString: "0,1-",
completions: 6,
wantError: errors.New(`cannot convert string to integer for index: ""`),
},
"invalid due to repeated '-'": {
indexesString: "0,1--3",
completions: 6,
wantError: errors.New(`the fragment "1--3" violates the requirement that an index interval can have at most two parts separated by '-'`),
},
"invalid due to repeated ','": {
indexesString: "0,,1,3",
completions: 6,
wantError: errors.New(`cannot convert string to integer for index: ""`),
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
gotErr := validateIndexesFormat(tc.indexesString, tc.completions)
if tc.wantError == nil && gotErr != nil {
t.Errorf("unexpected error: %s", gotErr)
} else if tc.wantError != nil && gotErr == nil {
t.Errorf("missing error: %s", tc.wantError)
} else if tc.wantError != nil && gotErr != nil {
if diff := cmp.Diff(tc.wantError.Error(), gotErr.Error()); diff != "" {
t.Errorf("unexpected error, diff: %s", diff)
}
}
})
}
}
func TestValidateFailedIndexesNotOverlapCompleted(t *testing.T) {
testCases := map[string]struct {
completedIndexesStr string
failedIndexesStr string
completions int32
wantError error
}{
"empty intervals": {
completedIndexesStr: "",
failedIndexesStr: "",
completions: 6,
},
"empty completed intervals": {
completedIndexesStr: "",
failedIndexesStr: "1-3",
completions: 6,
},
"empty failed intervals": {
completedIndexesStr: "1-2",
failedIndexesStr: "",
completions: 6,
},
"non-overlapping intervals": {
completedIndexesStr: "0,2-4,6-8,12-19",
failedIndexesStr: "1,9-10",
completions: 20,
},
"overlapping intervals": {
completedIndexesStr: "0,2-4,6-8,12-19",
failedIndexesStr: "1,8,9-10",
completions: 20,
wantError: errors.New("failedIndexes and completedIndexes overlap at index: 8"),
},
"overlapping intervals, corrupted completed interval skipped": {
completedIndexesStr: "0,2-4,x,6-8,12-19",
failedIndexesStr: "1,8,9-10",
completions: 20,
wantError: errors.New("failedIndexes and completedIndexes overlap at index: 8"),
},
"overlapping intervals, corrupted failed interval skipped": {
completedIndexesStr: "0,2-4,6-8,12-19",
failedIndexesStr: "1,y,8,9-10",
completions: 20,
wantError: errors.New("failedIndexes and completedIndexes overlap at index: 8"),
},
"overlapping intervals, first corrupted intervals skipped": {
completedIndexesStr: "x,0,2-4,6-8,12-19",
failedIndexesStr: "y,1,8,9-10",
completions: 20,
wantError: errors.New("failedIndexes and completedIndexes overlap at index: 8"),
},
"non-overlapping intervals, last intervals corrupted": {
completedIndexesStr: "0,2-4,6-8,12-19,x",
failedIndexesStr: "1,9-10,y",
completions: 20,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
gotErr := validateFailedIndexesNotOverlapCompleted(tc.completedIndexesStr, tc.failedIndexesStr, tc.completions)
if tc.wantError == nil && gotErr != nil {
t.Errorf("unexpected error: %s", gotErr)
} else if tc.wantError != nil && gotErr == nil {
t.Errorf("missing error: %s", tc.wantError)
} else if tc.wantError != nil && gotErr != nil {
if diff := cmp.Diff(tc.wantError.Error(), gotErr.Error()); diff != "" {
t.Errorf("unexpected error, diff: %s", diff)
}
}
})
}
}