sigs.k8s.io/cluster-api@v1.7.1/internal/topology/check/compatibility.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 check implements checks for managed topology. 18 package check 19 20 import ( 21 "fmt" 22 "strings" 23 24 "k8s.io/apimachinery/pkg/runtime/schema" 25 "k8s.io/apimachinery/pkg/util/sets" 26 "k8s.io/apimachinery/pkg/util/validation" 27 "k8s.io/apimachinery/pkg/util/validation/field" 28 "sigs.k8s.io/controller-runtime/pkg/client" 29 30 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 31 ) 32 33 // ObjectsAreStrictlyCompatible checks if two referenced objects are strictly compatible, meaning that 34 // they are compatible and the name of the objects do not change. 35 func ObjectsAreStrictlyCompatible(current, desired client.Object) field.ErrorList { 36 var allErrs field.ErrorList 37 if current.GetName() != desired.GetName() { 38 allErrs = append(allErrs, field.Forbidden( 39 field.NewPath("metadata", "name"), 40 fmt.Sprintf("metadata.name of %s/%s cannot be changed from %q to %q to prevent incompatible changes in the Cluster", 41 current.GetObjectKind().GroupVersionKind().GroupKind().String(), current.GetName(), current.GetName(), desired.GetName()), 42 )) 43 } 44 allErrs = append(allErrs, ObjectsAreCompatible(current, desired)...) 45 return allErrs 46 } 47 48 // ObjectsAreCompatible checks if two referenced objects are compatible, meaning that 49 // they are of the same GroupKind and in the same namespace. 50 func ObjectsAreCompatible(current, desired client.Object) field.ErrorList { 51 var allErrs field.ErrorList 52 53 currentGK := current.GetObjectKind().GroupVersionKind().GroupKind() 54 desiredGK := desired.GetObjectKind().GroupVersionKind().GroupKind() 55 if currentGK.Group != desiredGK.Group { 56 allErrs = append(allErrs, field.Forbidden( 57 field.NewPath("metadata", "apiVersion"), 58 fmt.Sprintf("apiVersion.group of %s/%s cannot be changed from %q to %q to prevent incompatible changes in the Cluster", 59 currentGK.String(), current.GetName(), currentGK.Group, desiredGK.Group), 60 )) 61 } 62 if currentGK.Kind != desiredGK.Kind { 63 allErrs = append(allErrs, field.Forbidden( 64 field.NewPath("metadata", "kind"), 65 fmt.Sprintf("apiVersion.kind of %s/%s cannot be changed from %q to %q to prevent incompatible changes in the Cluster", 66 currentGK.String(), current.GetName(), currentGK.Kind, desiredGK.Kind), 67 )) 68 } 69 allErrs = append(allErrs, ObjectsAreInTheSameNamespace(current, desired)...) 70 return allErrs 71 } 72 73 // ObjectsAreInTheSameNamespace checks if two referenced objects are in the same namespace. 74 func ObjectsAreInTheSameNamespace(current, desired client.Object) field.ErrorList { 75 var allErrs field.ErrorList 76 77 // NOTE: this should never happen (webhooks prevent it), but checking for extra safety. 78 if current.GetNamespace() != desired.GetNamespace() { 79 allErrs = append(allErrs, field.Forbidden( 80 field.NewPath("metadata", "namespace"), 81 fmt.Sprintf("metadata.namespace of %s/%s cannot be changed from %q to %q because templates must be in the same namespace as the Cluster", 82 current.GetObjectKind().GroupVersionKind().GroupKind().String(), current.GetName(), current.GetNamespace(), desired.GetNamespace()), 83 )) 84 } 85 return allErrs 86 } 87 88 // LocalObjectTemplatesAreCompatible checks if two referenced objects are compatible, meaning that 89 // they are of the same GroupKind and in the same namespace. 90 func LocalObjectTemplatesAreCompatible(current, desired clusterv1.LocalObjectTemplate, pathPrefix *field.Path) field.ErrorList { 91 var allErrs field.ErrorList 92 93 currentGK := current.Ref.GetObjectKind().GroupVersionKind().GroupKind() 94 desiredGK := desired.Ref.GetObjectKind().GroupVersionKind().GroupKind() 95 96 if currentGK.Group != desiredGK.Group { 97 allErrs = append(allErrs, field.Forbidden( 98 pathPrefix.Child("ref", "apiVersion"), 99 fmt.Sprintf("apiVersion.group cannot be changed from %q to %q to prevent incompatible changes in the Clusters", 100 currentGK.Group, desiredGK.Group), 101 )) 102 } 103 if currentGK.Kind != desiredGK.Kind { 104 allErrs = append(allErrs, field.Forbidden( 105 pathPrefix.Child("ref", "kind"), 106 fmt.Sprintf("apiVersion.kind cannot be changed from %q to %q to prevent incompatible changes in the Clusters", 107 currentGK.Kind, desiredGK.Kind), 108 )) 109 } 110 allErrs = append(allErrs, LocalObjectTemplatesAreInSameNamespace(current, desired, pathPrefix)...) 111 return allErrs 112 } 113 114 // LocalObjectTemplatesAreInSameNamespace checks if two referenced objects are in the same namespace. 115 func LocalObjectTemplatesAreInSameNamespace(current, desired clusterv1.LocalObjectTemplate, pathPrefix *field.Path) field.ErrorList { 116 var allErrs field.ErrorList 117 if current.Ref.Namespace != desired.Ref.Namespace { 118 allErrs = append(allErrs, field.Forbidden( 119 pathPrefix.Child("ref", "namespace"), 120 fmt.Sprintf("templates must be in the same namespace as the ClusterClass (%s)", 121 current.Ref.Namespace), 122 )) 123 } 124 return allErrs 125 } 126 127 // LocalObjectTemplateIsValid ensures the template is in the correct namespace, has no nil references, and has a valid Kind and GroupVersion. 128 func LocalObjectTemplateIsValid(template *clusterv1.LocalObjectTemplate, namespace string, pathPrefix *field.Path) field.ErrorList { 129 var allErrs field.ErrorList 130 131 // check if ref is not nil. 132 if template.Ref == nil { 133 return field.ErrorList{field.Required( 134 pathPrefix.Child("ref"), 135 "template reference must be defined", 136 )} 137 } 138 139 // check if a name is provided 140 if template.Ref.Name == "" { 141 allErrs = append(allErrs, 142 field.Required( 143 pathPrefix.Child("ref", "name"), 144 "template name must be defined", 145 ), 146 ) 147 } 148 149 // validate if namespace matches the provided namespace 150 if namespace != "" && template.Ref.Namespace != namespace { 151 allErrs = append( 152 allErrs, 153 field.Invalid( 154 pathPrefix.Child("ref", "namespace"), 155 template.Ref.Namespace, 156 fmt.Sprintf("template must be in the same namespace as the ClusterClass (%s)", namespace), 157 ), 158 ) 159 } 160 161 // check if kind is a template 162 if len(template.Ref.Kind) <= len(clusterv1.TemplateSuffix) || !strings.HasSuffix(template.Ref.Kind, clusterv1.TemplateSuffix) { 163 allErrs = append(allErrs, 164 field.Invalid( 165 pathPrefix.Child("ref", "kind"), 166 template.Ref.Kind, 167 fmt.Sprintf("template kind must be of form \"<name>%s\"", clusterv1.TemplateSuffix), 168 ), 169 ) 170 } 171 172 // check if apiVersion is valid 173 gv, err := schema.ParseGroupVersion(template.Ref.APIVersion) 174 if err != nil { 175 allErrs = append(allErrs, 176 field.Invalid( 177 pathPrefix.Child("ref", "apiVersion"), 178 template.Ref.APIVersion, 179 fmt.Sprintf("template apiVersion must be a valid Kubernetes apiVersion: %v", err), 180 ), 181 ) 182 } 183 if err == nil && gv.Empty() { 184 allErrs = append(allErrs, 185 field.Required( 186 pathPrefix.Child("ref", "apiVersion"), 187 "template apiVersion must be defined", 188 ), 189 ) 190 } 191 return allErrs 192 } 193 194 // ClusterClassesAreCompatible checks the compatibility between new and old versions of a Cluster Class. 195 // It checks that: 196 // 1) InfrastructureCluster Templates are compatible. 197 // 2) ControlPlane Templates are compatible. 198 // 3) ControlPlane InfrastructureMachineTemplates are compatible. 199 // 4) MachineDeploymentClasses are compatible. 200 // 5) MachinePoolClasses are compatible. 201 func ClusterClassesAreCompatible(current, desired *clusterv1.ClusterClass) field.ErrorList { 202 var allErrs field.ErrorList 203 if current == nil { 204 return append(allErrs, field.Invalid(field.NewPath(""), "", "could not compare ClusterClass compatibility: current ClusterClass must not be nil")) 205 } 206 if desired == nil { 207 return append(allErrs, field.Invalid(field.NewPath(""), "", "could not compare ClusterClass compatibility: desired ClusterClass must not be nil")) 208 } 209 210 // Validate InfrastructureClusterTemplate changes desired a compatible way. 211 allErrs = append(allErrs, LocalObjectTemplatesAreCompatible(current.Spec.Infrastructure, desired.Spec.Infrastructure, 212 field.NewPath("spec", "infrastructure"))...) 213 214 // Validate control plane changes desired a compatible way. 215 allErrs = append(allErrs, LocalObjectTemplatesAreCompatible(current.Spec.ControlPlane.LocalObjectTemplate, desired.Spec.ControlPlane.LocalObjectTemplate, 216 field.NewPath("spec", "controlPlane"))...) 217 if desired.Spec.ControlPlane.MachineInfrastructure != nil && current.Spec.ControlPlane.MachineInfrastructure != nil { 218 allErrs = append(allErrs, LocalObjectTemplatesAreCompatible(*current.Spec.ControlPlane.MachineInfrastructure, *desired.Spec.ControlPlane.MachineInfrastructure, 219 field.NewPath("spec", "controlPlane", "machineInfrastructure"))...) 220 } 221 222 // Validate changes to MachineDeployments. 223 allErrs = append(allErrs, MachineDeploymentClassesAreCompatible(current, desired)...) 224 225 // Validate changes to MachinePools. 226 allErrs = append(allErrs, MachinePoolClassesAreCompatible(current, desired)...) 227 228 return allErrs 229 } 230 231 // MachineDeploymentClassesAreCompatible checks if each MachineDeploymentClass in the new ClusterClass is a compatible change from the previous ClusterClass. 232 // It checks if the MachineDeploymentClass.Template.Infrastructure reference has changed its Group or Kind. 233 func MachineDeploymentClassesAreCompatible(current, desired *clusterv1.ClusterClass) field.ErrorList { 234 var allErrs field.ErrorList 235 236 // Ensure previous MachineDeployment class was modified in a compatible way. 237 for _, class := range desired.Spec.Workers.MachineDeployments { 238 for i, oldClass := range current.Spec.Workers.MachineDeployments { 239 if class.Class == oldClass.Class { 240 // NOTE: class.Template.Metadata and class.Template.Bootstrap are allowed to change; 241 242 // class.Template.Bootstrap is ensured syntactically correct by LocalObjectTemplateIsValid. 243 244 // Validates class.Template.Infrastructure template changes in a compatible way 245 allErrs = append(allErrs, LocalObjectTemplatesAreCompatible(oldClass.Template.Infrastructure, class.Template.Infrastructure, 246 field.NewPath("spec", "workers", "machineDeployments").Index(i))...) 247 } 248 } 249 } 250 return allErrs 251 } 252 253 // MachineDeploymentClassesAreUnique checks that no two MachineDeploymentClasses in a ClusterClass share a name. 254 func MachineDeploymentClassesAreUnique(clusterClass *clusterv1.ClusterClass) field.ErrorList { 255 var allErrs field.ErrorList 256 classes := sets.Set[string]{} 257 for i, class := range clusterClass.Spec.Workers.MachineDeployments { 258 if classes.Has(class.Class) { 259 allErrs = append(allErrs, 260 field.Invalid( 261 field.NewPath("spec", "workers", "machineDeployments").Index(i).Child("class"), 262 class.Class, 263 fmt.Sprintf("MachineDeployment class must be unique. MachineDeployment with class %q is defined more than once", class.Class), 264 ), 265 ) 266 } 267 classes.Insert(class.Class) 268 } 269 return allErrs 270 } 271 272 // MachinePoolClassesAreCompatible checks if each MachinePoolClass in the new ClusterClass is a compatible change from the previous ClusterClass. 273 // It checks if the MachinePoolClass.Template.Infrastructure reference has changed its Group or Kind. 274 func MachinePoolClassesAreCompatible(current, desired *clusterv1.ClusterClass) field.ErrorList { 275 var allErrs field.ErrorList 276 277 // Ensure previous MachinePool class was modified in a compatible way. 278 for _, class := range desired.Spec.Workers.MachinePools { 279 for i, oldClass := range current.Spec.Workers.MachinePools { 280 if class.Class == oldClass.Class { 281 // NOTE: class.Template.Metadata and class.Template.Bootstrap are allowed to change; 282 283 // class.Template.Bootstrap is ensured syntactically correct by LocalObjectTemplateIsValid. 284 285 // Validates class.Template.Infrastructure template changes in a compatible way 286 allErrs = append(allErrs, LocalObjectTemplatesAreCompatible(oldClass.Template.Infrastructure, class.Template.Infrastructure, 287 field.NewPath("spec", "workers", "machinePools").Index(i))...) 288 } 289 } 290 } 291 return allErrs 292 } 293 294 // MachinePoolClassesAreUnique checks that no two MachinePoolClasses in a ClusterClass share a name. 295 func MachinePoolClassesAreUnique(clusterClass *clusterv1.ClusterClass) field.ErrorList { 296 var allErrs field.ErrorList 297 classes := sets.Set[string]{} 298 for i, class := range clusterClass.Spec.Workers.MachinePools { 299 if classes.Has(class.Class) { 300 allErrs = append(allErrs, 301 field.Invalid( 302 field.NewPath("spec", "workers", "machinePools").Index(i).Child("class"), 303 class.Class, 304 fmt.Sprintf("MachinePool class must be unique. MachinePool with class %q is defined more than once", class.Class), 305 ), 306 ) 307 } 308 classes.Insert(class.Class) 309 } 310 return allErrs 311 } 312 313 // MachineDeploymentTopologiesAreValidAndDefinedInClusterClass checks that each MachineDeploymentTopology name is not empty 314 // and unique, and each class in use is defined in ClusterClass.spec.Workers.MachineDeployments. 315 func MachineDeploymentTopologiesAreValidAndDefinedInClusterClass(desired *clusterv1.Cluster, clusterClass *clusterv1.ClusterClass) field.ErrorList { 316 var allErrs field.ErrorList 317 if desired.Spec.Topology.Workers == nil { 318 return nil 319 } 320 if len(desired.Spec.Topology.Workers.MachineDeployments) == 0 { 321 return nil 322 } 323 // MachineDeployment clusterClass must be defined in the ClusterClass. 324 machineDeploymentClasses := mdClassNamesFromWorkerClass(clusterClass.Spec.Workers) 325 names := sets.Set[string]{} 326 for i, md := range desired.Spec.Topology.Workers.MachineDeployments { 327 if errs := validation.IsValidLabelValue(md.Name); len(errs) != 0 { 328 for _, err := range errs { 329 allErrs = append( 330 allErrs, 331 field.Invalid( 332 field.NewPath("spec", "topology", "workers", "machineDeployments").Index(i).Child("name"), 333 md.Name, 334 fmt.Sprintf("must be a valid label value %s", err), 335 ), 336 ) 337 } 338 } 339 340 if !machineDeploymentClasses.Has(md.Class) { 341 allErrs = append(allErrs, 342 field.Invalid( 343 field.NewPath("spec", "topology", "workers", "machineDeployments").Index(i).Child("class"), 344 md.Class, 345 fmt.Sprintf("MachineDeploymentClass with name %q does not exist in ClusterClass %q", 346 md.Class, clusterClass.Name), 347 ), 348 ) 349 } 350 351 // MachineDeploymentTopology name should not be empty. 352 if md.Name == "" { 353 allErrs = append( 354 allErrs, 355 field.Required( 356 field.NewPath("spec", "topology", "workers", "machineDeployments").Index(i).Child("name"), 357 "name must not be empty", 358 ), 359 ) 360 continue 361 } 362 363 if names.Has(md.Name) { 364 allErrs = append(allErrs, 365 field.Invalid( 366 field.NewPath("spec", "topology", "workers", "machineDeployments").Index(i).Child("name"), 367 md.Name, 368 fmt.Sprintf("name must be unique. MachineDeployment with name %q is defined more than once", md.Name), 369 ), 370 ) 371 } 372 names.Insert(md.Name) 373 } 374 return allErrs 375 } 376 377 // MachinePoolTopologiesAreValidAndDefinedInClusterClass checks that each MachinePoolTopology name is not empty 378 // and unique, and each class in use is defined in ClusterClass.spec.Workers.MachinePools. 379 func MachinePoolTopologiesAreValidAndDefinedInClusterClass(desired *clusterv1.Cluster, clusterClass *clusterv1.ClusterClass) field.ErrorList { 380 var allErrs field.ErrorList 381 if desired.Spec.Topology.Workers == nil { 382 return nil 383 } 384 if len(desired.Spec.Topology.Workers.MachinePools) == 0 { 385 return nil 386 } 387 // MachinePool clusterClass must be defined in the ClusterClass. 388 machinePoolClasses := mpClassNamesFromWorkerClass(clusterClass.Spec.Workers) 389 names := sets.Set[string]{} 390 for i, mp := range desired.Spec.Topology.Workers.MachinePools { 391 if errs := validation.IsValidLabelValue(mp.Name); len(errs) != 0 { 392 for _, err := range errs { 393 allErrs = append( 394 allErrs, 395 field.Invalid( 396 field.NewPath("spec", "topology", "workers", "machinePools").Index(i).Child("name"), 397 mp.Name, 398 fmt.Sprintf("must be a valid label value %s", err), 399 ), 400 ) 401 } 402 } 403 404 if !machinePoolClasses.Has(mp.Class) { 405 allErrs = append(allErrs, 406 field.Invalid( 407 field.NewPath("spec", "topology", "workers", "machinePools").Index(i).Child("class"), 408 mp.Class, 409 fmt.Sprintf("MachinePoolClass with name %q does not exist in ClusterClass %q", 410 mp.Class, clusterClass.Name), 411 ), 412 ) 413 } 414 415 // MachinePoolTopology name should not be empty. 416 if mp.Name == "" { 417 allErrs = append( 418 allErrs, 419 field.Required( 420 field.NewPath("spec", "topology", "workers", "machinePools").Index(i).Child("name"), 421 "name must not be empty", 422 ), 423 ) 424 continue 425 } 426 427 if names.Has(mp.Name) { 428 allErrs = append(allErrs, 429 field.Invalid( 430 field.NewPath("spec", "topology", "workers", "machinePools").Index(i).Child("name"), 431 mp.Name, 432 fmt.Sprintf("name must be unique. MachinePool with name %q is defined more than once", mp.Name), 433 ), 434 ) 435 } 436 names.Insert(mp.Name) 437 } 438 return allErrs 439 } 440 441 // ClusterClassReferencesAreValid checks that each template reference in the ClusterClass is valid . 442 func ClusterClassReferencesAreValid(clusterClass *clusterv1.ClusterClass) field.ErrorList { 443 var allErrs field.ErrorList 444 445 allErrs = append(allErrs, LocalObjectTemplateIsValid(&clusterClass.Spec.Infrastructure, clusterClass.Namespace, 446 field.NewPath("spec", "infrastructure"))...) 447 allErrs = append(allErrs, LocalObjectTemplateIsValid(&clusterClass.Spec.ControlPlane.LocalObjectTemplate, clusterClass.Namespace, 448 field.NewPath("spec", "controlPlane"))...) 449 if clusterClass.Spec.ControlPlane.MachineInfrastructure != nil { 450 allErrs = append(allErrs, LocalObjectTemplateIsValid(clusterClass.Spec.ControlPlane.MachineInfrastructure, clusterClass.Namespace, field.NewPath("spec", "controlPlane", "machineInfrastructure"))...) 451 } 452 453 for i := range clusterClass.Spec.Workers.MachineDeployments { 454 mdc := clusterClass.Spec.Workers.MachineDeployments[i] 455 allErrs = append(allErrs, LocalObjectTemplateIsValid(&mdc.Template.Bootstrap, clusterClass.Namespace, 456 field.NewPath("spec", "workers", "machineDeployments").Index(i).Child("template", "bootstrap"))...) 457 allErrs = append(allErrs, LocalObjectTemplateIsValid(&mdc.Template.Infrastructure, clusterClass.Namespace, 458 field.NewPath("spec", "workers", "machineDeployments").Index(i).Child("template", "infrastructure"))...) 459 } 460 461 for i := range clusterClass.Spec.Workers.MachinePools { 462 mpc := clusterClass.Spec.Workers.MachinePools[i] 463 allErrs = append(allErrs, LocalObjectTemplateIsValid(&mpc.Template.Bootstrap, clusterClass.Namespace, 464 field.NewPath("spec", "workers", "machinePools").Index(i).Child("template", "bootstrap"))...) 465 allErrs = append(allErrs, LocalObjectTemplateIsValid(&mpc.Template.Infrastructure, clusterClass.Namespace, 466 field.NewPath("spec", "workers", "machinePools").Index(i).Child("template", "infrastructure"))...) 467 } 468 469 return allErrs 470 } 471 472 // mdClassNamesFromWorkerClass returns the set of MachineDeployment class names. 473 func mdClassNamesFromWorkerClass(w clusterv1.WorkersClass) sets.Set[string] { 474 classes := sets.Set[string]{} 475 for _, class := range w.MachineDeployments { 476 classes.Insert(class.Class) 477 } 478 return classes 479 } 480 481 // mpClassNamesFromWorkerClass returns the set of MachinePool class names. 482 func mpClassNamesFromWorkerClass(w clusterv1.WorkersClass) sets.Set[string] { 483 classes := sets.Set[string]{} 484 for _, class := range w.MachinePools { 485 classes.Insert(class.Class) 486 } 487 return classes 488 }