sigs.k8s.io/cluster-api@v1.6.3/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  )
   154  
   155  const minimumCertificatesExpiryDays = 7
   156  
   157  // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
   158  func (webhook *KubeadmControlPlane) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
   159  	// add a * to indicate everything beneath is ok.
   160  	// For example, {"spec", "*"} will allow any path under "spec" to change.
   161  	allowedPaths := [][]string{
   162  		// metadata
   163  		{"metadata", "*"},
   164  		// spec.kubeadmConfigSpec.clusterConfiguration
   165  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "local", "imageRepository"},
   166  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "local", "imageTag"},
   167  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "local", "extraArgs"},
   168  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "local", "extraArgs", "*"},
   169  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "local", "dataDir"},
   170  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "local", "peerCertSANs"},
   171  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "local", "serverCertSANs"},
   172  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "external", "endpoints"},
   173  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "external", "caFile"},
   174  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "external", "certFile"},
   175  		{spec, kubeadmConfigSpec, clusterConfiguration, "etcd", "external", "keyFile"},
   176  		{spec, kubeadmConfigSpec, clusterConfiguration, "dns", "imageRepository"},
   177  		{spec, kubeadmConfigSpec, clusterConfiguration, "dns", "imageTag"},
   178  		{spec, kubeadmConfigSpec, clusterConfiguration, "imageRepository"},
   179  		{spec, kubeadmConfigSpec, clusterConfiguration, apiServer},
   180  		{spec, kubeadmConfigSpec, clusterConfiguration, apiServer, "*"},
   181  		{spec, kubeadmConfigSpec, clusterConfiguration, controllerManager},
   182  		{spec, kubeadmConfigSpec, clusterConfiguration, controllerManager, "*"},
   183  		{spec, kubeadmConfigSpec, clusterConfiguration, scheduler},
   184  		{spec, kubeadmConfigSpec, clusterConfiguration, scheduler, "*"},
   185  		// spec.kubeadmConfigSpec.initConfiguration
   186  		{spec, kubeadmConfigSpec, initConfiguration, nodeRegistration},
   187  		{spec, kubeadmConfigSpec, initConfiguration, nodeRegistration, "*"},
   188  		{spec, kubeadmConfigSpec, initConfiguration, patches, directory},
   189  		{spec, kubeadmConfigSpec, initConfiguration, patches},
   190  		{spec, kubeadmConfigSpec, initConfiguration, skipPhases},
   191  		{spec, kubeadmConfigSpec, initConfiguration, "bootstrapTokens"},
   192  		{spec, kubeadmConfigSpec, initConfiguration, "localAPIEndpoint"},
   193  		{spec, kubeadmConfigSpec, initConfiguration, "localAPIEndpoint", "*"},
   194  		// spec.kubeadmConfigSpec.joinConfiguration
   195  		{spec, kubeadmConfigSpec, joinConfiguration, nodeRegistration},
   196  		{spec, kubeadmConfigSpec, joinConfiguration, nodeRegistration, "*"},
   197  		{spec, kubeadmConfigSpec, joinConfiguration, patches, directory},
   198  		{spec, kubeadmConfigSpec, joinConfiguration, patches},
   199  		{spec, kubeadmConfigSpec, joinConfiguration, skipPhases},
   200  		{spec, kubeadmConfigSpec, joinConfiguration, "caCertPath"},
   201  		{spec, kubeadmConfigSpec, joinConfiguration, "controlPlane"},
   202  		{spec, kubeadmConfigSpec, joinConfiguration, "controlPlane", "*"},
   203  		{spec, kubeadmConfigSpec, joinConfiguration, "discovery"},
   204  		{spec, kubeadmConfigSpec, joinConfiguration, "discovery", "*"},
   205  		// spec.kubeadmConfigSpec
   206  		{spec, kubeadmConfigSpec, preKubeadmCommands},
   207  		{spec, kubeadmConfigSpec, postKubeadmCommands},
   208  		{spec, kubeadmConfigSpec, files},
   209  		{spec, kubeadmConfigSpec, "verbosity"},
   210  		{spec, kubeadmConfigSpec, users},
   211  		{spec, kubeadmConfigSpec, ntp},
   212  		{spec, kubeadmConfigSpec, ntp, "*"},
   213  		{spec, kubeadmConfigSpec, ignition},
   214  		{spec, kubeadmConfigSpec, ignition, "*"},
   215  		{spec, kubeadmConfigSpec, diskSetup},
   216  		{spec, kubeadmConfigSpec, diskSetup, "*"},
   217  		{spec, kubeadmConfigSpec, "format"},
   218  		{spec, kubeadmConfigSpec, "mounts"},
   219  		{spec, kubeadmConfigSpec, "useExperimentalRetryJoin"},
   220  		// spec.machineTemplate
   221  		{spec, "machineTemplate", "metadata"},
   222  		{spec, "machineTemplate", "metadata", "*"},
   223  		{spec, "machineTemplate", "infrastructureRef", "apiVersion"},
   224  		{spec, "machineTemplate", "infrastructureRef", "name"},
   225  		{spec, "machineTemplate", "infrastructureRef", "kind"},
   226  		{spec, "machineTemplate", "nodeDrainTimeout"},
   227  		{spec, "machineTemplate", "nodeVolumeDetachTimeout"},
   228  		{spec, "machineTemplate", "nodeDeletionTimeout"},
   229  		// spec
   230  		{spec, "replicas"},
   231  		{spec, "version"},
   232  		{spec, "remediationStrategy"},
   233  		{spec, "remediationStrategy", "*"},
   234  		{spec, "rolloutAfter"},
   235  		{spec, "rolloutBefore"},
   236  		{spec, "rolloutBefore", "*"},
   237  		{spec, "rolloutStrategy"},
   238  		{spec, "rolloutStrategy", "*"},
   239  	}
   240  
   241  	oldK, ok := oldObj.(*controlplanev1.KubeadmControlPlane)
   242  	if !ok {
   243  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a KubeadmControlPlane but got a %T", oldObj))
   244  	}
   245  
   246  	newK, ok := newObj.(*controlplanev1.KubeadmControlPlane)
   247  	if !ok {
   248  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a KubeadmControlPlane but got a %T", newObj))
   249  	}
   250  
   251  	allErrs := validateKubeadmControlPlaneSpec(newK.Spec, newK.Namespace, field.NewPath("spec"))
   252  
   253  	originalJSON, err := json.Marshal(oldK)
   254  	if err != nil {
   255  		return nil, apierrors.NewInternalError(err)
   256  	}
   257  	modifiedJSON, err := json.Marshal(newK)
   258  	if err != nil {
   259  		return nil, apierrors.NewInternalError(err)
   260  	}
   261  
   262  	diff, err := jsonpatch.CreateMergePatch(originalJSON, modifiedJSON)
   263  	if err != nil {
   264  		return nil, apierrors.NewInternalError(err)
   265  	}
   266  	jsonPatch := map[string]interface{}{}
   267  	if err := json.Unmarshal(diff, &jsonPatch); err != nil {
   268  		return nil, apierrors.NewInternalError(err)
   269  	}
   270  	// Build a list of all paths that are trying to change
   271  	diffpaths := paths([]string{}, jsonPatch)
   272  	// Every path in the diff must be valid for the update function to work.
   273  	for _, path := range diffpaths {
   274  		// Ignore paths that are empty
   275  		if len(path) == 0 {
   276  			continue
   277  		}
   278  		if !allowed(allowedPaths, path) {
   279  			if len(path) == 1 {
   280  				allErrs = append(allErrs, field.Forbidden(field.NewPath(path[0]), "cannot be modified"))
   281  				continue
   282  			}
   283  			allErrs = append(allErrs, field.Forbidden(field.NewPath(path[0], path[1:]...), "cannot be modified"))
   284  		}
   285  	}
   286  
   287  	allErrs = append(allErrs, webhook.validateVersion(oldK, newK)...)
   288  	allErrs = append(allErrs, validateClusterConfiguration(oldK.Spec.KubeadmConfigSpec.ClusterConfiguration, newK.Spec.KubeadmConfigSpec.ClusterConfiguration, field.NewPath("spec", "kubeadmConfigSpec", "clusterConfiguration"))...)
   289  	allErrs = append(allErrs, webhook.validateCoreDNSVersion(oldK, newK)...)
   290  	allErrs = append(allErrs, newK.Spec.KubeadmConfigSpec.Validate(field.NewPath("spec", "kubeadmConfigSpec"))...)
   291  
   292  	if len(allErrs) > 0 {
   293  		return nil, apierrors.NewInvalid(clusterv1.GroupVersion.WithKind("KubeadmControlPlane").GroupKind(), newK.Name, allErrs)
   294  	}
   295  
   296  	return nil, nil
   297  }
   298  
   299  func validateKubeadmControlPlaneSpec(s controlplanev1.KubeadmControlPlaneSpec, namespace string, pathPrefix *field.Path) field.ErrorList {
   300  	allErrs := field.ErrorList{}
   301  
   302  	if s.Replicas == nil {
   303  		allErrs = append(
   304  			allErrs,
   305  			field.Required(
   306  				pathPrefix.Child("replicas"),
   307  				"is required",
   308  			),
   309  		)
   310  	} else if *s.Replicas <= 0 {
   311  		// The use of the scale subresource should provide a guarantee that negative values
   312  		// should not be accepted for this field, but since we have to validate that Replicas != 0
   313  		// it doesn't hurt to also additionally validate for negative numbers here as well.
   314  		allErrs = append(
   315  			allErrs,
   316  			field.Forbidden(
   317  				pathPrefix.Child("replicas"),
   318  				"cannot be less than or equal to 0",
   319  			),
   320  		)
   321  	}
   322  
   323  	externalEtcd := false
   324  	if s.KubeadmConfigSpec.ClusterConfiguration != nil {
   325  		if s.KubeadmConfigSpec.ClusterConfiguration.Etcd.External != nil {
   326  			externalEtcd = true
   327  		}
   328  	}
   329  
   330  	if !externalEtcd {
   331  		if s.Replicas != nil && *s.Replicas%2 == 0 {
   332  			allErrs = append(
   333  				allErrs,
   334  				field.Forbidden(
   335  					pathPrefix.Child("replicas"),
   336  					"cannot be an even number when etcd is stacked",
   337  				),
   338  			)
   339  		}
   340  	}
   341  
   342  	if s.MachineTemplate.InfrastructureRef.APIVersion == "" {
   343  		allErrs = append(
   344  			allErrs,
   345  			field.Invalid(
   346  				pathPrefix.Child("machineTemplate", "infrastructure", "apiVersion"),
   347  				s.MachineTemplate.InfrastructureRef.APIVersion,
   348  				"cannot be empty",
   349  			),
   350  		)
   351  	}
   352  	if s.MachineTemplate.InfrastructureRef.Kind == "" {
   353  		allErrs = append(
   354  			allErrs,
   355  			field.Invalid(
   356  				pathPrefix.Child("machineTemplate", "infrastructure", "kind"),
   357  				s.MachineTemplate.InfrastructureRef.Kind,
   358  				"cannot be empty",
   359  			),
   360  		)
   361  	}
   362  	if s.MachineTemplate.InfrastructureRef.Name == "" {
   363  		allErrs = append(
   364  			allErrs,
   365  			field.Invalid(
   366  				pathPrefix.Child("machineTemplate", "infrastructure", "name"),
   367  				s.MachineTemplate.InfrastructureRef.Name,
   368  				"cannot be empty",
   369  			),
   370  		)
   371  	}
   372  	if s.MachineTemplate.InfrastructureRef.Namespace != namespace {
   373  		allErrs = append(
   374  			allErrs,
   375  			field.Invalid(
   376  				pathPrefix.Child("machineTemplate", "infrastructure", "namespace"),
   377  				s.MachineTemplate.InfrastructureRef.Namespace,
   378  				"must match metadata.namespace",
   379  			),
   380  		)
   381  	}
   382  
   383  	// Validate the metadata of the MachineTemplate
   384  	allErrs = append(allErrs, s.MachineTemplate.ObjectMeta.Validate(pathPrefix.Child("machineTemplate", "metadata"))...)
   385  
   386  	if !version.KubeSemver.MatchString(s.Version) {
   387  		allErrs = append(allErrs, field.Invalid(pathPrefix.Child("version"), s.Version, "must be a valid semantic version"))
   388  	}
   389  
   390  	allErrs = append(allErrs, validateRolloutBefore(s.RolloutBefore, pathPrefix.Child("rolloutBefore"))...)
   391  	allErrs = append(allErrs, validateRolloutStrategy(s.RolloutStrategy, s.Replicas, pathPrefix.Child("rolloutStrategy"))...)
   392  
   393  	return allErrs
   394  }
   395  
   396  func validateRolloutBefore(rolloutBefore *controlplanev1.RolloutBefore, pathPrefix *field.Path) field.ErrorList {
   397  	allErrs := field.ErrorList{}
   398  
   399  	if rolloutBefore == nil {
   400  		return allErrs
   401  	}
   402  
   403  	if rolloutBefore.CertificatesExpiryDays != nil {
   404  		if *rolloutBefore.CertificatesExpiryDays < minimumCertificatesExpiryDays {
   405  			allErrs = append(allErrs, field.Invalid(pathPrefix.Child("certificatesExpiryDays"), *rolloutBefore.CertificatesExpiryDays, fmt.Sprintf("must be greater than or equal to %v", minimumCertificatesExpiryDays)))
   406  		}
   407  	}
   408  
   409  	return allErrs
   410  }
   411  
   412  func validateRolloutStrategy(rolloutStrategy *controlplanev1.RolloutStrategy, replicas *int32, pathPrefix *field.Path) field.ErrorList {
   413  	allErrs := field.ErrorList{}
   414  
   415  	if rolloutStrategy == nil {
   416  		return allErrs
   417  	}
   418  
   419  	if rolloutStrategy.Type != controlplanev1.RollingUpdateStrategyType {
   420  		allErrs = append(
   421  			allErrs,
   422  			field.Required(
   423  				pathPrefix.Child("type"),
   424  				"only RollingUpdateStrategyType is supported",
   425  			),
   426  		)
   427  	}
   428  
   429  	ios1 := intstr.FromInt(1)
   430  	ios0 := intstr.FromInt(0)
   431  
   432  	if rolloutStrategy.RollingUpdate.MaxSurge.IntValue() == ios0.IntValue() && (replicas != nil && *replicas < int32(3)) {
   433  		allErrs = append(
   434  			allErrs,
   435  			field.Required(
   436  				pathPrefix.Child("rollingUpdate"),
   437  				"when KubeadmControlPlane is configured to scale-in, replica count needs to be at least 3",
   438  			),
   439  		)
   440  	}
   441  
   442  	if rolloutStrategy.RollingUpdate.MaxSurge.IntValue() != ios1.IntValue() && rolloutStrategy.RollingUpdate.MaxSurge.IntValue() != ios0.IntValue() {
   443  		allErrs = append(
   444  			allErrs,
   445  			field.Required(
   446  				pathPrefix.Child("rollingUpdate", "maxSurge"),
   447  				"value must be 1 or 0",
   448  			),
   449  		)
   450  	}
   451  
   452  	return allErrs
   453  }
   454  
   455  func validateClusterConfiguration(oldClusterConfiguration, newClusterConfiguration *bootstrapv1.ClusterConfiguration, pathPrefix *field.Path) field.ErrorList {
   456  	allErrs := field.ErrorList{}
   457  
   458  	if newClusterConfiguration == nil {
   459  		return allErrs
   460  	}
   461  
   462  	// TODO: Remove when kubeadm types include OpenAPI validation
   463  	if !container.ImageTagIsValid(newClusterConfiguration.DNS.ImageTag) {
   464  		allErrs = append(
   465  			allErrs,
   466  			field.Forbidden(
   467  				pathPrefix.Child("dns", "imageTag"),
   468  				fmt.Sprintf("tag %s is invalid", newClusterConfiguration.DNS.ImageTag),
   469  			),
   470  		)
   471  	}
   472  
   473  	if newClusterConfiguration.DNS.ImageTag != "" {
   474  		if _, err := version.ParseMajorMinorPatchTolerant(newClusterConfiguration.DNS.ImageTag); err != nil {
   475  			allErrs = append(allErrs,
   476  				field.Invalid(
   477  					field.NewPath("dns", "imageTag"),
   478  					newClusterConfiguration.DNS.ImageTag,
   479  					fmt.Sprintf("failed to parse CoreDNS version: %v", err),
   480  				),
   481  			)
   482  		}
   483  	}
   484  
   485  	// TODO: Remove when kubeadm types include OpenAPI validation
   486  	if newClusterConfiguration.Etcd.Local != nil && !container.ImageTagIsValid(newClusterConfiguration.Etcd.Local.ImageTag) {
   487  		allErrs = append(
   488  			allErrs,
   489  			field.Forbidden(
   490  				pathPrefix.Child("etcd", "local", "imageTag"),
   491  				fmt.Sprintf("tag %s is invalid", newClusterConfiguration.Etcd.Local.ImageTag),
   492  			),
   493  		)
   494  	}
   495  
   496  	if newClusterConfiguration.Etcd.Local != nil && newClusterConfiguration.Etcd.External != nil {
   497  		allErrs = append(
   498  			allErrs,
   499  			field.Forbidden(
   500  				pathPrefix.Child("etcd", "local"),
   501  				"cannot have both external and local etcd",
   502  			),
   503  		)
   504  	}
   505  
   506  	// update validations
   507  	if oldClusterConfiguration != nil {
   508  		if newClusterConfiguration.Etcd.External != nil && oldClusterConfiguration.Etcd.Local != nil {
   509  			allErrs = append(
   510  				allErrs,
   511  				field.Forbidden(
   512  					pathPrefix.Child("etcd", "external"),
   513  					"cannot change between external and local etcd",
   514  				),
   515  			)
   516  		}
   517  
   518  		if newClusterConfiguration.Etcd.Local != nil && oldClusterConfiguration.Etcd.External != nil {
   519  			allErrs = append(
   520  				allErrs,
   521  				field.Forbidden(
   522  					pathPrefix.Child("etcd", "local"),
   523  					"cannot change between external and local etcd",
   524  				),
   525  			)
   526  		}
   527  	}
   528  
   529  	return allErrs
   530  }
   531  
   532  func allowed(allowList [][]string, path []string) bool {
   533  	for _, allowed := range allowList {
   534  		if pathsMatch(allowed, path) {
   535  			return true
   536  		}
   537  	}
   538  	return false
   539  }
   540  
   541  func pathsMatch(allowed, path []string) bool {
   542  	// if either are empty then no match can be made
   543  	if len(allowed) == 0 || len(path) == 0 {
   544  		return false
   545  	}
   546  	i := 0
   547  	for i = range path {
   548  		// reached the end of the allowed path and no match was found
   549  		if i > len(allowed)-1 {
   550  			return false
   551  		}
   552  		if allowed[i] == "*" {
   553  			return true
   554  		}
   555  		if path[i] != allowed[i] {
   556  			return false
   557  		}
   558  	}
   559  	// path has been completely iterated and has not matched the end of the path.
   560  	// e.g. allowed: []string{"a","b","c"}, path: []string{"a"}
   561  	return i >= len(allowed)-1
   562  }
   563  
   564  // paths builds a slice of paths that are being modified.
   565  func paths(path []string, diff map[string]interface{}) [][]string {
   566  	allPaths := [][]string{}
   567  	for key, m := range diff {
   568  		nested, ok := m.(map[string]interface{})
   569  		if !ok {
   570  			// We have to use a copy of path, because otherwise the slice we append to
   571  			// allPaths would be overwritten in another iteration.
   572  			tmp := make([]string, len(path))
   573  			copy(tmp, path)
   574  			allPaths = append(allPaths, append(tmp, key))
   575  			continue
   576  		}
   577  		allPaths = append(allPaths, paths(append(path, key), nested)...)
   578  	}
   579  	return allPaths
   580  }
   581  
   582  func (webhook *KubeadmControlPlane) validateCoreDNSVersion(oldK, newK *controlplanev1.KubeadmControlPlane) (allErrs field.ErrorList) {
   583  	if newK.Spec.KubeadmConfigSpec.ClusterConfiguration == nil || oldK.Spec.KubeadmConfigSpec.ClusterConfiguration == nil {
   584  		return allErrs
   585  	}
   586  	// return if either current or target versions is empty
   587  	if newK.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS.ImageTag == "" || oldK.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS.ImageTag == "" {
   588  		return allErrs
   589  	}
   590  	targetDNS := &newK.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS
   591  
   592  	fromVersion, err := version.ParseMajorMinorPatchTolerant(oldK.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS.ImageTag)
   593  	if err != nil {
   594  		allErrs = append(allErrs,
   595  			field.Invalid(
   596  				field.NewPath("spec", "kubeadmConfigSpec", "clusterConfiguration", "dns", "imageTag"),
   597  				oldK.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS.ImageTag,
   598  				fmt.Sprintf("failed to parse current CoreDNS version: %v", err),
   599  			),
   600  		)
   601  		return allErrs
   602  	}
   603  
   604  	toVersion, err := version.ParseMajorMinorPatchTolerant(targetDNS.ImageTag)
   605  	if err != nil {
   606  		allErrs = append(allErrs,
   607  			field.Invalid(
   608  				field.NewPath("spec", "kubeadmConfigSpec", "clusterConfiguration", "dns", "imageTag"),
   609  				targetDNS.ImageTag,
   610  				fmt.Sprintf("failed to parse target CoreDNS version: %v", err),
   611  			),
   612  		)
   613  		return allErrs
   614  	}
   615  	// If the versions are equal return here without error.
   616  	// This allows an upgrade where the version of CoreDNS in use is not supported by the migration tool.
   617  	if toVersion.Equals(fromVersion) {
   618  		return allErrs
   619  	}
   620  	if err := migration.ValidUpMigration(fromVersion.String(), toVersion.String()); err != nil {
   621  		allErrs = append(
   622  			allErrs,
   623  			field.Forbidden(
   624  				field.NewPath("spec", "kubeadmConfigSpec", "clusterConfiguration", "dns", "imageTag"),
   625  				fmt.Sprintf("cannot migrate CoreDNS up to '%v' from '%v': %v", toVersion, fromVersion, err),
   626  			),
   627  		)
   628  	}
   629  
   630  	return allErrs
   631  }
   632  
   633  func (webhook *KubeadmControlPlane) validateVersion(oldK, newK *controlplanev1.KubeadmControlPlane) (allErrs field.ErrorList) {
   634  	previousVersion := oldK.Spec.Version
   635  	fromVersion, err := version.ParseMajorMinorPatch(previousVersion)
   636  	if err != nil {
   637  		allErrs = append(allErrs,
   638  			field.InternalError(
   639  				field.NewPath("spec", "version"),
   640  				errors.Wrapf(err, "failed to parse current kubeadmcontrolplane version: %s", previousVersion),
   641  			),
   642  		)
   643  		return allErrs
   644  	}
   645  
   646  	toVersion, err := version.ParseMajorMinorPatch(newK.Spec.Version)
   647  	if err != nil {
   648  		allErrs = append(allErrs,
   649  			field.InternalError(
   650  				field.NewPath("spec", "version"),
   651  				errors.Wrapf(err, "failed to parse updated kubeadmcontrolplane version: %s", newK.Spec.Version),
   652  			),
   653  		)
   654  		return allErrs
   655  	}
   656  
   657  	// Check if we're trying to upgrade to Kubernetes v1.19.0, which is not supported.
   658  	//
   659  	// See https://github.com/kubernetes-sigs/cluster-api/issues/3564
   660  	if fromVersion.NE(toVersion) && toVersion.Equals(semver.MustParse("1.19.0")) {
   661  		allErrs = append(allErrs,
   662  			field.Forbidden(
   663  				field.NewPath("spec", "version"),
   664  				"cannot update Kubernetes version to v1.19.0, for more information see https://github.com/kubernetes-sigs/cluster-api/issues/3564",
   665  			),
   666  		)
   667  		return allErrs
   668  	}
   669  
   670  	// Validate that the update is upgrading at most one minor version.
   671  	// Note: Skipping a minor version is not allowed.
   672  	// Note: Checking against this ceilVersion allows upgrading to the next minor
   673  	// version irrespective of the patch version.
   674  	ceilVersion := semver.Version{
   675  		Major: fromVersion.Major,
   676  		Minor: fromVersion.Minor + 2,
   677  		Patch: 0,
   678  	}
   679  	if toVersion.GTE(ceilVersion) {
   680  		allErrs = append(allErrs,
   681  			field.Forbidden(
   682  				field.NewPath("spec", "version"),
   683  				fmt.Sprintf("cannot update Kubernetes version from %s to %s", previousVersion, newK.Spec.Version),
   684  			),
   685  		)
   686  	}
   687  
   688  	// The Kubernetes ecosystem has been requested to move users to the new registry due to cost issues.
   689  	// This validation enforces the move to the new registry by forcing users to upgrade to kubeadm versions
   690  	// with the new registry.
   691  	// NOTE: This only affects users relying on the community maintained registry.
   692  	// NOTE: Pinning to the upstream registry is not recommended because it could lead to issues
   693  	// given how the migration has been implemented in kubeadm.
   694  	//
   695  	// Block if imageRepository is not set (i.e. the default registry should be used),
   696  	if (newK.Spec.KubeadmConfigSpec.ClusterConfiguration == nil ||
   697  		newK.Spec.KubeadmConfigSpec.ClusterConfiguration.ImageRepository == "") &&
   698  		// the version changed (i.e. we have an upgrade),
   699  		toVersion.NE(fromVersion) &&
   700  		// the version is >= v1.22.0 and < v1.26.0
   701  		toVersion.GTE(kubeadm.MinKubernetesVersionImageRegistryMigration) &&
   702  		toVersion.LT(kubeadm.NextKubernetesVersionImageRegistryMigration) &&
   703  		// and the default registry of the new Kubernetes/kubeadm version is the old default registry.
   704  		kubeadm.GetDefaultRegistry(toVersion) == kubeadm.OldDefaultImageRepository {
   705  		allErrs = append(allErrs,
   706  			field.Forbidden(
   707  				field.NewPath("spec", "version"),
   708  				"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)",
   709  			),
   710  		)
   711  	}
   712  
   713  	return allErrs
   714  }
   715  
   716  // ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
   717  func (webhook *KubeadmControlPlane) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
   718  	return nil, nil
   719  }