k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/registry/batch/cronjob/strategy_test.go (about) 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/ptr" 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: ptr.To("Asia/Shanghai"), 42 JobTemplate: batch.JobTemplateSpec{ 43 Spec: batch.JobSpec{ 44 Template: validPodTemplateSpec, 45 CompletionMode: completionModePtr(batch.IndexedCompletion), 46 Completions: ptr.To[int32](10), 47 Parallelism: ptr.To[int32](10), 48 }, 49 }, 50 } 51 cronjobSpecWithTZinSchedule = batch.CronJobSpec{ 52 Schedule: "CRON_TZ=UTC 5 5 * * ?", 53 ConcurrencyPolicy: batch.AllowConcurrent, 54 TimeZone: ptr.To("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 }