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 }