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 }