sigs.k8s.io/cluster-api@v1.7.1/internal/webhooks/machinedeployment_test.go (about) 1 /* 2 Copyright 2021 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 webhooks 18 19 import ( 20 "context" 21 "strings" 22 "testing" 23 24 . "github.com/onsi/gomega" 25 admissionv1 "k8s.io/api/admission/v1" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/runtime" 28 "k8s.io/apimachinery/pkg/util/intstr" 29 "k8s.io/utils/ptr" 30 "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 31 32 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 33 "sigs.k8s.io/cluster-api/internal/webhooks/util" 34 ) 35 36 func TestMachineDeploymentDefault(t *testing.T) { 37 g := NewWithT(t) 38 md := &clusterv1.MachineDeployment{ 39 ObjectMeta: metav1.ObjectMeta{ 40 Name: "test-md", 41 }, 42 Spec: clusterv1.MachineDeploymentSpec{ 43 ClusterName: "test-cluster", 44 Template: clusterv1.MachineTemplateSpec{ 45 Spec: clusterv1.MachineSpec{ 46 Version: ptr.To("1.19.10"), 47 }, 48 }, 49 }, 50 } 51 52 scheme := runtime.NewScheme() 53 g.Expect(clusterv1.AddToScheme(scheme)).To(Succeed()) 54 webhook := &MachineDeployment{ 55 decoder: admission.NewDecoder(scheme), 56 } 57 58 reqCtx := admission.NewContextWithRequest(ctx, admission.Request{ 59 AdmissionRequest: admissionv1.AdmissionRequest{ 60 Operation: admissionv1.Create, 61 }, 62 }) 63 t.Run("for MachineDeployment", util.CustomDefaultValidateTest(reqCtx, md, webhook)) 64 65 g.Expect(webhook.Default(reqCtx, md)).To(Succeed()) 66 67 g.Expect(md.Labels[clusterv1.ClusterNameLabel]).To(Equal(md.Spec.ClusterName)) 68 69 g.Expect(md.Spec.MinReadySeconds).To(Equal(ptr.To[int32](0))) 70 g.Expect(md.Spec.Replicas).To(Equal(ptr.To[int32](1))) 71 g.Expect(md.Spec.RevisionHistoryLimit).To(Equal(ptr.To[int32](1))) 72 g.Expect(md.Spec.ProgressDeadlineSeconds).To(Equal(ptr.To[int32](600))) 73 g.Expect(md.Spec.Strategy).ToNot(BeNil()) 74 75 g.Expect(md.Spec.Selector.MatchLabels).To(HaveKeyWithValue(clusterv1.MachineDeploymentNameLabel, "test-md")) 76 g.Expect(md.Spec.Template.Labels).To(HaveKeyWithValue(clusterv1.MachineDeploymentNameLabel, "test-md")) 77 g.Expect(md.Spec.Selector.MatchLabels).To(HaveKeyWithValue(clusterv1.ClusterNameLabel, "test-cluster")) 78 g.Expect(md.Spec.Template.Labels).To(HaveKeyWithValue(clusterv1.ClusterNameLabel, "test-cluster")) 79 80 g.Expect(md.Spec.Strategy.Type).To(Equal(clusterv1.RollingUpdateMachineDeploymentStrategyType)) 81 g.Expect(md.Spec.Strategy.RollingUpdate).ToNot(BeNil()) 82 g.Expect(md.Spec.Strategy.RollingUpdate.MaxSurge.IntValue()).To(Equal(1)) 83 g.Expect(md.Spec.Strategy.RollingUpdate.MaxUnavailable.IntValue()).To(Equal(0)) 84 85 g.Expect(*md.Spec.Template.Spec.Version).To(Equal("v1.19.10")) 86 } 87 88 func TestCalculateMachineDeploymentReplicas(t *testing.T) { 89 tests := []struct { 90 name string 91 newMD *clusterv1.MachineDeployment 92 oldMD *clusterv1.MachineDeployment 93 expectedReplicas int32 94 expectErr bool 95 }{ 96 { 97 name: "if new MD has replicas set, keep that value", 98 newMD: &clusterv1.MachineDeployment{ 99 Spec: clusterv1.MachineDeploymentSpec{ 100 Replicas: ptr.To[int32](5), 101 }, 102 }, 103 expectedReplicas: 5, 104 }, 105 { 106 name: "if new MD does not have replicas set and no annotations, use 1", 107 newMD: &clusterv1.MachineDeployment{}, 108 expectedReplicas: 1, 109 }, 110 { 111 name: "if new MD only has min size annotation, fallback to 1", 112 newMD: &clusterv1.MachineDeployment{ 113 ObjectMeta: metav1.ObjectMeta{ 114 Annotations: map[string]string{ 115 clusterv1.AutoscalerMinSizeAnnotation: "3", 116 }, 117 }, 118 }, 119 expectedReplicas: 1, 120 }, 121 { 122 name: "if new MD only has max size annotation, fallback to 1", 123 newMD: &clusterv1.MachineDeployment{ 124 ObjectMeta: metav1.ObjectMeta{ 125 Annotations: map[string]string{ 126 clusterv1.AutoscalerMaxSizeAnnotation: "7", 127 }, 128 }, 129 }, 130 expectedReplicas: 1, 131 }, 132 { 133 name: "if new MD has min and max size annotation and min size is invalid, fail", 134 newMD: &clusterv1.MachineDeployment{ 135 ObjectMeta: metav1.ObjectMeta{ 136 Annotations: map[string]string{ 137 clusterv1.AutoscalerMinSizeAnnotation: "abc", 138 clusterv1.AutoscalerMaxSizeAnnotation: "7", 139 }, 140 }, 141 }, 142 expectErr: true, 143 }, 144 { 145 name: "if new MD has min and max size annotation and max size is invalid, fail", 146 newMD: &clusterv1.MachineDeployment{ 147 ObjectMeta: metav1.ObjectMeta{ 148 Annotations: map[string]string{ 149 clusterv1.AutoscalerMinSizeAnnotation: "3", 150 clusterv1.AutoscalerMaxSizeAnnotation: "abc", 151 }, 152 }, 153 }, 154 expectErr: true, 155 }, 156 { 157 name: "if new MD has min and max size annotation and new MD is a new MD, use min size", 158 newMD: &clusterv1.MachineDeployment{ 159 ObjectMeta: metav1.ObjectMeta{ 160 Annotations: map[string]string{ 161 clusterv1.AutoscalerMinSizeAnnotation: "3", 162 clusterv1.AutoscalerMaxSizeAnnotation: "7", 163 }, 164 }, 165 }, 166 expectedReplicas: 3, 167 }, 168 { 169 name: "if new MD has min and max size annotation and old MD doesn't have replicas set, use min size", 170 newMD: &clusterv1.MachineDeployment{ 171 ObjectMeta: metav1.ObjectMeta{ 172 Annotations: map[string]string{ 173 clusterv1.AutoscalerMinSizeAnnotation: "3", 174 clusterv1.AutoscalerMaxSizeAnnotation: "7", 175 }, 176 }, 177 }, 178 oldMD: &clusterv1.MachineDeployment{}, 179 expectedReplicas: 3, 180 }, 181 { 182 name: "if new MD has min and max size annotation and old MD replicas is below min size, use min size", 183 newMD: &clusterv1.MachineDeployment{ 184 ObjectMeta: metav1.ObjectMeta{ 185 Annotations: map[string]string{ 186 clusterv1.AutoscalerMinSizeAnnotation: "3", 187 clusterv1.AutoscalerMaxSizeAnnotation: "7", 188 }, 189 }, 190 }, 191 oldMD: &clusterv1.MachineDeployment{ 192 Spec: clusterv1.MachineDeploymentSpec{ 193 Replicas: ptr.To[int32](1), 194 }, 195 }, 196 expectedReplicas: 3, 197 }, 198 { 199 name: "if new MD has min and max size annotation and old MD replicas is above max size, use max size", 200 newMD: &clusterv1.MachineDeployment{ 201 ObjectMeta: metav1.ObjectMeta{ 202 Annotations: map[string]string{ 203 clusterv1.AutoscalerMinSizeAnnotation: "3", 204 clusterv1.AutoscalerMaxSizeAnnotation: "7", 205 }, 206 }, 207 }, 208 oldMD: &clusterv1.MachineDeployment{ 209 Spec: clusterv1.MachineDeploymentSpec{ 210 Replicas: ptr.To[int32](15), 211 }, 212 }, 213 expectedReplicas: 7, 214 }, 215 { 216 name: "if new MD has min and max size annotation and old MD replicas is between min and max size, use old MD replicas", 217 newMD: &clusterv1.MachineDeployment{ 218 ObjectMeta: metav1.ObjectMeta{ 219 Annotations: map[string]string{ 220 clusterv1.AutoscalerMinSizeAnnotation: "3", 221 clusterv1.AutoscalerMaxSizeAnnotation: "7", 222 }, 223 }, 224 }, 225 oldMD: &clusterv1.MachineDeployment{ 226 Spec: clusterv1.MachineDeploymentSpec{ 227 Replicas: ptr.To[int32](4), 228 }, 229 }, 230 expectedReplicas: 4, 231 }, 232 } 233 234 for _, tt := range tests { 235 t.Run(tt.name, func(t *testing.T) { 236 g := NewWithT(t) 237 238 replicas, err := calculateMachineDeploymentReplicas(context.Background(), tt.oldMD, tt.newMD, false) 239 240 if tt.expectErr { 241 g.Expect(err).To(HaveOccurred()) 242 return 243 } 244 245 g.Expect(err).ToNot(HaveOccurred()) 246 g.Expect(replicas).To(Equal(tt.expectedReplicas)) 247 }) 248 } 249 } 250 251 func TestMachineDeploymentValidation(t *testing.T) { 252 badMaxSurge := intstr.FromString("1") 253 badMaxUnavailable := intstr.FromString("0") 254 255 goodMaxSurgePercentage := intstr.FromString("1%") 256 goodMaxUnavailablePercentage := intstr.FromString("0%") 257 258 goodMaxSurgeInt := intstr.FromInt(1) 259 goodMaxUnavailableInt := intstr.FromInt(0) 260 tests := []struct { 261 name string 262 md *clusterv1.MachineDeployment 263 mdName string 264 selectors map[string]string 265 labels map[string]string 266 strategy clusterv1.MachineDeploymentStrategy 267 expectErr bool 268 }{ 269 { 270 name: "pass with name of under 63 characters", 271 mdName: "short-name", 272 expectErr: false, 273 }, 274 { 275 name: "pass with _, -, . characters in name", 276 mdName: "thisNameContains.A_Non-Alphanumeric", 277 expectErr: false, 278 }, 279 { 280 name: "error with name of more than 63 characters", 281 mdName: "thisNameIsReallyMuchLongerThanTheMaximumLengthOfSixtyThreeCharacters", 282 expectErr: true, 283 }, 284 { 285 name: "error when name starts with NonAlphanumeric character", 286 mdName: "-thisNameStartsWithANonAlphanumeric", 287 expectErr: true, 288 }, 289 { 290 name: "error when name ends with NonAlphanumeric character", 291 mdName: "thisNameEndsWithANonAlphanumeric.", 292 expectErr: true, 293 }, 294 { 295 name: "error when name contains invalid NonAlphanumeric character", 296 mdName: "thisNameContainsInvalid!@NonAlphanumerics", 297 expectErr: true, 298 }, 299 { 300 name: "should return error on mismatch", 301 selectors: map[string]string{"foo": "bar"}, 302 labels: map[string]string{"foo": "baz"}, 303 expectErr: true, 304 }, 305 { 306 name: "should return error on missing labels", 307 selectors: map[string]string{"foo": "bar"}, 308 labels: map[string]string{"": ""}, 309 expectErr: true, 310 }, 311 { 312 name: "should return error if all selectors don't match", 313 selectors: map[string]string{"foo": "bar", "hello": "world"}, 314 labels: map[string]string{"foo": "bar"}, 315 expectErr: true, 316 }, 317 { 318 name: "should not return error on match", 319 selectors: map[string]string{"foo": "bar"}, 320 labels: map[string]string{"foo": "bar"}, 321 expectErr: false, 322 }, 323 { 324 name: "should return error for invalid selector", 325 selectors: map[string]string{"-123-foo": "bar"}, 326 labels: map[string]string{"-123-foo": "bar"}, 327 expectErr: true, 328 }, 329 { 330 name: "should return error for invalid maxSurge", 331 selectors: map[string]string{"foo": "bar"}, 332 labels: map[string]string{"foo": "bar"}, 333 strategy: clusterv1.MachineDeploymentStrategy{ 334 Type: clusterv1.RollingUpdateMachineDeploymentStrategyType, 335 RollingUpdate: &clusterv1.MachineRollingUpdateDeployment{ 336 MaxUnavailable: &goodMaxUnavailableInt, 337 MaxSurge: &badMaxSurge, 338 }, 339 }, 340 expectErr: true, 341 }, 342 { 343 name: "should return error for invalid maxUnavailable", 344 selectors: map[string]string{"foo": "bar"}, 345 labels: map[string]string{"foo": "bar"}, 346 strategy: clusterv1.MachineDeploymentStrategy{ 347 Type: clusterv1.RollingUpdateMachineDeploymentStrategyType, 348 RollingUpdate: &clusterv1.MachineRollingUpdateDeployment{ 349 MaxUnavailable: &badMaxUnavailable, 350 MaxSurge: &goodMaxSurgeInt, 351 }, 352 }, 353 expectErr: true, 354 }, 355 { 356 name: "should not return error for valid int maxSurge and maxUnavailable", 357 selectors: map[string]string{"foo": "bar"}, 358 labels: map[string]string{"foo": "bar"}, 359 strategy: clusterv1.MachineDeploymentStrategy{ 360 Type: clusterv1.RollingUpdateMachineDeploymentStrategyType, 361 RollingUpdate: &clusterv1.MachineRollingUpdateDeployment{ 362 MaxUnavailable: &goodMaxUnavailableInt, 363 MaxSurge: &goodMaxSurgeInt, 364 }, 365 }, 366 expectErr: false, 367 }, 368 { 369 name: "should not return error for valid percentage string maxSurge and maxUnavailable", 370 selectors: map[string]string{"foo": "bar"}, 371 labels: map[string]string{"foo": "bar"}, 372 strategy: clusterv1.MachineDeploymentStrategy{ 373 Type: clusterv1.RollingUpdateMachineDeploymentStrategyType, 374 RollingUpdate: &clusterv1.MachineRollingUpdateDeployment{ 375 MaxUnavailable: &goodMaxUnavailablePercentage, 376 MaxSurge: &goodMaxSurgePercentage, 377 }, 378 }, 379 expectErr: false, 380 }, 381 } 382 383 for i := range tests { 384 tt := tests[i] 385 t.Run(tt.name, func(t *testing.T) { 386 g := NewWithT(t) 387 md := &clusterv1.MachineDeployment{ 388 ObjectMeta: metav1.ObjectMeta{ 389 Name: tt.mdName, 390 }, 391 Spec: clusterv1.MachineDeploymentSpec{ 392 Strategy: &tt.strategy, 393 Selector: metav1.LabelSelector{ 394 MatchLabels: tt.selectors, 395 }, 396 Template: clusterv1.MachineTemplateSpec{ 397 ObjectMeta: clusterv1.ObjectMeta{ 398 Labels: tt.labels, 399 }, 400 }, 401 }, 402 } 403 404 scheme := runtime.NewScheme() 405 g.Expect(clusterv1.AddToScheme(scheme)).To(Succeed()) 406 webhook := MachineDeployment{ 407 decoder: admission.NewDecoder(scheme), 408 } 409 410 if tt.expectErr { 411 warnings, err := webhook.ValidateCreate(ctx, md) 412 g.Expect(err).To(HaveOccurred()) 413 g.Expect(warnings).To(BeEmpty()) 414 warnings, err = webhook.ValidateUpdate(ctx, md, md) 415 g.Expect(err).To(HaveOccurred()) 416 g.Expect(warnings).To(BeEmpty()) 417 } else { 418 warnings, err := webhook.ValidateCreate(ctx, md) 419 g.Expect(err).ToNot(HaveOccurred()) 420 g.Expect(warnings).To(BeEmpty()) 421 warnings, err = webhook.ValidateUpdate(ctx, md, md) 422 g.Expect(err).ToNot(HaveOccurred()) 423 g.Expect(warnings).To(BeEmpty()) 424 } 425 }) 426 } 427 } 428 429 func TestMachineDeploymentVersionValidation(t *testing.T) { 430 tests := []struct { 431 name string 432 version string 433 expectErr bool 434 }{ 435 { 436 name: "should succeed when given a valid semantic version with prepended 'v'", 437 version: "v1.17.2", 438 expectErr: false, 439 }, 440 { 441 name: "should return error when given a valid semantic version without 'v'", 442 version: "1.17.2", 443 expectErr: true, 444 }, 445 { 446 name: "should return error when given an invalid semantic version", 447 version: "1", 448 expectErr: true, 449 }, 450 { 451 name: "should return error when given an invalid semantic version", 452 version: "v1", 453 expectErr: true, 454 }, 455 { 456 name: "should return error when given an invalid semantic version", 457 version: "wrong_version", 458 expectErr: true, 459 }, 460 } 461 462 for _, tt := range tests { 463 t.Run(tt.name, func(t *testing.T) { 464 g := NewWithT(t) 465 466 md := &clusterv1.MachineDeployment{ 467 Spec: clusterv1.MachineDeploymentSpec{ 468 Template: clusterv1.MachineTemplateSpec{ 469 Spec: clusterv1.MachineSpec{ 470 Version: ptr.To(tt.version), 471 }, 472 }, 473 }, 474 } 475 476 scheme := runtime.NewScheme() 477 g.Expect(clusterv1.AddToScheme(scheme)).To(Succeed()) 478 webhook := MachineDeployment{ 479 decoder: admission.NewDecoder(scheme), 480 } 481 482 if tt.expectErr { 483 warnings, err := webhook.ValidateCreate(ctx, md) 484 g.Expect(err).To(HaveOccurred()) 485 g.Expect(warnings).To(BeEmpty()) 486 warnings, err = webhook.ValidateUpdate(ctx, md, md) 487 g.Expect(err).To(HaveOccurred()) 488 g.Expect(warnings).To(BeEmpty()) 489 } else { 490 warnings, err := webhook.ValidateCreate(ctx, md) 491 g.Expect(err).ToNot(HaveOccurred()) 492 g.Expect(warnings).To(BeEmpty()) 493 warnings, err = webhook.ValidateUpdate(ctx, md, md) 494 g.Expect(err).ToNot(HaveOccurred()) 495 g.Expect(warnings).To(BeEmpty()) 496 } 497 }) 498 } 499 } 500 501 func TestMachineDeploymentClusterNameImmutable(t *testing.T) { 502 tests := []struct { 503 name string 504 oldClusterName string 505 newClusterName string 506 expectErr bool 507 }{ 508 { 509 name: "when the cluster name has not changed", 510 oldClusterName: "foo", 511 newClusterName: "foo", 512 expectErr: false, 513 }, 514 { 515 name: "when the cluster name has changed", 516 oldClusterName: "foo", 517 newClusterName: "bar", 518 expectErr: true, 519 }, 520 } 521 522 for _, tt := range tests { 523 t.Run(tt.name, func(t *testing.T) { 524 g := NewWithT(t) 525 526 newMD := &clusterv1.MachineDeployment{ 527 Spec: clusterv1.MachineDeploymentSpec{ 528 ClusterName: tt.newClusterName, 529 }, 530 } 531 532 oldMD := &clusterv1.MachineDeployment{ 533 Spec: clusterv1.MachineDeploymentSpec{ 534 ClusterName: tt.oldClusterName, 535 }, 536 } 537 538 scheme := runtime.NewScheme() 539 g.Expect(clusterv1.AddToScheme(scheme)).To(Succeed()) 540 webhook := MachineDeployment{ 541 decoder: admission.NewDecoder(scheme), 542 } 543 544 warnings, err := webhook.ValidateUpdate(ctx, oldMD, newMD) 545 if tt.expectErr { 546 g.Expect(err).To(HaveOccurred()) 547 } else { 548 g.Expect(err).ToNot(HaveOccurred()) 549 } 550 g.Expect(warnings).To(BeEmpty()) 551 }) 552 } 553 } 554 555 func TestMachineDeploymentTemplateMetadataValidation(t *testing.T) { 556 tests := []struct { 557 name string 558 labels map[string]string 559 annotations map[string]string 560 expectErr bool 561 }{ 562 { 563 name: "should return error for invalid labels and annotations", 564 labels: map[string]string{ 565 "foo": "$invalid-key", 566 "bar": strings.Repeat("a", 64) + "too-long-value", 567 "/invalid-key": "foo", 568 }, 569 annotations: map[string]string{ 570 "/invalid-key": "foo", 571 }, 572 expectErr: true, 573 }, 574 } 575 576 for _, tt := range tests { 577 t.Run(tt.name, func(t *testing.T) { 578 g := NewWithT(t) 579 md := &clusterv1.MachineDeployment{ 580 Spec: clusterv1.MachineDeploymentSpec{ 581 Template: clusterv1.MachineTemplateSpec{ 582 ObjectMeta: clusterv1.ObjectMeta{ 583 Labels: tt.labels, 584 Annotations: tt.annotations, 585 }, 586 }, 587 }, 588 } 589 590 scheme := runtime.NewScheme() 591 g.Expect(clusterv1.AddToScheme(scheme)).To(Succeed()) 592 webhook := MachineDeployment{ 593 decoder: admission.NewDecoder(scheme), 594 } 595 596 if tt.expectErr { 597 warnings, err := webhook.ValidateCreate(ctx, md) 598 g.Expect(err).To(HaveOccurred()) 599 g.Expect(warnings).To(BeEmpty()) 600 warnings, err = webhook.ValidateUpdate(ctx, md, md) 601 g.Expect(err).To(HaveOccurred()) 602 g.Expect(warnings).To(BeEmpty()) 603 } else { 604 warnings, err := webhook.ValidateCreate(ctx, md) 605 g.Expect(err).ToNot(HaveOccurred()) 606 g.Expect(warnings).To(BeEmpty()) 607 warnings, err = webhook.ValidateUpdate(ctx, md, md) 608 g.Expect(err).ToNot(HaveOccurred()) 609 g.Expect(warnings).To(BeEmpty()) 610 } 611 }) 612 } 613 }