sigs.k8s.io/cluster-api@v1.7.1/controlplane/kubeadm/internal/webhooks/kubeadm_control_plane_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 "strings" 21 "testing" 22 "time" 23 24 . "github.com/onsi/gomega" 25 corev1 "k8s.io/api/core/v1" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/util/intstr" 28 utilfeature "k8s.io/component-base/featuregate/testing" 29 "k8s.io/utils/ptr" 30 ctrl "sigs.k8s.io/controller-runtime" 31 32 bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1" 33 controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" 34 "sigs.k8s.io/cluster-api/feature" 35 "sigs.k8s.io/cluster-api/internal/webhooks/util" 36 ) 37 38 var ( 39 invalidNamespaceName = "bar" 40 ctx = ctrl.SetupSignalHandler() 41 ) 42 43 func TestKubeadmControlPlaneDefault(t *testing.T) { 44 g := NewWithT(t) 45 46 kcp := &controlplanev1.KubeadmControlPlane{ 47 ObjectMeta: metav1.ObjectMeta{ 48 Namespace: "foo", 49 }, 50 Spec: controlplanev1.KubeadmControlPlaneSpec{ 51 Version: "v1.18.3", 52 MachineTemplate: controlplanev1.KubeadmControlPlaneMachineTemplate{ 53 InfrastructureRef: corev1.ObjectReference{ 54 APIVersion: "test/v1alpha1", 55 Kind: "UnknownInfraMachine", 56 Name: "foo", 57 }, 58 }, 59 RolloutStrategy: &controlplanev1.RolloutStrategy{}, 60 }, 61 } 62 updateDefaultingValidationKCP := kcp.DeepCopy() 63 updateDefaultingValidationKCP.Spec.Version = "v1.18.3" 64 updateDefaultingValidationKCP.Spec.MachineTemplate.InfrastructureRef = corev1.ObjectReference{ 65 APIVersion: "test/v1alpha1", 66 Kind: "UnknownInfraMachine", 67 Name: "foo", 68 Namespace: "foo", 69 } 70 webhook := &KubeadmControlPlane{} 71 t.Run("for KubeadmControlPlane", util.CustomDefaultValidateTest(ctx, updateDefaultingValidationKCP, webhook)) 72 g.Expect(webhook.Default(ctx, kcp)).To(Succeed()) 73 74 g.Expect(kcp.Spec.KubeadmConfigSpec.Format).To(Equal(bootstrapv1.CloudConfig)) 75 g.Expect(kcp.Spec.MachineTemplate.InfrastructureRef.Namespace).To(Equal(kcp.Namespace)) 76 g.Expect(kcp.Spec.Version).To(Equal("v1.18.3")) 77 g.Expect(kcp.Spec.RolloutStrategy.Type).To(Equal(controlplanev1.RollingUpdateStrategyType)) 78 g.Expect(kcp.Spec.RolloutStrategy.RollingUpdate.MaxSurge.IntVal).To(Equal(int32(1))) 79 } 80 81 func TestKubeadmControlPlaneValidateCreate(t *testing.T) { 82 valid := &controlplanev1.KubeadmControlPlane{ 83 ObjectMeta: metav1.ObjectMeta{ 84 Name: "test", 85 Namespace: "foo", 86 }, 87 Spec: controlplanev1.KubeadmControlPlaneSpec{ 88 MachineTemplate: controlplanev1.KubeadmControlPlaneMachineTemplate{ 89 InfrastructureRef: corev1.ObjectReference{ 90 APIVersion: "test/v1alpha1", 91 Kind: "UnknownInfraMachine", 92 Namespace: "foo", 93 Name: "infraTemplate", 94 }, 95 }, 96 KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{ 97 ClusterConfiguration: &bootstrapv1.ClusterConfiguration{}, 98 }, 99 Replicas: ptr.To[int32](1), 100 Version: "v1.19.0", 101 RolloutStrategy: &controlplanev1.RolloutStrategy{ 102 Type: controlplanev1.RollingUpdateStrategyType, 103 RollingUpdate: &controlplanev1.RollingUpdate{ 104 MaxSurge: &intstr.IntOrString{ 105 IntVal: 1, 106 }, 107 }, 108 }, 109 }, 110 } 111 112 invalidMaxSurge := valid.DeepCopy() 113 invalidMaxSurge.Spec.RolloutStrategy.RollingUpdate.MaxSurge.IntVal = int32(3) 114 115 stringMaxSurge := valid.DeepCopy() 116 val := intstr.FromString("1") 117 stringMaxSurge.Spec.RolloutStrategy.RollingUpdate.MaxSurge = &val 118 119 invalidNamespace := valid.DeepCopy() 120 invalidNamespace.Spec.MachineTemplate.InfrastructureRef.Namespace = invalidNamespaceName 121 122 missingReplicas := valid.DeepCopy() 123 missingReplicas.Spec.Replicas = nil 124 125 zeroReplicas := valid.DeepCopy() 126 zeroReplicas.Spec.Replicas = ptr.To[int32](0) 127 128 evenReplicas := valid.DeepCopy() 129 evenReplicas.Spec.Replicas = ptr.To[int32](2) 130 131 evenReplicasExternalEtcd := evenReplicas.DeepCopy() 132 evenReplicasExternalEtcd.Spec.KubeadmConfigSpec = bootstrapv1.KubeadmConfigSpec{ 133 ClusterConfiguration: &bootstrapv1.ClusterConfiguration{ 134 Etcd: bootstrapv1.Etcd{ 135 External: &bootstrapv1.ExternalEtcd{}, 136 }, 137 }, 138 } 139 140 validVersion := valid.DeepCopy() 141 validVersion.Spec.Version = "v1.16.6" 142 143 invalidVersion1 := valid.DeepCopy() 144 invalidVersion1.Spec.Version = "vv1.16.6" 145 146 invalidVersion2 := valid.DeepCopy() 147 invalidVersion2.Spec.Version = "1.16.6" 148 149 invalidCoreDNSVersion := valid.DeepCopy() 150 invalidCoreDNSVersion.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS.ImageTag = "v1.7" // not a valid semantic version 151 152 invalidRolloutBeforeCertificateExpiryDays := valid.DeepCopy() 153 invalidRolloutBeforeCertificateExpiryDays.Spec.RolloutBefore = &controlplanev1.RolloutBefore{ 154 CertificatesExpiryDays: ptr.To[int32](5), // less than minimum 155 } 156 157 invalidIgnitionConfiguration := valid.DeepCopy() 158 invalidIgnitionConfiguration.Spec.KubeadmConfigSpec.Ignition = &bootstrapv1.IgnitionSpec{} 159 160 validIgnitionConfiguration := valid.DeepCopy() 161 validIgnitionConfiguration.Spec.KubeadmConfigSpec.Format = bootstrapv1.Ignition 162 validIgnitionConfiguration.Spec.KubeadmConfigSpec.Ignition = &bootstrapv1.IgnitionSpec{} 163 164 invalidMetadata := valid.DeepCopy() 165 invalidMetadata.Spec.MachineTemplate.ObjectMeta.Labels = map[string]string{ 166 "foo": "$invalid-key", 167 "bar": strings.Repeat("a", 64) + "too-long-value", 168 "/invalid-key": "foo", 169 } 170 invalidMetadata.Spec.MachineTemplate.ObjectMeta.Annotations = map[string]string{ 171 "/invalid-key": "foo", 172 } 173 174 tests := []struct { 175 name string 176 enableIgnitionFeature bool 177 expectErr bool 178 kcp *controlplanev1.KubeadmControlPlane 179 }{ 180 { 181 name: "should succeed when given a valid config", 182 expectErr: false, 183 kcp: valid, 184 }, 185 { 186 name: "should return error when kubeadmControlPlane namespace and infrastructureTemplate namespace mismatch", 187 expectErr: true, 188 kcp: invalidNamespace, 189 }, 190 { 191 name: "should return error when replicas is nil", 192 expectErr: true, 193 kcp: missingReplicas, 194 }, 195 { 196 name: "should return error when replicas is zero", 197 expectErr: true, 198 kcp: zeroReplicas, 199 }, 200 { 201 name: "should return error when replicas is even", 202 expectErr: true, 203 kcp: evenReplicas, 204 }, 205 { 206 name: "should allow even replicas when using external etcd", 207 expectErr: false, 208 kcp: evenReplicasExternalEtcd, 209 }, 210 { 211 name: "should succeed when given a valid semantic version with prepended 'v'", 212 expectErr: false, 213 kcp: validVersion, 214 }, 215 { 216 name: "should error when given a valid semantic version without 'v'", 217 expectErr: true, 218 kcp: invalidVersion2, 219 }, 220 { 221 name: "should return error when given an invalid semantic version", 222 expectErr: true, 223 kcp: invalidVersion1, 224 }, 225 { 226 name: "should return error when given an invalid semantic CoreDNS version", 227 expectErr: true, 228 kcp: invalidCoreDNSVersion, 229 }, 230 { 231 name: "should return error when maxSurge is not 1", 232 expectErr: true, 233 kcp: invalidMaxSurge, 234 }, 235 { 236 name: "should succeed when maxSurge is a string", 237 expectErr: false, 238 kcp: stringMaxSurge, 239 }, 240 { 241 name: "should return error when given an invalid rolloutBefore.certificatesExpiryDays value", 242 expectErr: true, 243 kcp: invalidRolloutBeforeCertificateExpiryDays, 244 }, 245 246 { 247 name: "should return error when Ignition configuration is invalid", 248 enableIgnitionFeature: true, 249 expectErr: true, 250 kcp: invalidIgnitionConfiguration, 251 }, 252 { 253 name: "should succeed when Ignition configuration is valid", 254 enableIgnitionFeature: true, 255 expectErr: false, 256 kcp: validIgnitionConfiguration, 257 }, 258 { 259 name: "should return error for invalid metadata", 260 enableIgnitionFeature: true, 261 expectErr: true, 262 kcp: invalidMetadata, 263 }, 264 } 265 266 for _, tt := range tests { 267 t.Run(tt.name, func(t *testing.T) { 268 if tt.enableIgnitionFeature { 269 // NOTE: KubeadmBootstrapFormatIgnition feature flag is disabled by default. 270 // Enabling the feature flag temporarily for this test. 271 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.KubeadmBootstrapFormatIgnition, true)() 272 } 273 274 g := NewWithT(t) 275 276 webhook := &KubeadmControlPlane{} 277 278 warnings, err := webhook.ValidateCreate(ctx, tt.kcp) 279 if tt.expectErr { 280 g.Expect(err).To(HaveOccurred()) 281 } else { 282 g.Expect(err).ToNot(HaveOccurred()) 283 } 284 g.Expect(warnings).To(BeEmpty()) 285 }) 286 } 287 } 288 289 func TestKubeadmControlPlaneValidateUpdate(t *testing.T) { 290 before := &controlplanev1.KubeadmControlPlane{ 291 ObjectMeta: metav1.ObjectMeta{ 292 Name: "test", 293 Namespace: "foo", 294 }, 295 Spec: controlplanev1.KubeadmControlPlaneSpec{ 296 MachineTemplate: controlplanev1.KubeadmControlPlaneMachineTemplate{ 297 InfrastructureRef: corev1.ObjectReference{ 298 APIVersion: "test/v1alpha1", 299 Kind: "UnknownInfraMachine", 300 Namespace: "foo", 301 Name: "infraTemplate", 302 }, 303 NodeDrainTimeout: &metav1.Duration{Duration: time.Second}, 304 NodeVolumeDetachTimeout: &metav1.Duration{Duration: time.Second}, 305 NodeDeletionTimeout: &metav1.Duration{Duration: time.Second}, 306 }, 307 Replicas: ptr.To[int32](1), 308 RolloutStrategy: &controlplanev1.RolloutStrategy{ 309 Type: controlplanev1.RollingUpdateStrategyType, 310 RollingUpdate: &controlplanev1.RollingUpdate{ 311 MaxSurge: &intstr.IntOrString{ 312 IntVal: 1, 313 }, 314 }, 315 }, 316 KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{ 317 InitConfiguration: &bootstrapv1.InitConfiguration{ 318 LocalAPIEndpoint: bootstrapv1.APIEndpoint{ 319 AdvertiseAddress: "127.0.0.1", 320 BindPort: int32(443), 321 }, 322 NodeRegistration: bootstrapv1.NodeRegistrationOptions{ 323 Name: "test", 324 }, 325 }, 326 ClusterConfiguration: &bootstrapv1.ClusterConfiguration{ 327 ClusterName: "test", 328 DNS: bootstrapv1.DNS{ 329 ImageMeta: bootstrapv1.ImageMeta{ 330 ImageRepository: "registry.k8s.io/coredns", 331 ImageTag: "1.6.5", 332 }, 333 }, 334 }, 335 JoinConfiguration: &bootstrapv1.JoinConfiguration{ 336 Discovery: bootstrapv1.Discovery{ 337 Timeout: &metav1.Duration{ 338 Duration: 10 * time.Minute, 339 }, 340 }, 341 NodeRegistration: bootstrapv1.NodeRegistrationOptions{ 342 Name: "test", 343 }, 344 }, 345 PreKubeadmCommands: []string{ 346 "test", "foo", 347 }, 348 PostKubeadmCommands: []string{ 349 "test", "foo", 350 }, 351 Files: []bootstrapv1.File{ 352 { 353 Path: "test", 354 }, 355 }, 356 Users: []bootstrapv1.User{ 357 { 358 Name: "user", 359 SSHAuthorizedKeys: []string{ 360 "ssh-rsa foo", 361 }, 362 }, 363 }, 364 NTP: &bootstrapv1.NTP{ 365 Servers: []string{"test-server-1", "test-server-2"}, 366 Enabled: ptr.To(true), 367 }, 368 }, 369 Version: "v1.16.6", 370 RolloutBefore: &controlplanev1.RolloutBefore{ 371 CertificatesExpiryDays: ptr.To[int32](7), 372 }, 373 }, 374 } 375 376 updateMaxSurgeVal := before.DeepCopy() 377 updateMaxSurgeVal.Spec.RolloutStrategy.RollingUpdate.MaxSurge.IntVal = int32(0) 378 updateMaxSurgeVal.Spec.Replicas = ptr.To[int32](3) 379 380 wrongReplicaCountForScaleIn := before.DeepCopy() 381 wrongReplicaCountForScaleIn.Spec.RolloutStrategy.RollingUpdate.MaxSurge.IntVal = int32(0) 382 383 validUpdateKubeadmConfigInit := before.DeepCopy() 384 validUpdateKubeadmConfigInit.Spec.KubeadmConfigSpec.InitConfiguration.NodeRegistration = bootstrapv1.NodeRegistrationOptions{} 385 386 invalidUpdateKubeadmConfigCluster := before.DeepCopy() 387 invalidUpdateKubeadmConfigCluster.Spec.KubeadmConfigSpec.ClusterConfiguration = &bootstrapv1.ClusterConfiguration{} 388 389 validUpdateKubeadmConfigJoin := before.DeepCopy() 390 validUpdateKubeadmConfigJoin.Spec.KubeadmConfigSpec.JoinConfiguration.NodeRegistration = bootstrapv1.NodeRegistrationOptions{} 391 392 beforeKubeadmConfigFormatSet := before.DeepCopy() 393 beforeKubeadmConfigFormatSet.Spec.KubeadmConfigSpec.Format = bootstrapv1.CloudConfig 394 invalidUpdateKubeadmConfigFormat := beforeKubeadmConfigFormatSet.DeepCopy() 395 invalidUpdateKubeadmConfigFormat.Spec.KubeadmConfigSpec.Format = bootstrapv1.Ignition 396 397 validUpdate := before.DeepCopy() 398 validUpdate.Labels = map[string]string{"blue": "green"} 399 validUpdate.Spec.KubeadmConfigSpec.PreKubeadmCommands = []string{"ab", "abc"} 400 validUpdate.Spec.KubeadmConfigSpec.PostKubeadmCommands = []string{"ab", "abc"} 401 validUpdate.Spec.KubeadmConfigSpec.Files = []bootstrapv1.File{ 402 { 403 Path: "ab", 404 }, 405 { 406 Path: "abc", 407 }, 408 } 409 validUpdate.Spec.Version = "v1.17.1" 410 validUpdate.Spec.KubeadmConfigSpec.Users = []bootstrapv1.User{ 411 { 412 Name: "bar", 413 SSHAuthorizedKeys: []string{ 414 "ssh-rsa bar", 415 "ssh-rsa foo", 416 }, 417 }, 418 } 419 validUpdate.Spec.MachineTemplate.ObjectMeta.Labels = map[string]string{ 420 "label": "labelValue", 421 } 422 validUpdate.Spec.MachineTemplate.ObjectMeta.Annotations = map[string]string{ 423 "annotation": "labelAnnotation", 424 } 425 validUpdate.Spec.MachineTemplate.InfrastructureRef.APIVersion = "test/v1alpha2" 426 validUpdate.Spec.MachineTemplate.InfrastructureRef.Name = "orange" 427 validUpdate.Spec.MachineTemplate.NodeDrainTimeout = &metav1.Duration{Duration: 10 * time.Second} 428 validUpdate.Spec.MachineTemplate.NodeVolumeDetachTimeout = &metav1.Duration{Duration: 10 * time.Second} 429 validUpdate.Spec.MachineTemplate.NodeDeletionTimeout = &metav1.Duration{Duration: 10 * time.Second} 430 validUpdate.Spec.Replicas = ptr.To[int32](5) 431 now := metav1.NewTime(time.Now()) 432 validUpdate.Spec.RolloutAfter = &now 433 validUpdate.Spec.RolloutBefore = &controlplanev1.RolloutBefore{ 434 CertificatesExpiryDays: ptr.To[int32](14), 435 } 436 validUpdate.Spec.RemediationStrategy = &controlplanev1.RemediationStrategy{ 437 MaxRetry: ptr.To[int32](50), 438 MinHealthyPeriod: &metav1.Duration{Duration: 10 * time.Hour}, 439 RetryPeriod: metav1.Duration{Duration: 10 * time.Minute}, 440 } 441 validUpdate.Spec.KubeadmConfigSpec.Format = bootstrapv1.CloudConfig 442 443 scaleToZero := before.DeepCopy() 444 scaleToZero.Spec.Replicas = ptr.To[int32](0) 445 446 scaleToEven := before.DeepCopy() 447 scaleToEven.Spec.Replicas = ptr.To[int32](2) 448 449 invalidNamespace := before.DeepCopy() 450 invalidNamespace.Spec.MachineTemplate.InfrastructureRef.Namespace = invalidNamespaceName 451 452 missingReplicas := before.DeepCopy() 453 missingReplicas.Spec.Replicas = nil 454 455 etcdLocalImageTag := before.DeepCopy() 456 etcdLocalImageTag.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local = &bootstrapv1.LocalEtcd{ 457 ImageMeta: bootstrapv1.ImageMeta{ 458 ImageTag: "v9.1.1", 459 }, 460 } 461 462 etcdLocalImageBuildTag := before.DeepCopy() 463 etcdLocalImageBuildTag.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local = &bootstrapv1.LocalEtcd{ 464 ImageMeta: bootstrapv1.ImageMeta{ 465 ImageTag: "v9.1.1_validBuild1", 466 }, 467 } 468 469 etcdLocalImageInvalidTag := before.DeepCopy() 470 etcdLocalImageInvalidTag.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local = &bootstrapv1.LocalEtcd{ 471 ImageMeta: bootstrapv1.ImageMeta{ 472 ImageTag: "v9.1.1+invalidBuild1", 473 }, 474 } 475 476 unsetEtcd := etcdLocalImageTag.DeepCopy() 477 unsetEtcd.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local = nil 478 479 networking := before.DeepCopy() 480 networking.Spec.KubeadmConfigSpec.ClusterConfiguration.Networking.DNSDomain = "some dns domain" 481 482 kubernetesVersion := before.DeepCopy() 483 kubernetesVersion.Spec.KubeadmConfigSpec.ClusterConfiguration.KubernetesVersion = "some kubernetes version" 484 485 controlPlaneEndpoint := before.DeepCopy() 486 controlPlaneEndpoint.Spec.KubeadmConfigSpec.ClusterConfiguration.ControlPlaneEndpoint = "some control plane endpoint" 487 488 apiServer := before.DeepCopy() 489 apiServer.Spec.KubeadmConfigSpec.ClusterConfiguration.APIServer = bootstrapv1.APIServer{ 490 ControlPlaneComponent: bootstrapv1.ControlPlaneComponent{ 491 ExtraArgs: map[string]string{"foo": "bar"}, 492 ExtraVolumes: []bootstrapv1.HostPathMount{{Name: "mount1"}}, 493 }, 494 TimeoutForControlPlane: &metav1.Duration{Duration: 5 * time.Minute}, 495 CertSANs: []string{"foo", "bar"}, 496 } 497 498 controllerManager := before.DeepCopy() 499 controllerManager.Spec.KubeadmConfigSpec.ClusterConfiguration.ControllerManager = bootstrapv1.ControlPlaneComponent{ 500 ExtraArgs: map[string]string{"controller manager field": "controller manager value"}, 501 ExtraVolumes: []bootstrapv1.HostPathMount{{Name: "mount", HostPath: "/foo", MountPath: "bar", ReadOnly: true, PathType: "File"}}, 502 } 503 504 scheduler := before.DeepCopy() 505 scheduler.Spec.KubeadmConfigSpec.ClusterConfiguration.Scheduler = bootstrapv1.ControlPlaneComponent{ 506 ExtraArgs: map[string]string{"scheduler field": "scheduler value"}, 507 ExtraVolumes: []bootstrapv1.HostPathMount{{Name: "mount", HostPath: "/foo", MountPath: "bar", ReadOnly: true, PathType: "File"}}, 508 } 509 510 dns := before.DeepCopy() 511 dns.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS = bootstrapv1.DNS{ 512 ImageMeta: bootstrapv1.ImageMeta{ 513 ImageRepository: "gcr.io/capi-test", 514 ImageTag: "v1.6.6_foobar.1", 515 }, 516 } 517 518 dnsBuildTag := before.DeepCopy() 519 dnsBuildTag.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS = bootstrapv1.DNS{ 520 ImageMeta: bootstrapv1.ImageMeta{ 521 ImageRepository: "gcr.io/capi-test", 522 ImageTag: "1.6.7", 523 }, 524 } 525 526 dnsInvalidTag := before.DeepCopy() 527 dnsInvalidTag.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS = bootstrapv1.DNS{ 528 ImageMeta: bootstrapv1.ImageMeta{ 529 ImageRepository: "gcr.io/capi-test", 530 ImageTag: "v0.20.0+invalidBuild1", 531 }, 532 } 533 534 dnsInvalidCoreDNSToVersion := dns.DeepCopy() 535 dnsInvalidCoreDNSToVersion.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS = bootstrapv1.DNS{ 536 ImageMeta: bootstrapv1.ImageMeta{ 537 ImageRepository: "gcr.io/capi-test", 538 ImageTag: "1.6.5", 539 }, 540 } 541 542 validCoreDNSCustomToVersion := dns.DeepCopy() 543 validCoreDNSCustomToVersion.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS = bootstrapv1.DNS{ 544 ImageMeta: bootstrapv1.ImageMeta{ 545 ImageRepository: "gcr.io/capi-test", 546 ImageTag: "v1.6.6_foobar.2", 547 }, 548 } 549 validUnsupportedCoreDNSVersion := dns.DeepCopy() 550 validUnsupportedCoreDNSVersion.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS = bootstrapv1.DNS{ 551 ImageMeta: bootstrapv1.ImageMeta{ 552 ImageRepository: "gcr.io/capi-test", 553 ImageTag: "v99.99.99", 554 }, 555 } 556 557 unsetCoreDNSToVersion := dns.DeepCopy() 558 unsetCoreDNSToVersion.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS = bootstrapv1.DNS{ 559 ImageMeta: bootstrapv1.ImageMeta{ 560 ImageRepository: "", 561 ImageTag: "", 562 }, 563 } 564 565 certificatesDir := before.DeepCopy() 566 certificatesDir.Spec.KubeadmConfigSpec.ClusterConfiguration.CertificatesDir = "a new certificates directory" 567 568 imageRepository := before.DeepCopy() 569 imageRepository.Spec.KubeadmConfigSpec.ClusterConfiguration.ImageRepository = "a new image repository" 570 571 featureGates := before.DeepCopy() 572 featureGates.Spec.KubeadmConfigSpec.ClusterConfiguration.FeatureGates = map[string]bool{"a feature gate": true} 573 574 externalEtcd := before.DeepCopy() 575 externalEtcd.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.External = &bootstrapv1.ExternalEtcd{ 576 KeyFile: "some key file", 577 } 578 579 localDataDir := before.DeepCopy() 580 localDataDir.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local = &bootstrapv1.LocalEtcd{ 581 DataDir: "some local data dir", 582 } 583 584 localPeerCertSANs := before.DeepCopy() 585 localPeerCertSANs.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local = &bootstrapv1.LocalEtcd{ 586 PeerCertSANs: []string{"a cert"}, 587 } 588 589 localServerCertSANs := before.DeepCopy() 590 localServerCertSANs.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local = &bootstrapv1.LocalEtcd{ 591 ServerCertSANs: []string{"a cert"}, 592 } 593 594 localExtraArgs := before.DeepCopy() 595 localExtraArgs.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local = &bootstrapv1.LocalEtcd{ 596 ExtraArgs: map[string]string{"an arg": "a value"}, 597 } 598 599 beforeExternalEtcdCluster := before.DeepCopy() 600 beforeExternalEtcdCluster.Spec.KubeadmConfigSpec.ClusterConfiguration = &bootstrapv1.ClusterConfiguration{ 601 Etcd: bootstrapv1.Etcd{ 602 External: &bootstrapv1.ExternalEtcd{ 603 Endpoints: []string{"127.0.0.1"}, 604 }, 605 }, 606 } 607 scaleToEvenExternalEtcdCluster := beforeExternalEtcdCluster.DeepCopy() 608 scaleToEvenExternalEtcdCluster.Spec.Replicas = ptr.To[int32](2) 609 610 beforeInvalidEtcdCluster := before.DeepCopy() 611 beforeInvalidEtcdCluster.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd = bootstrapv1.Etcd{ 612 Local: &bootstrapv1.LocalEtcd{ 613 ImageMeta: bootstrapv1.ImageMeta{ 614 ImageRepository: "image-repository", 615 ImageTag: "latest", 616 }, 617 }, 618 } 619 620 afterInvalidEtcdCluster := beforeInvalidEtcdCluster.DeepCopy() 621 afterInvalidEtcdCluster.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd = bootstrapv1.Etcd{ 622 External: &bootstrapv1.ExternalEtcd{ 623 Endpoints: []string{"127.0.0.1"}, 624 }, 625 } 626 627 withoutClusterConfiguration := before.DeepCopy() 628 withoutClusterConfiguration.Spec.KubeadmConfigSpec.ClusterConfiguration = nil 629 630 afterEtcdLocalDirAddition := before.DeepCopy() 631 afterEtcdLocalDirAddition.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local = &bootstrapv1.LocalEtcd{ 632 DataDir: "/data", 633 } 634 635 updateNTPServers := before.DeepCopy() 636 updateNTPServers.Spec.KubeadmConfigSpec.NTP.Servers = []string{"new-server"} 637 638 disableNTPServers := before.DeepCopy() 639 disableNTPServers.Spec.KubeadmConfigSpec.NTP.Enabled = ptr.To(false) 640 641 invalidRolloutBeforeCertificateExpiryDays := before.DeepCopy() 642 invalidRolloutBeforeCertificateExpiryDays.Spec.RolloutBefore = &controlplanev1.RolloutBefore{ 643 CertificatesExpiryDays: ptr.To[int32](5), // less than minimum 644 } 645 646 unsetRolloutBefore := before.DeepCopy() 647 unsetRolloutBefore.Spec.RolloutBefore = nil 648 649 invalidIgnitionConfiguration := before.DeepCopy() 650 invalidIgnitionConfiguration.Spec.KubeadmConfigSpec.Ignition = &bootstrapv1.IgnitionSpec{} 651 652 validIgnitionConfigurationBefore := before.DeepCopy() 653 validIgnitionConfigurationBefore.Spec.KubeadmConfigSpec.Format = bootstrapv1.Ignition 654 validIgnitionConfigurationBefore.Spec.KubeadmConfigSpec.Ignition = &bootstrapv1.IgnitionSpec{ 655 ContainerLinuxConfig: &bootstrapv1.ContainerLinuxConfig{}, 656 } 657 658 validIgnitionConfigurationAfter := validIgnitionConfigurationBefore.DeepCopy() 659 validIgnitionConfigurationAfter.Spec.KubeadmConfigSpec.Ignition.ContainerLinuxConfig.AdditionalConfig = "foo: bar" 660 661 updateInitConfigurationPatches := before.DeepCopy() 662 updateInitConfigurationPatches.Spec.KubeadmConfigSpec.InitConfiguration.Patches = &bootstrapv1.Patches{ 663 Directory: "/tmp/patches", 664 } 665 666 updateJoinConfigurationPatches := before.DeepCopy() 667 updateJoinConfigurationPatches.Spec.KubeadmConfigSpec.InitConfiguration.Patches = &bootstrapv1.Patches{ 668 Directory: "/tmp/patches", 669 } 670 671 updateInitConfigurationSkipPhases := before.DeepCopy() 672 updateInitConfigurationSkipPhases.Spec.KubeadmConfigSpec.InitConfiguration.SkipPhases = []string{"addon/kube-proxy"} 673 674 updateJoinConfigurationSkipPhases := before.DeepCopy() 675 updateJoinConfigurationSkipPhases.Spec.KubeadmConfigSpec.JoinConfiguration.SkipPhases = []string{"addon/kube-proxy"} 676 677 updateDiskSetup := before.DeepCopy() 678 updateDiskSetup.Spec.KubeadmConfigSpec.DiskSetup = &bootstrapv1.DiskSetup{ 679 Filesystems: []bootstrapv1.Filesystem{ 680 { 681 Device: "/dev/sda", 682 Filesystem: "ext4", 683 }, 684 }, 685 } 686 687 switchFromCloudInitToIgnition := before.DeepCopy() 688 switchFromCloudInitToIgnition.Spec.KubeadmConfigSpec.Format = bootstrapv1.Ignition 689 switchFromCloudInitToIgnition.Spec.KubeadmConfigSpec.Mounts = []bootstrapv1.MountPoints{ 690 {"/var/lib/testdir", "/var/lib/etcd/data"}, 691 } 692 693 invalidMetadata := before.DeepCopy() 694 invalidMetadata.Spec.MachineTemplate.ObjectMeta.Labels = map[string]string{ 695 "foo": "$invalid-key", 696 "bar": strings.Repeat("a", 64) + "too-long-value", 697 "/invalid-key": "foo", 698 } 699 invalidMetadata.Spec.MachineTemplate.ObjectMeta.Annotations = map[string]string{ 700 "/invalid-key": "foo", 701 } 702 703 beforeUseExperimentalRetryJoin := before.DeepCopy() 704 beforeUseExperimentalRetryJoin.Spec.KubeadmConfigSpec.UseExperimentalRetryJoin = true //nolint:staticcheck 705 updateUseExperimentalRetryJoin := before.DeepCopy() 706 updateUseExperimentalRetryJoin.Spec.KubeadmConfigSpec.UseExperimentalRetryJoin = false //nolint:staticcheck 707 708 tests := []struct { 709 name string 710 enableIgnitionFeature bool 711 expectErr bool 712 before *controlplanev1.KubeadmControlPlane 713 kcp *controlplanev1.KubeadmControlPlane 714 }{ 715 { 716 name: "should succeed when given a valid config", 717 expectErr: false, 718 before: before, 719 kcp: validUpdate, 720 }, 721 { 722 name: "should not return an error when trying to mutate the kubeadmconfigspec initconfiguration noderegistration", 723 expectErr: false, 724 before: before, 725 kcp: validUpdateKubeadmConfigInit, 726 }, 727 { 728 name: "should return error when trying to mutate the kubeadmconfigspec clusterconfiguration", 729 expectErr: true, 730 before: before, 731 kcp: invalidUpdateKubeadmConfigCluster, 732 }, 733 { 734 name: "should not return an error when trying to mutate the kubeadmconfigspec joinconfiguration noderegistration", 735 expectErr: false, 736 before: before, 737 kcp: validUpdateKubeadmConfigJoin, 738 }, 739 { 740 name: "should return error when trying to mutate the kubeadmconfigspec format from cloud-config to ignition", 741 expectErr: true, 742 before: beforeKubeadmConfigFormatSet, 743 kcp: invalidUpdateKubeadmConfigFormat, 744 }, 745 { 746 name: "should return error when trying to scale to zero", 747 expectErr: true, 748 before: before, 749 kcp: scaleToZero, 750 }, 751 { 752 name: "should return error when trying to scale to an even number", 753 expectErr: true, 754 before: before, 755 kcp: scaleToEven, 756 }, 757 { 758 name: "should return error when trying to reference cross namespace", 759 expectErr: true, 760 before: before, 761 kcp: invalidNamespace, 762 }, 763 { 764 name: "should return error when trying to scale to nil", 765 expectErr: true, 766 before: before, 767 kcp: missingReplicas, 768 }, 769 { 770 name: "should succeed when trying to scale to an even number with external etcd defined in ClusterConfiguration", 771 expectErr: false, 772 before: beforeExternalEtcdCluster, 773 kcp: scaleToEvenExternalEtcdCluster, 774 }, 775 { 776 name: "should succeed when making a change to the local etcd image tag", 777 expectErr: false, 778 before: before, 779 kcp: etcdLocalImageTag, 780 }, 781 { 782 name: "should succeed when making a change to the local etcd image tag", 783 expectErr: false, 784 before: before, 785 kcp: etcdLocalImageBuildTag, 786 }, 787 { 788 name: "should fail when using an invalid etcd image tag", 789 expectErr: true, 790 before: before, 791 kcp: etcdLocalImageInvalidTag, 792 }, 793 { 794 name: "should fail when making a change to the cluster config's networking struct", 795 expectErr: true, 796 before: before, 797 kcp: networking, 798 }, 799 { 800 name: "should fail when making a change to the cluster config's kubernetes version", 801 expectErr: true, 802 before: before, 803 kcp: kubernetesVersion, 804 }, 805 { 806 name: "should fail when making a change to the cluster config's controlPlaneEndpoint", 807 expectErr: true, 808 before: before, 809 kcp: controlPlaneEndpoint, 810 }, 811 { 812 name: "should allow changes to the cluster config's apiServer", 813 expectErr: false, 814 before: before, 815 kcp: apiServer, 816 }, 817 { 818 name: "should allow changes to the cluster config's controllerManager", 819 expectErr: false, 820 before: before, 821 kcp: controllerManager, 822 }, 823 { 824 name: "should allow changes to the cluster config's scheduler", 825 expectErr: false, 826 before: before, 827 kcp: scheduler, 828 }, 829 { 830 name: "should succeed when making a change to the cluster config's dns", 831 expectErr: false, 832 before: before, 833 kcp: dns, 834 }, 835 { 836 name: "should succeed when changing to a valid custom CoreDNS version", 837 expectErr: false, 838 before: dns, 839 kcp: validCoreDNSCustomToVersion, 840 }, 841 { 842 name: "should succeed when CoreDNS ImageTag is unset", 843 expectErr: false, 844 before: dns, 845 kcp: unsetCoreDNSToVersion, 846 }, 847 { 848 name: "should succeed when using an valid DNS build", 849 expectErr: false, 850 before: before, 851 kcp: dnsBuildTag, 852 }, 853 { 854 name: "should succeed when using the same CoreDNS version", 855 before: dns, 856 kcp: dns.DeepCopy(), 857 }, 858 { 859 name: "should succeed when using the same CoreDNS version - not supported", 860 before: validUnsupportedCoreDNSVersion, 861 kcp: validUnsupportedCoreDNSVersion, 862 }, 863 { 864 name: "should fail when using an invalid DNS build", 865 expectErr: true, 866 before: before, 867 kcp: dnsInvalidTag, 868 }, 869 { 870 name: "should fail when using an invalid CoreDNS version", 871 expectErr: true, 872 before: dns, 873 kcp: dnsInvalidCoreDNSToVersion, 874 }, 875 876 { 877 name: "should fail when making a change to the cluster config's certificatesDir", 878 expectErr: true, 879 before: before, 880 kcp: certificatesDir, 881 }, 882 { 883 name: "should fail when making a change to the cluster config's imageRepository", 884 expectErr: false, 885 before: before, 886 kcp: imageRepository, 887 }, 888 { 889 name: "should succeed when making a change to the cluster config's featureGates", 890 expectErr: false, 891 before: before, 892 kcp: featureGates, 893 }, 894 { 895 name: "should succeed when making a change to the cluster config's local etcd's configuration localDataDir field", 896 expectErr: false, 897 before: before, 898 kcp: localDataDir, 899 }, 900 { 901 name: "should succeed when making a change to the cluster config's local etcd's configuration localPeerCertSANs field", 902 expectErr: false, 903 before: before, 904 kcp: localPeerCertSANs, 905 }, 906 { 907 name: "should succeed when making a change to the cluster config's local etcd's configuration localServerCertSANs field", 908 expectErr: false, 909 before: before, 910 kcp: localServerCertSANs, 911 }, 912 { 913 name: "should succeed when making a change to the cluster config's local etcd's configuration localExtraArgs field", 914 expectErr: false, 915 before: before, 916 kcp: localExtraArgs, 917 }, 918 { 919 name: "should succeed when making a change to the cluster config's external etcd's configuration", 920 expectErr: false, 921 before: before, 922 kcp: externalEtcd, 923 }, 924 { 925 name: "should fail when attempting to unset the etcd local object", 926 expectErr: true, 927 before: etcdLocalImageTag, 928 kcp: unsetEtcd, 929 }, 930 { 931 name: "should fail if both local and external etcd are set", 932 expectErr: true, 933 before: beforeInvalidEtcdCluster, 934 kcp: afterInvalidEtcdCluster, 935 }, 936 { 937 name: "should pass if ClusterConfiguration is nil", 938 expectErr: false, 939 before: withoutClusterConfiguration, 940 kcp: withoutClusterConfiguration, 941 }, 942 { 943 name: "should fail if etcd local dir is changed from missing ClusterConfiguration", 944 expectErr: true, 945 before: withoutClusterConfiguration, 946 kcp: afterEtcdLocalDirAddition, 947 }, 948 { 949 name: "should not return an error when maxSurge value is updated to 0", 950 expectErr: false, 951 before: before, 952 kcp: updateMaxSurgeVal, 953 }, 954 { 955 name: "should return an error when maxSurge value is updated to 0, but replica count is < 3", 956 expectErr: true, 957 before: before, 958 kcp: wrongReplicaCountForScaleIn, 959 }, 960 { 961 name: "should pass if NTP servers are updated", 962 expectErr: false, 963 before: before, 964 kcp: updateNTPServers, 965 }, 966 { 967 name: "should pass if NTP servers is disabled during update", 968 expectErr: false, 969 before: before, 970 kcp: disableNTPServers, 971 }, 972 { 973 name: "should allow changes to initConfiguration.patches", 974 expectErr: false, 975 before: before, 976 kcp: updateInitConfigurationPatches, 977 }, 978 { 979 name: "should allow changes to joinConfiguration.patches", 980 expectErr: false, 981 before: before, 982 kcp: updateJoinConfigurationPatches, 983 }, 984 { 985 name: "should allow changes to initConfiguration.skipPhases", 986 expectErr: false, 987 before: before, 988 kcp: updateInitConfigurationSkipPhases, 989 }, 990 { 991 name: "should allow changes to joinConfiguration.skipPhases", 992 expectErr: false, 993 before: before, 994 kcp: updateJoinConfigurationSkipPhases, 995 }, 996 { 997 name: "should allow changes to diskSetup", 998 expectErr: false, 999 before: before, 1000 kcp: updateDiskSetup, 1001 }, 1002 { 1003 name: "should return error when rolloutBefore.certificatesExpiryDays is invalid", 1004 expectErr: true, 1005 before: before, 1006 kcp: invalidRolloutBeforeCertificateExpiryDays, 1007 }, 1008 { 1009 name: "should allow unsetting rolloutBefore", 1010 expectErr: false, 1011 before: before, 1012 kcp: unsetRolloutBefore, 1013 }, 1014 { 1015 name: "should return error when Ignition configuration is invalid", 1016 enableIgnitionFeature: true, 1017 expectErr: true, 1018 before: invalidIgnitionConfiguration, 1019 kcp: invalidIgnitionConfiguration, 1020 }, 1021 { 1022 name: "should succeed when Ignition configuration is modified", 1023 enableIgnitionFeature: true, 1024 expectErr: false, 1025 before: validIgnitionConfigurationBefore, 1026 kcp: validIgnitionConfigurationAfter, 1027 }, 1028 { 1029 name: "should succeed when CloudInit was used before", 1030 enableIgnitionFeature: true, 1031 expectErr: false, 1032 before: before, 1033 kcp: switchFromCloudInitToIgnition, 1034 }, 1035 { 1036 name: "should return error for invalid metadata", 1037 enableIgnitionFeature: true, 1038 expectErr: true, 1039 before: before, 1040 kcp: invalidMetadata, 1041 }, 1042 { 1043 name: "should allow changes to useExperimentalRetryJoin", 1044 expectErr: false, 1045 before: beforeUseExperimentalRetryJoin, 1046 kcp: updateUseExperimentalRetryJoin, 1047 }, 1048 } 1049 1050 for _, tt := range tests { 1051 t.Run(tt.name, func(t *testing.T) { 1052 if tt.enableIgnitionFeature { 1053 // NOTE: KubeadmBootstrapFormatIgnition feature flag is disabled by default. 1054 // Enabling the feature flag temporarily for this test. 1055 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.KubeadmBootstrapFormatIgnition, true)() 1056 } 1057 1058 g := NewWithT(t) 1059 1060 webhook := &KubeadmControlPlane{} 1061 1062 warnings, err := webhook.ValidateUpdate(ctx, tt.before.DeepCopy(), tt.kcp) 1063 if tt.expectErr { 1064 g.Expect(err).To(HaveOccurred()) 1065 } else { 1066 g.Expect(err).To(Succeed()) 1067 } 1068 g.Expect(warnings).To(BeEmpty()) 1069 }) 1070 } 1071 } 1072 1073 func TestValidateVersion(t *testing.T) { 1074 tests := []struct { 1075 name string 1076 clusterConfiguration *bootstrapv1.ClusterConfiguration 1077 oldVersion string 1078 newVersion string 1079 expectErr bool 1080 }{ 1081 // Basic validation of old and new version. 1082 { 1083 name: "error when old version is empty", 1084 oldVersion: "", 1085 newVersion: "v1.16.6", 1086 expectErr: true, 1087 }, 1088 { 1089 name: "error when old version is invalid", 1090 oldVersion: "invalid-version", 1091 newVersion: "v1.18.1", 1092 expectErr: true, 1093 }, 1094 { 1095 name: "error when new version is empty", 1096 oldVersion: "v1.16.6", 1097 newVersion: "", 1098 expectErr: true, 1099 }, 1100 { 1101 name: "error when new version is invalid", 1102 oldVersion: "v1.18.1", 1103 newVersion: "invalid-version", 1104 expectErr: true, 1105 }, 1106 // Validation that we block upgrade to v1.19.0. 1107 // Note: Upgrading to v1.19.0 is not supported, because of issues in v1.19.0, 1108 // see: https://github.com/kubernetes-sigs/cluster-api/issues/3564 1109 { 1110 name: "error when upgrading to v1.19.0", 1111 oldVersion: "v1.18.8", 1112 newVersion: "v1.19.0", 1113 expectErr: true, 1114 }, 1115 { 1116 name: "pass when both versions are v1.19.0", 1117 oldVersion: "v1.19.0", 1118 newVersion: "v1.19.0", 1119 expectErr: false, 1120 }, 1121 // Validation for skip-level upgrades. 1122 { 1123 name: "error when upgrading two minor versions", 1124 oldVersion: "v1.18.8", 1125 newVersion: "v1.20.0-alpha.0.734_ba502ee555924a", 1126 expectErr: true, 1127 }, 1128 { 1129 name: "pass when upgrading one minor version", 1130 oldVersion: "v1.20.1", 1131 newVersion: "v1.21.18", 1132 expectErr: false, 1133 }, 1134 // Validation for usage of the old registry. 1135 // Notes: 1136 // * kubeadm versions < v1.22 are always using the old registry. 1137 // * kubeadm versions >= v1.25.0 are always using the new registry. 1138 // * kubeadm versions in between are using the new registry 1139 // starting with certain patch versions. 1140 // This test validates that we don't block upgrades for < v1.22.0 and >= v1.25.0 1141 // and block upgrades to kubeadm versions in between with the old registry. 1142 { 1143 name: "pass when imageRepository is set", 1144 clusterConfiguration: &bootstrapv1.ClusterConfiguration{ 1145 ImageRepository: "k8s.gcr.io", 1146 }, 1147 oldVersion: "v1.21.1", 1148 newVersion: "v1.22.16", 1149 expectErr: false, 1150 }, 1151 { 1152 name: "pass when version didn't change", 1153 oldVersion: "v1.22.16", 1154 newVersion: "v1.22.16", 1155 expectErr: false, 1156 }, 1157 { 1158 name: "pass when new version is < v1.22.0", 1159 oldVersion: "v1.20.10", 1160 newVersion: "v1.21.5", 1161 expectErr: false, 1162 }, 1163 { 1164 name: "error when new version is using old registry (v1.22.0 <= version <= v1.22.16)", 1165 oldVersion: "v1.21.1", 1166 newVersion: "v1.22.16", // last patch release using old registry 1167 expectErr: true, 1168 }, 1169 { 1170 name: "pass when new version is using new registry (>= v1.22.17)", 1171 oldVersion: "v1.21.1", 1172 newVersion: "v1.22.17", // first patch release using new registry 1173 expectErr: false, 1174 }, 1175 { 1176 name: "error when new version is using old registry (v1.23.0 <= version <= v1.23.14)", 1177 oldVersion: "v1.22.17", 1178 newVersion: "v1.23.14", // last patch release using old registry 1179 expectErr: true, 1180 }, 1181 { 1182 name: "pass when new version is using new registry (>= v1.23.15)", 1183 oldVersion: "v1.22.17", 1184 newVersion: "v1.23.15", // first patch release using new registry 1185 expectErr: false, 1186 }, 1187 { 1188 name: "error when new version is using old registry (v1.24.0 <= version <= v1.24.8)", 1189 oldVersion: "v1.23.1", 1190 newVersion: "v1.24.8", // last patch release using old registry 1191 expectErr: true, 1192 }, 1193 { 1194 name: "pass when new version is using new registry (>= v1.24.9)", 1195 oldVersion: "v1.23.1", 1196 newVersion: "v1.24.9", // first patch release using new registry 1197 expectErr: false, 1198 }, 1199 { 1200 name: "pass when new version is using new registry (>= v1.25.0)", 1201 oldVersion: "v1.24.8", 1202 newVersion: "v1.25.0", // uses new registry 1203 expectErr: false, 1204 }, 1205 } 1206 1207 for _, tt := range tests { 1208 t.Run(tt.name, func(t *testing.T) { 1209 g := NewWithT(t) 1210 1211 kcpNew := controlplanev1.KubeadmControlPlane{ 1212 Spec: controlplanev1.KubeadmControlPlaneSpec{ 1213 KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{ 1214 ClusterConfiguration: tt.clusterConfiguration, 1215 }, 1216 Version: tt.newVersion, 1217 }, 1218 } 1219 1220 kcpOld := controlplanev1.KubeadmControlPlane{ 1221 Spec: controlplanev1.KubeadmControlPlaneSpec{ 1222 KubeadmConfigSpec: bootstrapv1.KubeadmConfigSpec{ 1223 ClusterConfiguration: tt.clusterConfiguration, 1224 }, 1225 Version: tt.oldVersion, 1226 }, 1227 } 1228 1229 webhook := &KubeadmControlPlane{} 1230 1231 allErrs := webhook.validateVersion(&kcpOld, &kcpNew) 1232 if tt.expectErr { 1233 g.Expect(allErrs).ToNot(BeEmpty()) 1234 } else { 1235 g.Expect(allErrs).To(BeEmpty()) 1236 } 1237 }) 1238 } 1239 } 1240 func TestKubeadmControlPlaneValidateUpdateAfterDefaulting(t *testing.T) { 1241 g := NewWithT(t) 1242 1243 before := &controlplanev1.KubeadmControlPlane{ 1244 ObjectMeta: metav1.ObjectMeta{ 1245 Name: "test", 1246 Namespace: "foo", 1247 }, 1248 Spec: controlplanev1.KubeadmControlPlaneSpec{ 1249 Version: "v1.19.0", 1250 MachineTemplate: controlplanev1.KubeadmControlPlaneMachineTemplate{ 1251 InfrastructureRef: corev1.ObjectReference{ 1252 APIVersion: "test/v1alpha1", 1253 Kind: "UnknownInfraMachine", 1254 Namespace: "foo", 1255 Name: "infraTemplate", 1256 }, 1257 }, 1258 }, 1259 } 1260 1261 afterDefault := before.DeepCopy() 1262 webhook := &KubeadmControlPlane{} 1263 g.Expect(webhook.Default(ctx, afterDefault)).To(Succeed()) 1264 1265 tests := []struct { 1266 name string 1267 expectErr bool 1268 before *controlplanev1.KubeadmControlPlane 1269 kcp *controlplanev1.KubeadmControlPlane 1270 }{ 1271 { 1272 name: "update should succeed after defaulting", 1273 expectErr: false, 1274 before: before, 1275 kcp: afterDefault, 1276 }, 1277 } 1278 1279 for _, tt := range tests { 1280 t.Run(tt.name, func(t *testing.T) { 1281 g := NewWithT(t) 1282 1283 webhook := &KubeadmControlPlane{} 1284 1285 warnings, err := webhook.ValidateUpdate(ctx, tt.before.DeepCopy(), tt.kcp) 1286 if tt.expectErr { 1287 g.Expect(err).To(HaveOccurred()) 1288 } else { 1289 g.Expect(err).To(Succeed()) 1290 g.Expect(tt.kcp.Spec.MachineTemplate.InfrastructureRef.Namespace).To(Equal(tt.before.Namespace)) 1291 g.Expect(tt.kcp.Spec.Version).To(Equal("v1.19.0")) 1292 g.Expect(tt.kcp.Spec.RolloutStrategy.Type).To(Equal(controlplanev1.RollingUpdateStrategyType)) 1293 g.Expect(tt.kcp.Spec.RolloutStrategy.RollingUpdate.MaxSurge.IntVal).To(Equal(int32(1))) 1294 g.Expect(tt.kcp.Spec.Replicas).To(Equal(ptr.To[int32](1))) 1295 } 1296 g.Expect(warnings).To(BeEmpty()) 1297 }) 1298 } 1299 } 1300 1301 func TestPathsMatch(t *testing.T) { 1302 tests := []struct { 1303 name string 1304 allowed, path []string 1305 match bool 1306 }{ 1307 { 1308 name: "a simple match case", 1309 allowed: []string{"a", "b", "c"}, 1310 path: []string{"a", "b", "c"}, 1311 match: true, 1312 }, 1313 { 1314 name: "a case can't match", 1315 allowed: []string{"a", "b", "c"}, 1316 path: []string{"a"}, 1317 match: false, 1318 }, 1319 { 1320 name: "an empty path for whatever reason", 1321 allowed: []string{"a"}, 1322 path: []string{""}, 1323 match: false, 1324 }, 1325 { 1326 name: "empty allowed matches nothing", 1327 allowed: []string{}, 1328 path: []string{"a"}, 1329 match: false, 1330 }, 1331 { 1332 name: "wildcard match", 1333 allowed: []string{"a", "b", "c", "d", "*"}, 1334 path: []string{"a", "b", "c", "d", "e", "f", "g"}, 1335 match: true, 1336 }, 1337 { 1338 name: "long path", 1339 allowed: []string{"a"}, 1340 path: []string{"a", "b", "c", "d", "e", "f", "g"}, 1341 match: false, 1342 }, 1343 } 1344 for _, tt := range tests { 1345 t.Run(tt.name, func(t *testing.T) { 1346 g := NewWithT(t) 1347 g.Expect(pathsMatch(tt.allowed, tt.path)).To(Equal(tt.match)) 1348 }) 1349 } 1350 } 1351 1352 func TestAllowed(t *testing.T) { 1353 tests := []struct { 1354 name string 1355 allowList [][]string 1356 path []string 1357 match bool 1358 }{ 1359 { 1360 name: "matches the first and none of the others", 1361 allowList: [][]string{ 1362 {"a", "b", "c"}, 1363 {"b", "d", "x"}, 1364 }, 1365 path: []string{"a", "b", "c"}, 1366 match: true, 1367 }, 1368 { 1369 name: "matches none in the allow list", 1370 allowList: [][]string{ 1371 {"a", "b", "c"}, 1372 {"b", "c", "d"}, 1373 {"e", "*"}, 1374 }, 1375 path: []string{"a"}, 1376 match: false, 1377 }, 1378 { 1379 name: "an empty path matches nothing", 1380 allowList: [][]string{ 1381 {"a", "b", "c"}, 1382 {"*"}, 1383 {"b", "c"}, 1384 }, 1385 path: []string{}, 1386 match: false, 1387 }, 1388 { 1389 name: "empty allowList matches nothing", 1390 allowList: [][]string{}, 1391 path: []string{"a"}, 1392 match: false, 1393 }, 1394 { 1395 name: "length test check", 1396 allowList: [][]string{ 1397 {"a", "b", "c", "d", "e", "f"}, 1398 {"a", "b", "c", "d", "e", "f", "g", "h"}, 1399 }, 1400 path: []string{"a", "b", "c", "d", "e", "f", "g"}, 1401 match: false, 1402 }, 1403 } 1404 for _, tt := range tests { 1405 t.Run(tt.name, func(t *testing.T) { 1406 g := NewWithT(t) 1407 g.Expect(allowed(tt.allowList, tt.path)).To(Equal(tt.match)) 1408 }) 1409 } 1410 } 1411 1412 func TestPaths(t *testing.T) { 1413 tests := []struct { 1414 name string 1415 path []string 1416 diff map[string]interface{} 1417 expected [][]string 1418 }{ 1419 { 1420 name: "basic check", 1421 diff: map[string]interface{}{ 1422 "spec": map[string]interface{}{ 1423 "replicas": 4, 1424 "version": "1.17.3", 1425 "kubeadmConfigSpec": map[string]interface{}{ 1426 "clusterConfiguration": map[string]interface{}{ 1427 "version": "v2.0.1", 1428 }, 1429 "initConfiguration": map[string]interface{}{ 1430 "bootstrapToken": []string{"abcd", "defg"}, 1431 }, 1432 "joinConfiguration": nil, 1433 }, 1434 }, 1435 }, 1436 expected: [][]string{ 1437 {"spec", "replicas"}, 1438 {"spec", "version"}, 1439 {"spec", "kubeadmConfigSpec", "joinConfiguration"}, 1440 {"spec", "kubeadmConfigSpec", "clusterConfiguration", "version"}, 1441 {"spec", "kubeadmConfigSpec", "initConfiguration", "bootstrapToken"}, 1442 }, 1443 }, 1444 { 1445 name: "empty input makes for empty output", 1446 path: []string{"a"}, 1447 diff: map[string]interface{}{}, 1448 expected: [][]string{}, 1449 }, 1450 { 1451 name: "long recursive check with two keys", 1452 diff: map[string]interface{}{ 1453 "spec": map[string]interface{}{ 1454 "kubeadmConfigSpec": map[string]interface{}{ 1455 "clusterConfiguration": map[string]interface{}{ 1456 "version": "v2.0.1", 1457 "abc": "d", 1458 }, 1459 }, 1460 }, 1461 }, 1462 expected: [][]string{ 1463 {"spec", "kubeadmConfigSpec", "clusterConfiguration", "version"}, 1464 {"spec", "kubeadmConfigSpec", "clusterConfiguration", "abc"}, 1465 }, 1466 }, 1467 } 1468 for _, tt := range tests { 1469 t.Run(tt.name, func(t *testing.T) { 1470 g := NewWithT(t) 1471 g.Expect(paths(tt.path, tt.diff)).To(ConsistOf(tt.expected)) 1472 }) 1473 } 1474 }