sigs.k8s.io/cluster-api@v1.7.1/controlplane/kubeadm/internal/webhooks/kubeadm_control_plane.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package webhooks
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"strings"
    24  
    25  	"github.com/blang/semver/v4"
    26  	"github.com/coredns/corefile-migration/migration"
    27  	jsonpatch "github.com/evanphx/json-patch/v5"
    28  	"github.com/pkg/errors"
    29  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/util/intstr"
    32  	"k8s.io/apimachinery/pkg/util/validation/field"
    33  	ctrl "sigs.k8s.io/controller-runtime"
    34  	"sigs.k8s.io/controller-runtime/pkg/webhook"
    35  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    36  
    37  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    38  	bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
    39  	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
    40  	"sigs.k8s.io/cluster-api/internal/util/kubeadm"
    41  	"sigs.k8s.io/cluster-api/util/container"
    42  	"sigs.k8s.io/cluster-api/util/version"
    43  )
    44  
    45  func (webhook *KubeadmControlPlane) SetupWebhookWithManager(mgr ctrl.Manager) error {
    46  	return ctrl.NewWebhookManagedBy(mgr).
    47  		For(&controlplanev1.KubeadmControlPlane{}).
    48  		WithDefaulter(webhook).
    49  		WithValidator(webhook).
    50  		Complete()
    51  }
    52  
    53  // +kubebuilder:webhook:verbs=create;update,path=/mutate-controlplane-cluster-x-k8s-io-v1beta1-kubeadmcontrolplane,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=controlplane.cluster.x-k8s.io,resources=kubeadmcontrolplanes,versions=v1beta1,name=default.kubeadmcontrolplane.controlplane.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
    54  // +kubebuilder:webhook:verbs=create;update,path=/validate-controlplane-cluster-x-k8s-io-v1beta1-kubeadmcontrolplane,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=controlplane.cluster.x-k8s.io,resources=kubeadmcontrolplanes,versions=v1beta1,name=validation.kubeadmcontrolplane.controlplane.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
    55  
    56  // KubeadmControlPlane implements a validation and defaulting webhook for KubeadmControlPlane.
    57  type KubeadmControlPlane struct{}
    58  
    59  var _ webhook.CustomValidator = &KubeadmControlPlane{}
    60  var _ webhook.CustomDefaulter = &KubeadmControlPlane{}
    61  
    62  // Default implements webhook.Defaulter so a webhook will be registered for the type.
    63  func (webhook *KubeadmControlPlane) Default(_ context.Context, obj runtime.Object) error {
    64  	k, ok := obj.(*controlplanev1.KubeadmControlPlane)
    65  	if !ok {
    66  		return apierrors.NewBadRequest(fmt.Sprintf("expected a KubeadmControlPlane but got a %T", obj))
    67  	}
    68  
    69  	defaultKubeadmControlPlaneSpec(&k.Spec, k.Namespace)
    70  
    71  	return nil
    72  }
    73  
    74  func defaultKubeadmControlPlaneSpec(s *controlplanev1.KubeadmControlPlaneSpec, namespace string) {
    75  	if s.Replicas == nil {
    76  		replicas := int32(1)
    77  		s.Replicas = &replicas
    78  	}
    79  
    80  	if s.MachineTemplate.InfrastructureRef.Namespace == "" {
    81  		s.MachineTemplate.InfrastructureRef.Namespace = namespace
    82  	}
    83  
    84  	if !strings.HasPrefix(s.Version, "v") {
    85  		s.Version = "v" + s.Version
    86  	}
    87  
    88  	s.KubeadmConfigSpec.Default()
    89  
    90  	s.RolloutStrategy = defaultRolloutStrategy(s.RolloutStrategy)
    91  }
    92  
    93  func defaultRolloutStrategy(rolloutStrategy *controlplanev1.RolloutStrategy) *controlplanev1.RolloutStrategy {
    94  	ios1 := intstr.FromInt(1)
    95  
    96  	if rolloutStrategy == nil {
    97  		rolloutStrategy = &controlplanev1.RolloutStrategy{}
    98  	}
    99  
   100  	// Enforce RollingUpdate strategy and default MaxSurge if not set.
   101  	if rolloutStrategy != nil {
   102  		if len(rolloutStrategy.Type) == 0 {
   103  			rolloutStrategy.Type = controlplanev1.RollingUpdateStrategyType
   104  		}
   105  		if rolloutStrategy.Type == controlplanev1.RollingUpdateStrategyType {
   106  			if rolloutStrategy.RollingUpdate == nil {
   107  				rolloutStrategy.RollingUpdate = &controlplanev1.RollingUpdate{}
   108  			}
   109  			rolloutStrategy.RollingUpdate.MaxSurge = intstr.ValueOrDefault(rolloutStrategy.RollingUpdate.MaxSurge, ios1)
   110  		}
   111  	}
   112  
   113  	return rolloutStrategy
   114  }
   115  
   116  // ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
   117  func (webhook *KubeadmControlPlane) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
   118  	k, ok := obj.(*controlplanev1.KubeadmControlPlane)
   119  	if !ok {
   120  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a KubeadmControlPlane but got a %T", obj))
   121  	}
   122  
   123  	spec := k.Spec
   124  	allErrs := validateKubeadmControlPlaneSpec(spec, k.Namespace, field.NewPath("spec"))
   125  	allErrs = append(allErrs, validateClusterConfiguration(nil, spec.KubeadmConfigSpec.ClusterConfiguration, field.NewPath("spec", "kubeadmConfigSpec", "clusterConfiguration"))...)
   126  	allErrs = append(allErrs, spec.KubeadmConfigSpec.Validate(field.NewPath("spec", "kubeadmConfigSpec"))...)
   127  	if len(allErrs) > 0 {
   128  		return nil, apierrors.NewInvalid(clusterv1.GroupVersion.WithKind("KubeadmControlPlane").GroupKind(), k.Name, allErrs)
   129  	}
   130  	return nil, nil
   131  }
   132  
   133  const (
   134  	spec                 = "spec"
   135  	kubeadmConfigSpec    = "kubeadmConfigSpec"
   136  	clusterConfiguration = "clusterConfiguration"
   137  	initConfiguration    = "initConfiguration"
   138  	joinConfiguration    = "joinConfiguration"
   139  	nodeRegistration     = "nodeRegistration"
   140  	skipPhases           = "skipPhases"
   141  	patches              = "patches"
   142  	directory            = "directory"
   143  	preKubeadmCommands   = "preKubeadmCommands"
   144  	postKubeadmCommands  = "postKubeadmCommands"
   145  	files                = "files"
   146  	users                = "users"
   147  	apiServer            = "apiServer"
   148  	controllerManager    = "controllerManager"
   149  	scheduler            = "scheduler"
   150  	ntp                  = "ntp"
   151  	ignition             = "ignition"
   152  	diskSetup            = "diskSetup"
   153  	featureGates         = "featureGates"
   154  )
   155  
   156  const minimumCertificatesExpiryDays = 7
   157  
   158  // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
   159  func (webhook *KubeadmControlPlane) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
   160  	// add a * to indicate everything beneath is ok.
   161  	// For example, {"spec", "*"} will allow any path under "spec" to change.
   162  	allowedPaths := [][]string{
   163  		// metadata
   164  		{"metadata", "*"},
   165  		// spec.kubeadmConfigSpec.clusterConfiguration
   166  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "local", "imageRepository"},
   167  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "local", "imageTag"},
   168  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "local", "extraArgs"},
   169  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "local", "extraArgs", "*"},
   170  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "local", "dataDir"},
   171  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "local", "peerCertSANs"},
   172  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "local", "serverCertSANs"},
   173  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "external", "endpoints"},
   174  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "external", "caFile"},
   175  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "external", "certFile"},
   176  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "external", "keyFile"},
   177  		{spec, kubeadmConfigSpec, clusterConfiguration, "dns", "imageRepository"},
   178  		{spec, kubeadmConfigSpec, clusterConfiguration, "dns", "imageTag"},
   179  		{spec, kubeadmConfigSpec, clusterConfiguration, "imageRepository"},
   180  		{spec, kubeadmConfigSpec, clusterConfiguration, featureGates},
   181  		{spec, kubeadmConfigSpec, clusterConfiguration, featureGates, "*"},
   182  		{spec, kubeadmConfigSpec, clusterConfiguration, apiServer},
   183  		{spec, kubeadmConfigSpec, clusterConfiguration, apiServer, "*"},
   184  		{spec, kubeadmConfigSpec, clusterConfiguration, controllerManager},
   185  		{spec, kubeadmConfigSpec, clusterConfiguration, controllerManager, "*"},
   186  		{spec, kubeadmConfigSpec, clusterConfiguration, scheduler},
   187  		{spec, kubeadmConfigSpec, clusterConfiguration, scheduler, "*"},
   188  		// spec.kubeadmConfigSpec.initConfiguration
   189  		{spec, kubeadmConfigSpec, initConfiguration, nodeRegistration},
   190  		{spec, kubeadmConfigSpec, initConfiguration, nodeRegistration, "*"},
   191  		{spec, kubeadmConfigSpec, initConfiguration, patches, directory},
   192  		{spec, kubeadmConfigSpec, initConfiguration, patches},
   193  		{spec, kubeadmConfigSpec, initConfiguration, skipPhases},
   194  		{spec, kubeadmConfigSpec, initConfiguration, "bootstrapTokens"},
   195  		{spec, kubeadmConfigSpec, initConfiguration, "localAPIEndpoint"},
   196  		{spec, kubeadmConfigSpec, initConfiguration, "localAPIEndpoint", "*"},
   197  		// spec.kubeadmConfigSpec.joinConfiguration
   198  		{spec, kubeadmConfigSpec, joinConfiguration, nodeRegistration},
   199  		{spec, kubeadmConfigSpec, joinConfiguration, nodeRegistration, "*"},
   200  		{spec, kubeadmConfigSpec, joinConfiguration, patches, directory},
   201  		{spec, kubeadmConfigSpec, joinConfiguration, patches},
   202  		{spec, kubeadmConfigSpec, joinConfiguration, skipPhases},
   203  		{spec, kubeadmConfigSpec, joinConfiguration, "caCertPath"},
   204  		{spec, kubeadmConfigSpec, joinConfiguration, "controlPlane"},
   205  		{spec, kubeadmConfigSpec, joinConfiguration, "controlPlane", "*"},
   206  		{spec, kubeadmConfigSpec, joinConfiguration, "discovery"},
   207  		{spec, kubeadmConfigSpec, joinConfiguration, "discovery", "*"},
   208  		// spec.kubeadmConfigSpec
   209  		{spec, kubeadmConfigSpec, preKubeadmCommands},
   210  		{spec, kubeadmConfigSpec, postKubeadmCommands},
   211  		{spec, kubeadmConfigSpec, files},
   212  		{spec, kubeadmConfigSpec, "verbosity"},
   213  		{spec, kubeadmConfigSpec, users},
   214  		{spec, kubeadmConfigSpec, ntp},
   215  		{spec, kubeadmConfigSpec, ntp, "*"},
   216  		{spec, kubeadmConfigSpec, ignition},
   217  		{spec, kubeadmConfigSpec, ignition, "*"},
   218  		{spec, kubeadmConfigSpec, diskSetup},
   219  		{spec, kubeadmConfigSpec, diskSetup, "*"},
   220  		{spec, kubeadmConfigSpec, "format"},
   221  		{spec, kubeadmConfigSpec, "mounts"},
   222  		{spec, kubeadmConfigSpec, "useExperimentalRetryJoin"},
   223  		// spec.machineTemplate
   224  		{spec, "machineTemplate", "metadata"},
   225  		{spec, "machineTemplate", "metadata", "*"},
   226  		{spec, "machineTemplate", "infrastructureRef", "apiVersion"},
   227  		{spec, "machineTemplate", "infrastructureRef", "name"},
   228  		{spec, "machineTemplate", "infrastructureRef", "kind"},
   229  		{spec, "machineTemplate", "nodeDrainTimeout"},
   230  		{spec, "machineTemplate", "nodeVolumeDetachTimeout"},
   231  		{spec, "machineTemplate", "nodeDeletionTimeout"},
   232  		// spec
   233  		{spec, "replicas"},
   234  		{spec, "version"},
   235  		{spec, "remediationStrategy"},
   236  		{spec, "remediationStrategy", "*"},
   237  		{spec, "rolloutAfter"},
   238  		{spec, "rolloutBefore"},
   239  		{spec, "rolloutBefore", "*"},
   240  		{spec, "rolloutStrategy"},
   241  		{spec, "rolloutStrategy", "*"},
   242  	}
   243  
   244  	oldK, ok := oldObj.(*controlplanev1.KubeadmControlPlane)
   245  	if !ok {
   246  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a KubeadmControlPlane but got a %T", oldObj))
   247  	}
   248  
   249  	newK, ok := newObj.(*controlplanev1.KubeadmControlPlane)
   250  	if !ok {
   251  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a KubeadmControlPlane but got a %T", newObj))
   252  	}
   253  
   254  	allErrs := validateKubeadmControlPlaneSpec(newK.Spec, newK.Namespace, field.NewPath("spec"))
   255  
   256  	originalJSON, err := json.Marshal(oldK)
   257  	if err != nil {
   258  		return nil, apierrors.NewInternalError(err)
   259  	}
   260  	modifiedJSON, err := json.Marshal(newK)
   261  	if err != nil {
   262  		return nil, apierrors.NewInternalError(err)
   263  	}
   264  
   265  	diff, err := jsonpatch.CreateMergePatch(originalJSON, modifiedJSON)
   266  	if err != nil {
   267  		return nil, apierrors.NewInternalError(err)
   268  	}
   269  	jsonPatch := map[string]interface{}{}
   270  	if err := json.Unmarshal(diff, &jsonPatch); err != nil {
   271  		return nil, apierrors.NewInternalError(err)
   272  	}
   273  	// Build a list of all paths that are trying to change
   274  	diffpaths := paths([]string{}, jsonPatch)
   275  	// Every path in the diff must be valid for the update function to work.
   276  	for _, path := range diffpaths {
   277  		// Ignore paths that are empty
   278  		if len(path) == 0 {
   279  			continue
   280  		}
   281  		if !allowed(allowedPaths, path) {
   282  			if len(path) == 1 {
   283  				allErrs = append(allErrs, field.Forbidden(field.NewPath(path[0]), "cannot be modified"))
   284  				continue
   285  			}
   286  			allErrs = append(allErrs, field.Forbidden(field.NewPath(path[0], path[1:]...), "cannot be modified"))
   287  		}
   288  	}
   289  
   290  	allErrs = append(allErrs, webhook.validateVersion(oldK, newK)...)
   291  	allErrs = append(allErrs, validateClusterConfiguration(oldK.Spec.KubeadmConfigSpec.ClusterConfiguration, newK.Spec.KubeadmConfigSpec.ClusterConfiguration, field.NewPath("spec", "kubeadmConfigSpec", "clusterConfiguration"))...)
   292  	allErrs = append(allErrs, webhook.validateCoreDNSVersion(oldK, newK)...)
   293  	allErrs = append(allErrs, newK.Spec.KubeadmConfigSpec.Validate(field.NewPath("spec", "kubeadmConfigSpec"))...)
   294  
   295  	if len(allErrs) > 0 {
   296  		return nil, apierrors.NewInvalid(clusterv1.GroupVersion.WithKind("KubeadmControlPlane").GroupKind(), newK.Name, allErrs)
   297  	}
   298  
   299  	return nil, nil
   300  }
   301  
   302  func validateKubeadmControlPlaneSpec(s controlplanev1.KubeadmControlPlaneSpec, namespace string, pathPrefix *field.Path) field.ErrorList {
   303  	allErrs := field.ErrorList{}
   304  
   305  	if s.Replicas == nil {
   306  		allErrs = append(
   307  			allErrs,
   308  			field.Required(
   309  				pathPrefix.Child("replicas"),
   310  				"is required",
   311  			),
   312  		)
   313  	} else if *s.Replicas <= 0 {
   314  		// The use of the scale subresource should provide a guarantee that negative values
   315  		// should not be accepted for this field, but since we have to validate that Replicas != 0
   316  		// it doesn't hurt to also additionally validate for negative numbers here as well.
   317  		allErrs = append(
   318  			allErrs,
   319  			field.Forbidden(
   320  				pathPrefix.Child("replicas"),
   321  				"cannot be less than or equal to 0",
   322  			),
   323  		)
   324  	}
   325  
   326  	externalEtcd := false
   327  	if s.KubeadmConfigSpec.ClusterConfiguration != nil {
   328  		if s.KubeadmConfigSpec.ClusterConfiguration.Etcd.External != nil {
   329  			externalEtcd = true
   330  		}
   331  	}
   332  
   333  	if !externalEtcd {
   334  		if s.Replicas != nil && *s.Replicas%2 == 0 {
   335  			allErrs = append(
   336  				allErrs,
   337  				field.Forbidden(
   338  					pathPrefix.Child("replicas"),
   339  					"cannot be an even number when etcd is stacked",
   340  				),
   341  			)
   342  		}
   343  	}
   344  
   345  	if s.MachineTemplate.InfrastructureRef.APIVersion == "" {
   346  		allErrs = append(
   347  			allErrs,
   348  			field.Invalid(
   349  				pathPrefix.Child("machineTemplate", "infrastructure", "apiVersion"),
   350  				s.MachineTemplate.InfrastructureRef.APIVersion,
   351  				"cannot be empty",
   352  			),
   353  		)
   354  	}
   355  	if s.MachineTemplate.InfrastructureRef.Kind == "" {
   356  		allErrs = append(
   357  			allErrs,
   358  			field.Invalid(
   359  				pathPrefix.Child("machineTemplate", "infrastructure", "kind"),
   360  				s.MachineTemplate.InfrastructureRef.Kind,
   361  				"cannot be empty",
   362  			),
   363  		)
   364  	}
   365  	if s.MachineTemplate.InfrastructureRef.Name == "" {
   366  		allErrs = append(
   367  			allErrs,
   368  			field.Invalid(
   369  				pathPrefix.Child("machineTemplate", "infrastructure", "name"),
   370  				s.MachineTemplate.InfrastructureRef.Name,
   371  				"cannot be empty",
   372  			),
   373  		)
   374  	}
   375  	if s.MachineTemplate.InfrastructureRef.Namespace != namespace {
   376  		allErrs = append(
   377  			allErrs,
   378  			field.Invalid(
   379  				pathPrefix.Child("machineTemplate", "infrastructure", "namespace"),
   380  				s.MachineTemplate.InfrastructureRef.Namespace,
   381  				"must match metadata.namespace",
   382  			),
   383  		)
   384  	}
   385  
   386  	// Validate the metadata of the MachineTemplate
   387  	allErrs = append(allErrs, s.MachineTemplate.ObjectMeta.Validate(pathPrefix.Child("machineTemplate", "metadata"))...)
   388  
   389  	if !version.KubeSemver.MatchString(s.Version) {
   390  		allErrs = append(allErrs, field.Invalid(pathPrefix.Child("version"), s.Version, "must be a valid semantic version"))
   391  	}
   392  
   393  	allErrs = append(allErrs, validateRolloutBefore(s.RolloutBefore, pathPrefix.Child("rolloutBefore"))...)
   394  	allErrs = append(allErrs, validateRolloutStrategy(s.RolloutStrategy, s.Replicas, pathPrefix.Child("rolloutStrategy"))...)
   395  
   396  	return allErrs
   397  }
   398  
   399  func validateRolloutBefore(rolloutBefore *controlplanev1.RolloutBefore, pathPrefix *field.Path) field.ErrorList {
   400  	allErrs := field.ErrorList{}
   401  
   402  	if rolloutBefore == nil {
   403  		return allErrs
   404  	}
   405  
   406  	if rolloutBefore.CertificatesExpiryDays != nil {
   407  		if *rolloutBefore.CertificatesExpiryDays < minimumCertificatesExpiryDays {
   408  			allErrs = append(allErrs, field.Invalid(pathPrefix.Child("certificatesExpiryDays"), *rolloutBefore.CertificatesExpiryDays, fmt.Sprintf("must be greater than or equal to %v", minimumCertificatesExpiryDays)))
   409  		}
   410  	}
   411  
   412  	return allErrs
   413  }
   414  
   415  func validateRolloutStrategy(rolloutStrategy *controlplanev1.RolloutStrategy, replicas *int32, pathPrefix *field.Path) field.ErrorList {
   416  	allErrs := field.ErrorList{}
   417  
   418  	if rolloutStrategy == nil {
   419  		return allErrs
   420  	}
   421  
   422  	if rolloutStrategy.Type != controlplanev1.RollingUpdateStrategyType {
   423  		allErrs = append(
   424  			allErrs,
   425  			field.Required(
   426  				pathPrefix.Child("type"),
   427  				"only RollingUpdateStrategyType is supported",
   428  			),
   429  		)
   430  	}
   431  
   432  	ios1 := intstr.FromInt(1)
   433  	ios0 := intstr.FromInt(0)
   434  
   435  	if rolloutStrategy.RollingUpdate.MaxSurge.IntValue() == ios0.IntValue() && (replicas != nil && *replicas < int32(3)) {
   436  		allErrs = append(
   437  			allErrs,
   438  			field.Required(
   439  				pathPrefix.Child("rollingUpdate"),
   440  				"when KubeadmControlPlane is configured to scale-in, replica count needs to be at least 3",
   441  			),
   442  		)
   443  	}
   444  
   445  	if rolloutStrategy.RollingUpdate.MaxSurge.IntValue() != ios1.IntValue() && rolloutStrategy.RollingUpdate.MaxSurge.IntValue() != ios0.IntValue() {
   446  		allErrs = append(
   447  			allErrs,
   448  			field.Required(
   449  				pathPrefix.Child("rollingUpdate", "maxSurge"),
   450  				"value must be 1 or 0",
   451  			),
   452  		)
   453  	}
   454  
   455  	return allErrs
   456  }
   457  
   458  func validateClusterConfiguration(oldClusterConfiguration, newClusterConfiguration *bootstrapv1.ClusterConfiguration, pathPrefix *field.Path) field.ErrorList {
   459  	allErrs := field.ErrorList{}
   460  
   461  	if newClusterConfiguration == nil {
   462  		return allErrs
   463  	}
   464  
   465  	// TODO: Remove when kubeadm types include OpenAPI validation
   466  	if !container.ImageTagIsValid(newClusterConfiguration.DNS.ImageTag) {
   467  		allErrs = append(
   468  			allErrs,
   469  			field.Forbidden(
   470  				pathPrefix.Child("dns", "imageTag"),
   471  				fmt.Sprintf("tag %s is invalid", newClusterConfiguration.DNS.ImageTag),
   472  			),
   473  		)
   474  	}
   475  
   476  	if newClusterConfiguration.DNS.ImageTag != "" {
   477  		if _, err := version.ParseMajorMinorPatchTolerant(newClusterConfiguration.DNS.ImageTag); err != nil {
   478  			allErrs = append(allErrs,
   479  				field.Invalid(
   480  					field.NewPath("dns", "imageTag"),
   481  					newClusterConfiguration.DNS.ImageTag,
   482  					fmt.Sprintf("failed to parse CoreDNS version: %v", err),
   483  				),
   484  			)
   485  		}
   486  	}
   487  
   488  	// TODO: Remove when kubeadm types include OpenAPI validation
   489  	if newClusterConfiguration.Etcd.Local != nil && !container.ImageTagIsValid(newClusterConfiguration.Etcd.Local.ImageTag) {
   490  		allErrs = append(
   491  			allErrs,
   492  			field.Forbidden(
   493  				pathPrefix.Child("etcd", "local", "imageTag"),
   494  				fmt.Sprintf("tag %s is invalid", newClusterConfiguration.Etcd.Local.ImageTag),
   495  			),
   496  		)
   497  	}
   498  
   499  	if newClusterConfiguration.Etcd.Local != nil && newClusterConfiguration.Etcd.External != nil {
   500  		allErrs = append(
   501  			allErrs,
   502  			field.Forbidden(
   503  				pathPrefix.Child("etcd", "local"),
   504  				"cannot have both external and local etcd",
   505  			),
   506  		)
   507  	}
   508  
   509  	// update validations
   510  	if oldClusterConfiguration != nil {
   511  		if newClusterConfiguration.Etcd.External != nil && oldClusterConfiguration.Etcd.Local != nil {
   512  			allErrs = append(
   513  				allErrs,
   514  				field.Forbidden(
   515  					pathPrefix.Child("etcd", "external"),
   516  					"cannot change between external and local etcd",
   517  				),
   518  			)
   519  		}
   520  
   521  		if newClusterConfiguration.Etcd.Local != nil && oldClusterConfiguration.Etcd.External != nil {
   522  			allErrs = append(
   523  				allErrs,
   524  				field.Forbidden(
   525  					pathPrefix.Child("etcd", "local"),
   526  					"cannot change between external and local etcd",
   527  				),
   528  			)
   529  		}
   530  	}
   531  
   532  	return allErrs
   533  }
   534  
   535  func allowed(allowList [][]string, path []string) bool {
   536  	for _, allowed := range allowList {
   537  		if pathsMatch(allowed, path) {
   538  			return true
   539  		}
   540  	}
   541  	return false
   542  }
   543  
   544  func pathsMatch(allowed, path []string) bool {
   545  	// if either are empty then no match can be made
   546  	if len(allowed) == 0 || len(path) == 0 {
   547  		return false
   548  	}
   549  	i := 0
   550  	for i = range path {
   551  		// reached the end of the allowed path and no match was found
   552  		if i > len(allowed)-1 {
   553  			return false
   554  		}
   555  		if allowed[i] == "*" {
   556  			return true
   557  		}
   558  		if path[i] != allowed[i] {
   559  			return false
   560  		}
   561  	}
   562  	// path has been completely iterated and has not matched the end of the path.
   563  	// e.g. allowed: []string{"a","b","c"}, path: []string{"a"}
   564  	return i >= len(allowed)-1
   565  }
   566  
   567  // paths builds a slice of paths that are being modified.
   568  func paths(path []string, diff map[string]interface{}) [][]string {
   569  	allPaths := [][]string{}
   570  	for key, m := range diff {
   571  		nested, ok := m.(map[string]interface{})
   572  		if !ok {
   573  			// We have to use a copy of path, because otherwise the slice we append to
   574  			// allPaths would be overwritten in another iteration.
   575  			tmp := make([]string, len(path))
   576  			copy(tmp, path)
   577  			allPaths = append(allPaths, append(tmp, key))
   578  			continue
   579  		}
   580  		allPaths = append(allPaths, paths(append(path, key), nested)...)
   581  	}
   582  	return allPaths
   583  }
   584  
   585  func (webhook *KubeadmControlPlane) validateCoreDNSVersion(oldK, newK *controlplanev1.KubeadmControlPlane) (allErrs field.ErrorList) {
   586  	if newK.Spec.KubeadmConfigSpec.ClusterConfiguration == nil || oldK.Spec.KubeadmConfigSpec.ClusterConfiguration == nil {
   587  		return allErrs
   588  	}
   589  	// return if either current or target versions is empty
   590  	if newK.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS.ImageTag == "" || oldK.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS.ImageTag == "" {
   591  		return allErrs
   592  	}
   593  	targetDNS := &newK.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS
   594  
   595  	fromVersion, err := version.ParseMajorMinorPatchTolerant(oldK.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS.ImageTag)
   596  	if err != nil {
   597  		allErrs = append(allErrs,
   598  			field.Invalid(
   599  				field.NewPath("spec", "kubeadmConfigSpec", "clusterConfiguration", "dns", "imageTag"),
   600  				oldK.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS.ImageTag,
   601  				fmt.Sprintf("failed to parse current CoreDNS version: %v", err),
   602  			),
   603  		)
   604  		return allErrs
   605  	}
   606  
   607  	toVersion, err := version.ParseMajorMinorPatchTolerant(targetDNS.ImageTag)
   608  	if err != nil {
   609  		allErrs = append(allErrs,
   610  			field.Invalid(
   611  				field.NewPath("spec", "kubeadmConfigSpec", "clusterConfiguration", "dns", "imageTag"),
   612  				targetDNS.ImageTag,
   613  				fmt.Sprintf("failed to parse target CoreDNS version: %v", err),
   614  			),
   615  		)
   616  		return allErrs
   617  	}
   618  	// If the versions are equal return here without error.
   619  	// This allows an upgrade where the version of CoreDNS in use is not supported by the migration tool.
   620  	if toVersion.Equals(fromVersion) {
   621  		return allErrs
   622  	}
   623  	if err := migration.ValidUpMigration(fromVersion.String(), toVersion.String()); err != nil {
   624  		allErrs = append(
   625  			allErrs,
   626  			field.Forbidden(
   627  				field.NewPath("spec", "kubeadmConfigSpec", "clusterConfiguration", "dns", "imageTag"),
   628  				fmt.Sprintf("cannot migrate CoreDNS up to '%v' from '%v': %v", toVersion, fromVersion, err),
   629  			),
   630  		)
   631  	}
   632  
   633  	return allErrs
   634  }
   635  
   636  func (webhook *KubeadmControlPlane) validateVersion(oldK, newK *controlplanev1.KubeadmControlPlane) (allErrs field.ErrorList) {
   637  	previousVersion := oldK.Spec.Version
   638  	fromVersion, err := version.ParseMajorMinorPatch(previousVersion)
   639  	if err != nil {
   640  		allErrs = append(allErrs,
   641  			field.InternalError(
   642  				field.NewPath("spec", "version"),
   643  				errors.Wrapf(err, "failed to parse current kubeadmcontrolplane version: %s", previousVersion),
   644  			),
   645  		)
   646  		return allErrs
   647  	}
   648  
   649  	toVersion, err := version.ParseMajorMinorPatch(newK.Spec.Version)
   650  	if err != nil {
   651  		allErrs = append(allErrs,
   652  			field.InternalError(
   653  				field.NewPath("spec", "version"),
   654  				errors.Wrapf(err, "failed to parse updated kubeadmcontrolplane version: %s", newK.Spec.Version),
   655  			),
   656  		)
   657  		return allErrs
   658  	}
   659  
   660  	// Check if we're trying to upgrade to Kubernetes v1.19.0, which is not supported.
   661  	//
   662  	// See https://github.com/kubernetes-sigs/cluster-api/issues/3564
   663  	if fromVersion.NE(toVersion) && toVersion.Equals(semver.MustParse("1.19.0")) {
   664  		allErrs = append(allErrs,
   665  			field.Forbidden(
   666  				field.NewPath("spec", "version"),
   667  				"cannot update Kubernetes version to v1.19.0, for more information see https://github.com/kubernetes-sigs/cluster-api/issues/3564",
   668  			),
   669  		)
   670  		return allErrs
   671  	}
   672  
   673  	// Validate that the update is upgrading at most one minor version.
   674  	// Note: Skipping a minor version is not allowed.
   675  	// Note: Checking against this ceilVersion allows upgrading to the next minor
   676  	// version irrespective of the patch version.
   677  	ceilVersion := semver.Version{
   678  		Major: fromVersion.Major,
   679  		Minor: fromVersion.Minor + 2,
   680  		Patch: 0,
   681  	}
   682  	if toVersion.GTE(ceilVersion) {
   683  		allErrs = append(allErrs,
   684  			field.Forbidden(
   685  				field.NewPath("spec", "version"),
   686  				fmt.Sprintf("cannot update Kubernetes version from %s to %s", previousVersion, newK.Spec.Version),
   687  			),
   688  		)
   689  	}
   690  
   691  	// The Kubernetes ecosystem has been requested to move users to the new registry due to cost issues.
   692  	// This validation enforces the move to the new registry by forcing users to upgrade to kubeadm versions
   693  	// with the new registry.
   694  	// NOTE: This only affects users relying on the community maintained registry.
   695  	// NOTE: Pinning to the upstream registry is not recommended because it could lead to issues
   696  	// given how the migration has been implemented in kubeadm.
   697  	//
   698  	// Block if imageRepository is not set (i.e. the default registry should be used),
   699  	if (newK.Spec.KubeadmConfigSpec.ClusterConfiguration == nil ||
   700  		newK.Spec.KubeadmConfigSpec.ClusterConfiguration.ImageRepository == "") &&
   701  		// the version changed (i.e. we have an upgrade),
   702  		toVersion.NE(fromVersion) &&
   703  		// the version is >= v1.22.0 and < v1.26.0
   704  		toVersion.GTE(kubeadm.MinKubernetesVersionImageRegistryMigration) &&
   705  		toVersion.LT(kubeadm.NextKubernetesVersionImageRegistryMigration) &&
   706  		// and the default registry of the new Kubernetes/kubeadm version is the old default registry.
   707  		kubeadm.GetDefaultRegistry(toVersion) == kubeadm.OldDefaultImageRepository {
   708  		allErrs = append(allErrs,
   709  			field.Forbidden(
   710  				field.NewPath("spec", "version"),
   711  				"cannot upgrade to a Kubernetes/kubeadm version which is using the old default registry. Please use a newer Kubernetes patch release which is using the new default registry (>= v1.22.17, >= v1.23.15, >= v1.24.9)",
   712  			),
   713  		)
   714  	}
   715  
   716  	return allErrs
   717  }
   718  
   719  // ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
   720  func (webhook *KubeadmControlPlane) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
   721  	return nil, nil
   722  }