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  }