sigs.k8s.io/cluster-api@v1.7.1/internal/webhooks/machineset_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 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/runtime" 27 "k8s.io/utils/ptr" 28 "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 29 30 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 31 "sigs.k8s.io/cluster-api/internal/webhooks/util" 32 ) 33 34 func TestMachineSetDefault(t *testing.T) { 35 g := NewWithT(t) 36 ms := &clusterv1.MachineSet{ 37 ObjectMeta: metav1.ObjectMeta{ 38 Name: "test-ms", 39 }, 40 Spec: clusterv1.MachineSetSpec{ 41 Template: clusterv1.MachineTemplateSpec{ 42 Spec: clusterv1.MachineSpec{ 43 Version: ptr.To("1.19.10"), 44 }, 45 }, 46 }, 47 } 48 49 scheme := runtime.NewScheme() 50 g.Expect(clusterv1.AddToScheme(scheme)).To(Succeed()) 51 webhook := &MachineSet{ 52 decoder: admission.NewDecoder(scheme), 53 } 54 55 reqCtx := admission.NewContextWithRequest(ctx, admission.Request{}) 56 t.Run("for MachineSet", util.CustomDefaultValidateTest(reqCtx, ms, webhook)) 57 g.Expect(webhook.Default(reqCtx, ms)).To(Succeed()) 58 59 g.Expect(ms.Labels[clusterv1.ClusterNameLabel]).To(Equal(ms.Spec.ClusterName)) 60 g.Expect(ms.Spec.DeletePolicy).To(Equal(string(clusterv1.RandomMachineSetDeletePolicy))) 61 g.Expect(ms.Spec.Selector.MatchLabels).To(HaveKeyWithValue(clusterv1.MachineSetNameLabel, "test-ms")) 62 g.Expect(ms.Spec.Template.Labels).To(HaveKeyWithValue(clusterv1.MachineSetNameLabel, "test-ms")) 63 g.Expect(*ms.Spec.Template.Spec.Version).To(Equal("v1.19.10")) 64 } 65 66 func TestCalculateMachineSetReplicas(t *testing.T) { 67 tests := []struct { 68 name string 69 newMS *clusterv1.MachineSet 70 oldMS *clusterv1.MachineSet 71 expectedReplicas int32 72 expectErr bool 73 }{ 74 { 75 name: "if new MS has replicas set, keep that value", 76 newMS: &clusterv1.MachineSet{ 77 Spec: clusterv1.MachineSetSpec{ 78 Replicas: ptr.To[int32](5), 79 }, 80 }, 81 expectedReplicas: 5, 82 }, 83 { 84 name: "if new MS does not have replicas set and no annotations, use 1", 85 newMS: &clusterv1.MachineSet{}, 86 expectedReplicas: 1, 87 }, 88 { 89 name: "if new MS only has min size annotation, fallback to 1", 90 newMS: &clusterv1.MachineSet{ 91 ObjectMeta: metav1.ObjectMeta{ 92 Annotations: map[string]string{ 93 clusterv1.AutoscalerMinSizeAnnotation: "3", 94 }, 95 }, 96 }, 97 expectedReplicas: 1, 98 }, 99 { 100 name: "if new MS only has max size annotation, fallback to 1", 101 newMS: &clusterv1.MachineSet{ 102 ObjectMeta: metav1.ObjectMeta{ 103 Annotations: map[string]string{ 104 clusterv1.AutoscalerMaxSizeAnnotation: "7", 105 }, 106 }, 107 }, 108 expectedReplicas: 1, 109 }, 110 { 111 name: "if new MS has min and max size annotation and min size is invalid, fail", 112 newMS: &clusterv1.MachineSet{ 113 ObjectMeta: metav1.ObjectMeta{ 114 Annotations: map[string]string{ 115 clusterv1.AutoscalerMinSizeAnnotation: "abc", 116 clusterv1.AutoscalerMaxSizeAnnotation: "7", 117 }, 118 }, 119 }, 120 expectErr: true, 121 }, 122 { 123 name: "if new MS has min and max size annotation and max size is invalid, fail", 124 newMS: &clusterv1.MachineSet{ 125 ObjectMeta: metav1.ObjectMeta{ 126 Annotations: map[string]string{ 127 clusterv1.AutoscalerMinSizeAnnotation: "3", 128 clusterv1.AutoscalerMaxSizeAnnotation: "abc", 129 }, 130 }, 131 }, 132 expectErr: true, 133 }, 134 { 135 name: "if new MS has min and max size annotation and new MS is a new MS, use min size", 136 newMS: &clusterv1.MachineSet{ 137 ObjectMeta: metav1.ObjectMeta{ 138 Annotations: map[string]string{ 139 clusterv1.AutoscalerMinSizeAnnotation: "3", 140 clusterv1.AutoscalerMaxSizeAnnotation: "7", 141 }, 142 }, 143 }, 144 expectedReplicas: 3, 145 }, 146 { 147 name: "if new MS has min and max size annotation and old MS doesn't have replicas set, use min size", 148 newMS: &clusterv1.MachineSet{ 149 ObjectMeta: metav1.ObjectMeta{ 150 Annotations: map[string]string{ 151 clusterv1.AutoscalerMinSizeAnnotation: "3", 152 clusterv1.AutoscalerMaxSizeAnnotation: "7", 153 }, 154 }, 155 }, 156 oldMS: &clusterv1.MachineSet{}, 157 expectedReplicas: 3, 158 }, 159 { 160 name: "if new MS has min and max size annotation and old MS replicas is below min size, use min size", 161 newMS: &clusterv1.MachineSet{ 162 ObjectMeta: metav1.ObjectMeta{ 163 Annotations: map[string]string{ 164 clusterv1.AutoscalerMinSizeAnnotation: "3", 165 clusterv1.AutoscalerMaxSizeAnnotation: "7", 166 }, 167 }, 168 }, 169 oldMS: &clusterv1.MachineSet{ 170 Spec: clusterv1.MachineSetSpec{ 171 Replicas: ptr.To[int32](1), 172 }, 173 }, 174 expectedReplicas: 3, 175 }, 176 { 177 name: "if new MS has min and max size annotation and old MS replicas is above max size, use max size", 178 newMS: &clusterv1.MachineSet{ 179 ObjectMeta: metav1.ObjectMeta{ 180 Annotations: map[string]string{ 181 clusterv1.AutoscalerMinSizeAnnotation: "3", 182 clusterv1.AutoscalerMaxSizeAnnotation: "7", 183 }, 184 }, 185 }, 186 oldMS: &clusterv1.MachineSet{ 187 Spec: clusterv1.MachineSetSpec{ 188 Replicas: ptr.To[int32](15), 189 }, 190 }, 191 expectedReplicas: 7, 192 }, 193 { 194 name: "if new MS has min and max size annotation and old MS replicas is between min and max size, use old MS replicas", 195 newMS: &clusterv1.MachineSet{ 196 ObjectMeta: metav1.ObjectMeta{ 197 Annotations: map[string]string{ 198 clusterv1.AutoscalerMinSizeAnnotation: "3", 199 clusterv1.AutoscalerMaxSizeAnnotation: "7", 200 }, 201 }, 202 }, 203 oldMS: &clusterv1.MachineSet{ 204 Spec: clusterv1.MachineSetSpec{ 205 Replicas: ptr.To[int32](4), 206 }, 207 }, 208 expectedReplicas: 4, 209 }, 210 } 211 212 for _, tt := range tests { 213 t.Run(tt.name, func(t *testing.T) { 214 g := NewWithT(t) 215 216 replicas, err := calculateMachineSetReplicas(context.Background(), tt.oldMS, tt.newMS, false) 217 218 if tt.expectErr { 219 g.Expect(err).To(HaveOccurred()) 220 return 221 } 222 223 g.Expect(err).ToNot(HaveOccurred()) 224 g.Expect(replicas).To(Equal(tt.expectedReplicas)) 225 }) 226 } 227 } 228 229 func TestMachineSetLabelSelectorMatchValidation(t *testing.T) { 230 tests := []struct { 231 name string 232 selectors map[string]string 233 labels map[string]string 234 expectErr bool 235 }{ 236 { 237 name: "should return error on mismatch", 238 selectors: map[string]string{"foo": "bar"}, 239 labels: map[string]string{"foo": "baz"}, 240 expectErr: true, 241 }, 242 { 243 name: "should return error on missing labels", 244 selectors: map[string]string{"foo": "bar"}, 245 labels: map[string]string{"": ""}, 246 expectErr: true, 247 }, 248 { 249 name: "should return error if all selectors don't match", 250 selectors: map[string]string{"foo": "bar", "hello": "world"}, 251 labels: map[string]string{"foo": "bar"}, 252 expectErr: true, 253 }, 254 { 255 name: "should not return error on match", 256 selectors: map[string]string{"foo": "bar"}, 257 labels: map[string]string{"foo": "bar"}, 258 expectErr: false, 259 }, 260 { 261 name: "should return error for invalid selector", 262 selectors: map[string]string{"-123-foo": "bar"}, 263 labels: map[string]string{"-123-foo": "bar"}, 264 expectErr: true, 265 }, 266 } 267 268 for _, tt := range tests { 269 t.Run(tt.name, func(t *testing.T) { 270 g := NewWithT(t) 271 ms := &clusterv1.MachineSet{ 272 Spec: clusterv1.MachineSetSpec{ 273 Selector: metav1.LabelSelector{ 274 MatchLabels: tt.selectors, 275 }, 276 Template: clusterv1.MachineTemplateSpec{ 277 ObjectMeta: clusterv1.ObjectMeta{ 278 Labels: tt.labels, 279 }, 280 }, 281 }, 282 } 283 webhook := &MachineSet{} 284 285 if tt.expectErr { 286 warnings, err := webhook.ValidateCreate(ctx, ms) 287 g.Expect(err).To(HaveOccurred()) 288 g.Expect(warnings).To(BeEmpty()) 289 warnings, err = webhook.ValidateUpdate(ctx, ms, ms) 290 g.Expect(err).To(HaveOccurred()) 291 g.Expect(warnings).To(BeEmpty()) 292 } else { 293 warnings, err := webhook.ValidateCreate(ctx, ms) 294 g.Expect(err).ToNot(HaveOccurred()) 295 g.Expect(warnings).To(BeEmpty()) 296 warnings, err = webhook.ValidateUpdate(ctx, ms, ms) 297 g.Expect(err).ToNot(HaveOccurred()) 298 g.Expect(warnings).To(BeEmpty()) 299 } 300 }) 301 } 302 } 303 304 func TestMachineSetClusterNameImmutable(t *testing.T) { 305 tests := []struct { 306 name string 307 oldClusterName string 308 newClusterName string 309 expectErr bool 310 }{ 311 { 312 name: "when the cluster name has not changed", 313 oldClusterName: "foo", 314 newClusterName: "foo", 315 expectErr: false, 316 }, 317 { 318 name: "when the cluster name has changed", 319 oldClusterName: "foo", 320 newClusterName: "bar", 321 expectErr: true, 322 }, 323 } 324 325 for _, tt := range tests { 326 t.Run(tt.name, func(t *testing.T) { 327 g := NewWithT(t) 328 329 newMS := &clusterv1.MachineSet{ 330 Spec: clusterv1.MachineSetSpec{ 331 ClusterName: tt.newClusterName, 332 }, 333 } 334 335 oldMS := &clusterv1.MachineSet{ 336 Spec: clusterv1.MachineSetSpec{ 337 ClusterName: tt.oldClusterName, 338 }, 339 } 340 341 warnings, err := (&MachineSet{}).ValidateUpdate(ctx, oldMS, newMS) 342 if tt.expectErr { 343 g.Expect(err).To(HaveOccurred()) 344 } else { 345 g.Expect(err).ToNot(HaveOccurred()) 346 } 347 g.Expect(warnings).To(BeEmpty()) 348 }) 349 } 350 } 351 352 func TestMachineSetVersionValidation(t *testing.T) { 353 tests := []struct { 354 name string 355 version string 356 expectErr bool 357 }{ 358 { 359 name: "should succeed when given a valid semantic version with prepended 'v'", 360 version: "v1.19.2", 361 expectErr: false, 362 }, 363 { 364 name: "should return error when given a valid semantic version without 'v'", 365 version: "1.19.2", 366 expectErr: true, 367 }, 368 { 369 name: "should return error when given an invalid semantic version", 370 version: "1", 371 expectErr: true, 372 }, 373 { 374 name: "should return error when given an invalid semantic version", 375 version: "v1", 376 expectErr: true, 377 }, 378 { 379 name: "should return error when given an invalid semantic version", 380 version: "wrong_version", 381 expectErr: true, 382 }, 383 } 384 385 for _, tt := range tests { 386 t.Run(tt.name, func(t *testing.T) { 387 g := NewWithT(t) 388 389 ms := &clusterv1.MachineSet{ 390 Spec: clusterv1.MachineSetSpec{ 391 Template: clusterv1.MachineTemplateSpec{ 392 Spec: clusterv1.MachineSpec{ 393 Version: ptr.To(tt.version), 394 }, 395 }, 396 }, 397 } 398 webhook := &MachineSet{} 399 400 if tt.expectErr { 401 warnings, err := webhook.ValidateCreate(ctx, ms) 402 g.Expect(err).To(HaveOccurred()) 403 g.Expect(warnings).To(BeEmpty()) 404 warnings, err = webhook.ValidateUpdate(ctx, ms, ms) 405 g.Expect(err).To(HaveOccurred()) 406 g.Expect(warnings).To(BeEmpty()) 407 } else { 408 warnings, err := webhook.ValidateCreate(ctx, ms) 409 g.Expect(err).ToNot(HaveOccurred()) 410 g.Expect(warnings).To(BeEmpty()) 411 warnings, err = webhook.ValidateUpdate(ctx, ms, ms) 412 g.Expect(err).ToNot(HaveOccurred()) 413 g.Expect(warnings).To(BeEmpty()) 414 } 415 }) 416 } 417 } 418 419 func TestValidateSkippedMachineSetPreflightChecks(t *testing.T) { 420 tests := []struct { 421 name string 422 ms *clusterv1.MachineSet 423 expectErr bool 424 }{ 425 { 426 name: "should pass if the machine set skip preflight checks annotation is not set", 427 ms: &clusterv1.MachineSet{}, 428 expectErr: false, 429 }, 430 { 431 name: "should pass if not preflight checks are skipped", 432 ms: &clusterv1.MachineSet{ 433 ObjectMeta: metav1.ObjectMeta{ 434 Annotations: map[string]string{ 435 clusterv1.MachineSetSkipPreflightChecksAnnotation: "", 436 }, 437 }, 438 }, 439 expectErr: false, 440 }, 441 { 442 name: "should pass if only valid preflight checks are skipped (single)", 443 ms: &clusterv1.MachineSet{ 444 ObjectMeta: metav1.ObjectMeta{ 445 Annotations: map[string]string{ 446 clusterv1.MachineSetSkipPreflightChecksAnnotation: string(clusterv1.MachineSetPreflightCheckKubeadmVersionSkew), 447 }, 448 }, 449 }, 450 expectErr: false, 451 }, 452 { 453 name: "should pass if only valid preflight checks are skipped (multiple)", 454 ms: &clusterv1.MachineSet{ 455 ObjectMeta: metav1.ObjectMeta{ 456 Annotations: map[string]string{ 457 clusterv1.MachineSetSkipPreflightChecksAnnotation: string(clusterv1.MachineSetPreflightCheckKubeadmVersionSkew) + "," + string(clusterv1.MachineSetPreflightCheckControlPlaneIsStable), 458 }, 459 }, 460 }, 461 expectErr: false, 462 }, 463 { 464 name: "should fail if invalid preflight checks are skipped", 465 ms: &clusterv1.MachineSet{ 466 ObjectMeta: metav1.ObjectMeta{ 467 Annotations: map[string]string{ 468 clusterv1.MachineSetSkipPreflightChecksAnnotation: string(clusterv1.MachineSetPreflightCheckKubeadmVersionSkew) + ",invalid-preflight-check-name", 469 }, 470 }, 471 }, 472 expectErr: true, 473 }, 474 } 475 476 for _, tt := range tests { 477 t.Run(tt.name, func(t *testing.T) { 478 g := NewWithT(t) 479 err := validateSkippedMachineSetPreflightChecks(tt.ms) 480 if tt.expectErr { 481 g.Expect(err).To(HaveOccurred()) 482 } else { 483 g.Expect(err).ToNot(HaveOccurred()) 484 } 485 }) 486 } 487 } 488 489 func TestMachineSetTemplateMetadataValidation(t *testing.T) { 490 tests := []struct { 491 name string 492 labels map[string]string 493 annotations map[string]string 494 expectErr bool 495 }{ 496 { 497 name: "should return error for invalid labels and annotations", 498 labels: map[string]string{ 499 "foo": "$invalid-key", 500 "bar": strings.Repeat("a", 64) + "too-long-value", 501 "/invalid-key": "foo", 502 }, 503 annotations: map[string]string{ 504 "/invalid-key": "foo", 505 }, 506 expectErr: true, 507 }, 508 } 509 510 for _, tt := range tests { 511 t.Run(tt.name, func(t *testing.T) { 512 g := NewWithT(t) 513 ms := &clusterv1.MachineSet{ 514 Spec: clusterv1.MachineSetSpec{ 515 Template: clusterv1.MachineTemplateSpec{ 516 ObjectMeta: clusterv1.ObjectMeta{ 517 Labels: tt.labels, 518 Annotations: tt.annotations, 519 }, 520 }, 521 }, 522 } 523 524 webhook := &MachineSet{} 525 526 if tt.expectErr { 527 warnings, err := webhook.ValidateCreate(ctx, ms) 528 g.Expect(err).To(HaveOccurred()) 529 g.Expect(warnings).To(BeEmpty()) 530 warnings, err = webhook.ValidateUpdate(ctx, ms, ms) 531 g.Expect(err).To(HaveOccurred()) 532 g.Expect(warnings).To(BeEmpty()) 533 } else { 534 warnings, err := webhook.ValidateCreate(ctx, ms) 535 g.Expect(err).ToNot(HaveOccurred()) 536 g.Expect(warnings).To(BeEmpty()) 537 warnings, err = webhook.ValidateUpdate(ctx, ms, ms) 538 g.Expect(err).ToNot(HaveOccurred()) 539 g.Expect(warnings).To(BeEmpty()) 540 } 541 }) 542 } 543 }