...

Source file src/k8s.io/kubernetes/pkg/registry/batch/cronjob/strategy_test.go

Documentation: k8s.io/kubernetes/pkg/registry/batch/cronjob

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cronjob
    18  
    19  import (
    20  	"testing"
    21  
    22  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    23  	genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
    24  	"k8s.io/apiserver/pkg/registry/rest"
    25  	"k8s.io/kubernetes/pkg/apis/batch"
    26  	api "k8s.io/kubernetes/pkg/apis/core"
    27  	"k8s.io/utils/pointer"
    28  )
    29  
    30  var (
    31  	validPodTemplateSpec = api.PodTemplateSpec{
    32  		Spec: api.PodSpec{
    33  			RestartPolicy: api.RestartPolicyOnFailure,
    34  			DNSPolicy:     api.DNSClusterFirst,
    35  			Containers:    []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}},
    36  		},
    37  	}
    38  	validCronjobSpec = batch.CronJobSpec{
    39  		Schedule:          "5 5 * * ?",
    40  		ConcurrencyPolicy: batch.AllowConcurrent,
    41  		TimeZone:          pointer.String("Asia/Shanghai"),
    42  		JobTemplate: batch.JobTemplateSpec{
    43  			Spec: batch.JobSpec{
    44  				Template:       validPodTemplateSpec,
    45  				CompletionMode: completionModePtr(batch.IndexedCompletion),
    46  				Completions:    pointer.Int32(10),
    47  				Parallelism:    pointer.Int32(10),
    48  			},
    49  		},
    50  	}
    51  	cronjobSpecWithTZinSchedule = batch.CronJobSpec{
    52  		Schedule:          "CRON_TZ=UTC 5 5 * * ?",
    53  		ConcurrencyPolicy: batch.AllowConcurrent,
    54  		TimeZone:          pointer.String("Asia/DoesNotExist"),
    55  		JobTemplate: batch.JobTemplateSpec{
    56  			Spec: batch.JobSpec{
    57  				Template: validPodTemplateSpec,
    58  			},
    59  		},
    60  	}
    61  )
    62  
    63  func completionModePtr(m batch.CompletionMode) *batch.CompletionMode {
    64  	return &m
    65  }
    66  
    67  func TestCronJobStrategy(t *testing.T) {
    68  	ctx := genericapirequest.NewDefaultContext()
    69  	if !Strategy.NamespaceScoped() {
    70  		t.Errorf("CronJob must be namespace scoped")
    71  	}
    72  	if Strategy.AllowCreateOnUpdate() {
    73  		t.Errorf("CronJob should not allow create on update")
    74  	}
    75  
    76  	cronJob := &batch.CronJob{
    77  		ObjectMeta: metav1.ObjectMeta{
    78  			Name:       "mycronjob",
    79  			Namespace:  metav1.NamespaceDefault,
    80  			Generation: 999,
    81  		},
    82  		Spec: batch.CronJobSpec{
    83  			Schedule:          "* * * * ?",
    84  			ConcurrencyPolicy: batch.AllowConcurrent,
    85  			JobTemplate: batch.JobTemplateSpec{
    86  				Spec: batch.JobSpec{
    87  					Template: validPodTemplateSpec,
    88  				},
    89  			},
    90  		},
    91  	}
    92  
    93  	Strategy.PrepareForCreate(ctx, cronJob)
    94  	if len(cronJob.Status.Active) != 0 {
    95  		t.Errorf("CronJob does not allow setting status on create")
    96  	}
    97  	if cronJob.Generation != 1 {
    98  		t.Errorf("expected Generation=1, got %d", cronJob.Generation)
    99  	}
   100  	errs := Strategy.Validate(ctx, cronJob)
   101  	if len(errs) != 0 {
   102  		t.Errorf("Unexpected error validating %v", errs)
   103  	}
   104  	now := metav1.Now()
   105  
   106  	// ensure we do not change generation for non-spec updates
   107  	updatedLabelCronJob := cronJob.DeepCopy()
   108  	updatedLabelCronJob.Labels = map[string]string{"a": "true"}
   109  	Strategy.PrepareForUpdate(ctx, updatedLabelCronJob, cronJob)
   110  	if updatedLabelCronJob.Generation != 1 {
   111  		t.Errorf("expected Generation=1, got %d", updatedLabelCronJob.Generation)
   112  	}
   113  
   114  	updatedCronJob := &batch.CronJob{
   115  		ObjectMeta: metav1.ObjectMeta{Name: "bar", ResourceVersion: "4"},
   116  		Spec: batch.CronJobSpec{
   117  			Schedule: "5 5 5 * ?",
   118  		},
   119  		Status: batch.CronJobStatus{
   120  			LastScheduleTime: &now,
   121  		},
   122  	}
   123  
   124  	// ensure we do not change status
   125  	Strategy.PrepareForUpdate(ctx, updatedCronJob, cronJob)
   126  	if updatedCronJob.Status.Active != nil {
   127  		t.Errorf("PrepareForUpdate should have preserved prior version status")
   128  	}
   129  	if updatedCronJob.Generation != 2 {
   130  		t.Errorf("expected Generation=2, got %d", updatedCronJob.Generation)
   131  	}
   132  	errs = Strategy.ValidateUpdate(ctx, updatedCronJob, cronJob)
   133  	if len(errs) == 0 {
   134  		t.Errorf("Expected a validation error")
   135  	}
   136  
   137  	// Make sure we correctly implement the interface.
   138  	// Otherwise a typo could silently change the default.
   139  	var gcds rest.GarbageCollectionDeleteStrategy = Strategy
   140  	if got, want := gcds.DefaultGarbageCollectionPolicy(genericapirequest.NewContext()), rest.DeleteDependents; got != want {
   141  		t.Errorf("DefaultGarbageCollectionPolicy() = %#v, want %#v", got, want)
   142  	}
   143  
   144  	var (
   145  		v1beta1Ctx      = genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{APIGroup: "batch", APIVersion: "v1beta1", Resource: "cronjobs"})
   146  		otherVersionCtx = genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{APIGroup: "batch", APIVersion: "v100", Resource: "cronjobs"})
   147  	)
   148  	if got, want := gcds.DefaultGarbageCollectionPolicy(v1beta1Ctx), rest.OrphanDependents; got != want {
   149  		t.Errorf("DefaultGarbageCollectionPolicy() = %#v, want %#v", got, want)
   150  	}
   151  	if got, want := gcds.DefaultGarbageCollectionPolicy(otherVersionCtx), rest.DeleteDependents; got != want {
   152  		t.Errorf("DefaultGarbageCollectionPolicy() = %#v, want %#v", got, want)
   153  	}
   154  }
   155  
   156  func TestCronJobStatusStrategy(t *testing.T) {
   157  	ctx := genericapirequest.NewDefaultContext()
   158  	if !StatusStrategy.NamespaceScoped() {
   159  		t.Errorf("CronJob must be namespace scoped")
   160  	}
   161  	if StatusStrategy.AllowCreateOnUpdate() {
   162  		t.Errorf("CronJob should not allow create on update")
   163  	}
   164  	validPodTemplateSpec := api.PodTemplateSpec{
   165  		Spec: api.PodSpec{
   166  			RestartPolicy: api.RestartPolicyOnFailure,
   167  			DNSPolicy:     api.DNSClusterFirst,
   168  			Containers:    []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}},
   169  		},
   170  	}
   171  	oldSchedule := "* * * * ?"
   172  	oldCronJob := &batch.CronJob{
   173  		ObjectMeta: metav1.ObjectMeta{
   174  			Name:            "mycronjob",
   175  			Namespace:       metav1.NamespaceDefault,
   176  			ResourceVersion: "10",
   177  		},
   178  		Spec: batch.CronJobSpec{
   179  			Schedule:          oldSchedule,
   180  			ConcurrencyPolicy: batch.AllowConcurrent,
   181  			JobTemplate: batch.JobTemplateSpec{
   182  				Spec: batch.JobSpec{
   183  					Template: validPodTemplateSpec,
   184  				},
   185  			},
   186  		},
   187  	}
   188  	now := metav1.Now()
   189  	newCronJob := &batch.CronJob{
   190  		ObjectMeta: metav1.ObjectMeta{
   191  			Name:            "mycronjob",
   192  			Namespace:       metav1.NamespaceDefault,
   193  			ResourceVersion: "9",
   194  		},
   195  		Spec: batch.CronJobSpec{
   196  			Schedule:          "5 5 * * ?",
   197  			ConcurrencyPolicy: batch.AllowConcurrent,
   198  			JobTemplate: batch.JobTemplateSpec{
   199  				Spec: batch.JobSpec{
   200  					Template: validPodTemplateSpec,
   201  				},
   202  			},
   203  		},
   204  		Status: batch.CronJobStatus{
   205  			LastScheduleTime: &now,
   206  		},
   207  	}
   208  
   209  	StatusStrategy.PrepareForUpdate(ctx, newCronJob, oldCronJob)
   210  	if newCronJob.Status.LastScheduleTime == nil {
   211  		t.Errorf("CronJob status updates must allow changes to cronJob status")
   212  	}
   213  	if newCronJob.Spec.Schedule != oldSchedule {
   214  		t.Errorf("CronJob status updates must now allow changes to cronJob spec")
   215  	}
   216  	errs := StatusStrategy.ValidateUpdate(ctx, newCronJob, oldCronJob)
   217  	if len(errs) != 0 {
   218  		t.Errorf("Unexpected error %v", errs)
   219  	}
   220  	if newCronJob.ResourceVersion != "9" {
   221  		t.Errorf("Incoming resource version on update should not be mutated")
   222  	}
   223  }
   224  
   225  func TestStrategy_ResetFields(t *testing.T) {
   226  	resetFields := Strategy.GetResetFields()
   227  	if len(resetFields) != 2 {
   228  		t.Errorf("ResetFields should have 2 elements, but have %d", len(resetFields))
   229  	}
   230  }
   231  
   232  func TestCronJobStatusStrategy_ResetFields(t *testing.T) {
   233  	resetFields := StatusStrategy.GetResetFields()
   234  	if len(resetFields) != 2 {
   235  		t.Errorf("ResetFields should have 2 elements, but have %d", len(resetFields))
   236  	}
   237  }
   238  
   239  func TestCronJobStrategy_WarningsOnCreate(t *testing.T) {
   240  	ctx := genericapirequest.NewDefaultContext()
   241  
   242  	now := metav1.Now()
   243  
   244  	testcases := map[string]struct {
   245  		cronjob           *batch.CronJob
   246  		wantWarningsCount int32
   247  	}{
   248  		"happy path cronjob": {
   249  			wantWarningsCount: 0,
   250  			cronjob: &batch.CronJob{
   251  				ObjectMeta: metav1.ObjectMeta{
   252  					Name:            "mycronjob",
   253  					Namespace:       metav1.NamespaceDefault,
   254  					ResourceVersion: "9",
   255  				},
   256  				Spec: validCronjobSpec,
   257  				Status: batch.CronJobStatus{
   258  					LastScheduleTime: &now,
   259  				},
   260  			},
   261  		},
   262  		"dns invalid name": {
   263  			wantWarningsCount: 1,
   264  			cronjob: &batch.CronJob{
   265  				ObjectMeta: metav1.ObjectMeta{
   266  					Name:            "my cronjob",
   267  					Namespace:       metav1.NamespaceDefault,
   268  					ResourceVersion: "9",
   269  				},
   270  				Spec: validCronjobSpec,
   271  				Status: batch.CronJobStatus{
   272  					LastScheduleTime: &now,
   273  				},
   274  			},
   275  		},
   276  	}
   277  	for name, tc := range testcases {
   278  		t.Run(name, func(t *testing.T) {
   279  			gotWarnings := Strategy.WarningsOnCreate(ctx, tc.cronjob)
   280  			if len(gotWarnings) != int(tc.wantWarningsCount) {
   281  				t.Errorf("%s: got warning length of %d but expected %d", name, len(gotWarnings), tc.wantWarningsCount)
   282  			}
   283  		})
   284  	}
   285  }
   286  
   287  func TestCronJobStrategy_WarningsOnUpdate(t *testing.T) {
   288  	ctx := genericapirequest.NewDefaultContext()
   289  	now := metav1.Now()
   290  
   291  	cases := map[string]struct {
   292  		oldCronJob        *batch.CronJob
   293  		cronjob           *batch.CronJob
   294  		wantWarningsCount int32
   295  	}{
   296  		"generation 0 for both": {
   297  			wantWarningsCount: 0,
   298  			oldCronJob: &batch.CronJob{
   299  				ObjectMeta: metav1.ObjectMeta{
   300  					Name:            "mycronjob",
   301  					Namespace:       metav1.NamespaceDefault,
   302  					ResourceVersion: "9",
   303  					Generation:      0,
   304  				},
   305  				Spec: validCronjobSpec,
   306  				Status: batch.CronJobStatus{
   307  					LastScheduleTime: &now,
   308  				},
   309  			},
   310  			cronjob: &batch.CronJob{
   311  				ObjectMeta: metav1.ObjectMeta{
   312  					Name:            "mycronjob",
   313  					Namespace:       metav1.NamespaceDefault,
   314  					ResourceVersion: "9",
   315  					Generation:      0,
   316  				},
   317  				Spec: validCronjobSpec,
   318  				Status: batch.CronJobStatus{
   319  					LastScheduleTime: &now,
   320  				},
   321  			},
   322  		},
   323  		"generation 1 for new; force WarningsOnUpdate to check PodTemplate for updates": {
   324  			wantWarningsCount: 0,
   325  			oldCronJob: &batch.CronJob{
   326  				ObjectMeta: metav1.ObjectMeta{
   327  					Name:            "mycronjob",
   328  					Namespace:       metav1.NamespaceDefault,
   329  					ResourceVersion: "9",
   330  					Generation:      1,
   331  				},
   332  				Spec: validCronjobSpec,
   333  				Status: batch.CronJobStatus{
   334  					LastScheduleTime: &now,
   335  				},
   336  			},
   337  			cronjob: &batch.CronJob{
   338  				ObjectMeta: metav1.ObjectMeta{
   339  					Name:            "mycronjob",
   340  					Namespace:       metav1.NamespaceDefault,
   341  					ResourceVersion: "9",
   342  					Generation:      0,
   343  				},
   344  				Spec: validCronjobSpec,
   345  				Status: batch.CronJobStatus{
   346  					LastScheduleTime: &now,
   347  				},
   348  			},
   349  		},
   350  		"force validation failure in pod template": {
   351  			oldCronJob: &batch.CronJob{
   352  				ObjectMeta: metav1.ObjectMeta{
   353  					Name:            "mycronjob",
   354  					Namespace:       metav1.NamespaceDefault,
   355  					ResourceVersion: "0",
   356  					Generation:      1,
   357  				},
   358  				Spec: validCronjobSpec,
   359  				Status: batch.CronJobStatus{
   360  					LastScheduleTime: &now,
   361  				},
   362  			},
   363  			cronjob: &batch.CronJob{
   364  				ObjectMeta: metav1.ObjectMeta{
   365  					Name:            "mycronjob",
   366  					Namespace:       metav1.NamespaceDefault,
   367  					ResourceVersion: "0",
   368  					Generation:      0,
   369  				},
   370  				Spec: batch.CronJobSpec{
   371  					Schedule:          "5 5 * * ?",
   372  					ConcurrencyPolicy: batch.AllowConcurrent,
   373  					JobTemplate: batch.JobTemplateSpec{
   374  						Spec: batch.JobSpec{
   375  							Template: api.PodTemplateSpec{
   376  								Spec: api.PodSpec{ImagePullSecrets: []api.LocalObjectReference{{Name: ""}}},
   377  							},
   378  						},
   379  					},
   380  				},
   381  				Status: batch.CronJobStatus{
   382  					LastScheduleTime: &now,
   383  				},
   384  			},
   385  			wantWarningsCount: 1,
   386  		},
   387  		"timezone invalid failure": {
   388  			oldCronJob: &batch.CronJob{
   389  				ObjectMeta: metav1.ObjectMeta{
   390  					Name:            "mycronjob",
   391  					Namespace:       metav1.NamespaceDefault,
   392  					ResourceVersion: "0",
   393  					Generation:      1,
   394  				},
   395  				Spec: validCronjobSpec,
   396  				Status: batch.CronJobStatus{
   397  					LastScheduleTime: &now,
   398  				},
   399  			},
   400  			cronjob: &batch.CronJob{
   401  				ObjectMeta: metav1.ObjectMeta{
   402  					Name:            "mycronjob",
   403  					Namespace:       metav1.NamespaceDefault,
   404  					ResourceVersion: "0",
   405  					Generation:      0,
   406  				},
   407  				Spec: cronjobSpecWithTZinSchedule,
   408  				Status: batch.CronJobStatus{
   409  					LastScheduleTime: &now,
   410  				},
   411  			},
   412  			wantWarningsCount: 1,
   413  		},
   414  	}
   415  	for val, tc := range cases {
   416  		t.Run(val, func(t *testing.T) {
   417  			gotWarnings := Strategy.WarningsOnUpdate(ctx, tc.cronjob, tc.oldCronJob)
   418  			if len(gotWarnings) != int(tc.wantWarningsCount) {
   419  				t.Errorf("%s: got warning length of %d but expected %d", val, len(gotWarnings), tc.wantWarningsCount)
   420  			}
   421  		})
   422  	}
   423  }
   424  

View as plain text