sigs.k8s.io/cluster-api@v1.7.1/internal/webhooks/patch_validation.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 "encoding/json" 21 "fmt" 22 "strconv" 23 "strings" 24 "text/template" 25 26 "github.com/Masterminds/sprig/v3" 27 "github.com/pkg/errors" 28 corev1 "k8s.io/api/core/v1" 29 "k8s.io/apimachinery/pkg/util/sets" 30 "k8s.io/apimachinery/pkg/util/validation" 31 "k8s.io/apimachinery/pkg/util/validation/field" 32 33 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 34 "sigs.k8s.io/cluster-api/feature" 35 ) 36 37 // validatePatches returns errors if the Patches in the ClusterClass violate any validation rules. 38 func validatePatches(clusterClass *clusterv1.ClusterClass) field.ErrorList { 39 var allErrs field.ErrorList 40 names := sets.Set[string]{} 41 for i, patch := range clusterClass.Spec.Patches { 42 allErrs = append( 43 allErrs, 44 validatePatch(patch, names, clusterClass, field.NewPath("spec", "patches").Index(i))..., 45 ) 46 names.Insert(patch.Name) 47 } 48 return allErrs 49 } 50 51 func validatePatch(patch clusterv1.ClusterClassPatch, names sets.Set[string], clusterClass *clusterv1.ClusterClass, path *field.Path) field.ErrorList { 52 var allErrs field.ErrorList 53 allErrs = append(allErrs, 54 validatePatchName(patch, names, path)..., 55 ) 56 allErrs = append(allErrs, 57 validatePatchDefinitions(patch, clusterClass, path)..., 58 ) 59 return allErrs 60 } 61 62 func validatePatchName(patch clusterv1.ClusterClassPatch, names sets.Set[string], path *field.Path) field.ErrorList { 63 var allErrs field.ErrorList 64 if patch.Name == "" { 65 allErrs = append(allErrs, 66 field.Required( 67 path.Child("name"), 68 "patch name must be defined", 69 ), 70 ) 71 } 72 if patch.Name == clusterv1.VariableDefinitionFromInline { 73 allErrs = append(allErrs, 74 field.Required( 75 path.Child("name"), 76 fmt.Sprintf("%q can not be used as the name of a patch", clusterv1.VariableDefinitionFromInline), 77 ), 78 ) 79 } 80 81 if names.Has(patch.Name) { 82 allErrs = append(allErrs, 83 field.Invalid( 84 path.Child("name"), 85 patch.Name, 86 fmt.Sprintf("patch names must be unique. Patch with name %q is defined more than once", patch.Name), 87 ), 88 ) 89 } 90 return allErrs 91 } 92 93 func validatePatchDefinitions(patch clusterv1.ClusterClassPatch, clusterClass *clusterv1.ClusterClass, path *field.Path) field.ErrorList { 94 var allErrs field.ErrorList 95 96 allErrs = append(allErrs, validateEnabledIf(patch.EnabledIf, path.Child("enabledIf"))...) 97 98 if patch.Definitions == nil && patch.External == nil { 99 allErrs = append(allErrs, 100 field.Required( 101 path, 102 "one of definitions or external must be defined", 103 )) 104 } 105 106 if patch.Definitions != nil && patch.External != nil { 107 allErrs = append(allErrs, 108 field.Invalid( 109 path, 110 patch, 111 "only one of definitions or external can be defined", 112 )) 113 } 114 115 if patch.Definitions != nil { 116 for i, definition := range patch.Definitions { 117 allErrs = append(allErrs, 118 validateJSONPatches(definition.JSONPatches, clusterClass.Spec.Variables, path.Child("definitions").Index(i).Child("jsonPatches"))...) 119 allErrs = append(allErrs, 120 validateSelectors(definition.Selector, clusterClass, path.Child("definitions").Index(i).Child("selector"))...) 121 } 122 } 123 if patch.External != nil { 124 if !feature.Gates.Enabled(feature.RuntimeSDK) { 125 allErrs = append(allErrs, 126 field.Forbidden( 127 path.Child("external"), 128 "patch.external can be used only if the RuntimeSDK feature flag is enabled", 129 )) 130 } 131 if patch.External.ValidateExtension == nil && patch.External.GenerateExtension == nil { 132 allErrs = append(allErrs, 133 field.Invalid( 134 path.Child("external"), 135 patch.External, 136 "one of validateExtension and generateExtension must be defined", 137 )) 138 } 139 } 140 return allErrs 141 } 142 143 // validateSelectors validates if enabledIf is a valid template if it is set. 144 func validateEnabledIf(enabledIf *string, path *field.Path) field.ErrorList { 145 var allErrs field.ErrorList 146 147 if enabledIf != nil { 148 // Error if template can not be parsed. 149 _, err := template.New("enabledIf").Funcs(sprig.HermeticTxtFuncMap()).Parse(*enabledIf) 150 if err != nil { 151 allErrs = append(allErrs, 152 field.Invalid( 153 path, 154 *enabledIf, 155 fmt.Sprintf("template can not be parsed: %v", err), 156 )) 157 } 158 } 159 160 return allErrs 161 } 162 163 // validateSelectors tests to see if the selector matches any template in the ClusterClass. 164 // It returns nil as soon as it finds any matching template and an error if there is no match. 165 func validateSelectors(selector clusterv1.PatchSelector, class *clusterv1.ClusterClass, path *field.Path) field.ErrorList { 166 var allErrs field.ErrorList 167 168 // Return an error if none of the possible selectors are enabled. 169 if !(selector.MatchResources.InfrastructureCluster || selector.MatchResources.ControlPlane || 170 (selector.MatchResources.MachineDeploymentClass != nil && len(selector.MatchResources.MachineDeploymentClass.Names) > 0) || 171 (selector.MatchResources.MachinePoolClass != nil && len(selector.MatchResources.MachinePoolClass.Names) > 0)) { 172 return append(allErrs, 173 field.Invalid( 174 path, 175 prettyPrint(selector), 176 "no selector enabled", 177 )) 178 } 179 180 if selector.MatchResources.InfrastructureCluster { 181 if !selectorMatchTemplate(selector, class.Spec.Infrastructure.Ref) { 182 allErrs = append(allErrs, field.Invalid( 183 path.Child("matchResources", "infrastructureCluster"), 184 selector.MatchResources.InfrastructureCluster, 185 "selector is enabled but does not match the infrastructure ref", 186 )) 187 } 188 } 189 190 if selector.MatchResources.ControlPlane { 191 match := false 192 if selectorMatchTemplate(selector, class.Spec.ControlPlane.Ref) { 193 match = true 194 } 195 if class.Spec.ControlPlane.MachineInfrastructure != nil && 196 selectorMatchTemplate(selector, class.Spec.ControlPlane.MachineInfrastructure.Ref) { 197 match = true 198 } 199 if !match { 200 allErrs = append(allErrs, field.Invalid( 201 path.Child("matchResources", "controlPlane"), 202 selector.MatchResources.ControlPlane, 203 "selector is enabled but matches neither the controlPlane ref nor the controlPlane machineInfrastructure ref", 204 )) 205 } 206 } 207 208 if selector.MatchResources.MachineDeploymentClass != nil && len(selector.MatchResources.MachineDeploymentClass.Names) > 0 { 209 for i, name := range selector.MatchResources.MachineDeploymentClass.Names { 210 match := false 211 err := validateSelectorName(name, path, "machineDeploymentClass", i) 212 if err != nil { 213 allErrs = append(allErrs, err) 214 break 215 } 216 for _, md := range class.Spec.Workers.MachineDeployments { 217 var matches bool 218 if md.Class == name || name == "*" { 219 matches = true 220 } else if strings.HasPrefix(name, "*") && strings.HasSuffix(md.Class, strings.TrimPrefix(name, "*")) { 221 matches = true 222 } else if strings.HasSuffix(name, "*") && strings.HasPrefix(md.Class, strings.TrimSuffix(name, "*")) { 223 matches = true 224 } 225 226 if matches { 227 if selectorMatchTemplate(selector, md.Template.Infrastructure.Ref) || 228 selectorMatchTemplate(selector, md.Template.Bootstrap.Ref) { 229 match = true 230 break 231 } 232 } 233 } 234 if !match { 235 allErrs = append(allErrs, field.Invalid( 236 path.Child("matchResources", "machineDeploymentClass", "names").Index(i), 237 name, 238 "selector is enabled but matches neither the bootstrap ref nor the infrastructure ref of a MachineDeployment class", 239 )) 240 } 241 } 242 } 243 244 if selector.MatchResources.MachinePoolClass != nil && len(selector.MatchResources.MachinePoolClass.Names) > 0 { 245 for i, name := range selector.MatchResources.MachinePoolClass.Names { 246 match := false 247 err := validateSelectorName(name, path, "machinePoolClass", i) 248 if err != nil { 249 allErrs = append(allErrs, err) 250 break 251 } 252 for _, mp := range class.Spec.Workers.MachinePools { 253 var matches bool 254 if mp.Class == name || name == "*" { 255 matches = true 256 } else if strings.HasPrefix(name, "*") && strings.HasSuffix(mp.Class, strings.TrimPrefix(name, "*")) { 257 matches = true 258 } else if strings.HasSuffix(name, "*") && strings.HasPrefix(mp.Class, strings.TrimSuffix(name, "*")) { 259 matches = true 260 } 261 262 if matches { 263 if selectorMatchTemplate(selector, mp.Template.Infrastructure.Ref) || 264 selectorMatchTemplate(selector, mp.Template.Bootstrap.Ref) { 265 match = true 266 break 267 } 268 } 269 } 270 if !match { 271 allErrs = append(allErrs, field.Invalid( 272 path.Child("matchResources", "machinePoolClass", "names").Index(i), 273 name, 274 "selector is enabled but matches neither the bootstrap ref nor the infrastructure ref of a MachinePool class", 275 )) 276 } 277 } 278 } 279 280 return allErrs 281 } 282 283 // validateSelectorName validates if the selector name is valid. 284 func validateSelectorName(name string, path *field.Path, resourceName string, index int) *field.Error { 285 if strings.Contains(name, "*") { 286 // selector can at most have a single * rune 287 if strings.Count(name, "*") > 1 { 288 return field.Invalid( 289 path.Child("matchResources", resourceName, "names").Index(index), 290 name, 291 "selector can at most contain a single \"*\" rune") 292 } 293 294 // the * rune can appear only at the beginning, or ending of the selector. 295 if strings.Contains(name, "*") && !(strings.HasPrefix(name, "*") || strings.HasSuffix(name, "*")) { 296 // templateMDClass or templateMPClass can only have "*" rune at the start or end of the string 297 return field.Invalid( 298 path.Child("matchResources", resourceName, "names").Index(index), 299 name, 300 "\"*\" rune can only appear at the beginning, or ending of the selector") 301 } 302 // a valid selector without "*" should comply with Kubernetes naming standards. 303 if validation.IsQualifiedName(strings.ReplaceAll(name, "*", "a")) != nil { 304 return field.Invalid( 305 path.Child("matchResources", resourceName, "names").Index(index), 306 name, 307 "selector does not comply with the Kubernetes naming standards") 308 } 309 } 310 return nil 311 } 312 313 // selectorMatchTemplate returns true if APIVersion and Kind for the given selector match the reference. 314 func selectorMatchTemplate(selector clusterv1.PatchSelector, reference *corev1.ObjectReference) bool { 315 if reference == nil { 316 return false 317 } 318 return selector.Kind == reference.Kind && selector.APIVersion == reference.APIVersion 319 } 320 321 var validOps = sets.Set[string]{}.Insert("add", "replace", "remove") 322 323 func validateJSONPatches(jsonPatches []clusterv1.JSONPatch, variables []clusterv1.ClusterClassVariable, path *field.Path) field.ErrorList { 324 var allErrs field.ErrorList 325 variableSet, _ := getClusterClassVariablesMapWithReverseIndex(variables) 326 327 for i, jsonPatch := range jsonPatches { 328 if !validOps.Has(jsonPatch.Op) { 329 allErrs = append(allErrs, 330 field.NotSupported( 331 path.Index(i).Child("op"), 332 prettyPrint(jsonPatch), 333 sets.List(validOps), 334 )) 335 } 336 337 if !strings.HasPrefix(jsonPatch.Path, "/spec/") { 338 allErrs = append(allErrs, 339 field.Invalid( 340 path.Index(i).Child("path"), 341 prettyPrint(jsonPatch), 342 "jsonPatch path must start with \"/spec/\"", 343 )) 344 } 345 346 // Validate that array access is only prepend or append for add and not allowed for replace or remove. 347 allErrs = append(allErrs, 348 validateIndexAccess(jsonPatch, path.Index(i).Child("path"))..., 349 ) 350 351 // Validate the value and valueFrom fields for the patch. 352 allErrs = append(allErrs, 353 validateJSONPatchValues(jsonPatch, variableSet, path.Index(i))..., 354 ) 355 } 356 return allErrs 357 } 358 359 func validateJSONPatchValues(jsonPatch clusterv1.JSONPatch, variableSet map[string]*clusterv1.ClusterClassVariable, path *field.Path) field.ErrorList { 360 var allErrs field.ErrorList 361 362 // move to the next variable if the jsonPatch does not have "replace" or "add" op. Additional validation is not needed. 363 if jsonPatch.Op != "add" && jsonPatch.Op != "replace" { 364 return allErrs 365 } 366 367 if jsonPatch.Value == nil && jsonPatch.ValueFrom == nil { 368 allErrs = append(allErrs, 369 field.Invalid( 370 path, 371 prettyPrint(jsonPatch), 372 "jsonPatch must define one of value or valueFrom", 373 )) 374 } 375 376 if jsonPatch.Value != nil && jsonPatch.ValueFrom != nil { 377 allErrs = append(allErrs, 378 field.Invalid( 379 path, 380 prettyPrint(jsonPatch), 381 "jsonPatch can not define both value and valueFrom", 382 )) 383 } 384 385 // Attempt to marshal the JSON to discover if it is valid. If jsonPatch.Value.Raw is set to nil skip this check 386 // and accept the nil value. 387 if jsonPatch.Value != nil && jsonPatch.Value.Raw != nil { 388 var v interface{} 389 if err := json.Unmarshal(jsonPatch.Value.Raw, &v); err != nil { 390 allErrs = append(allErrs, 391 field.Invalid( 392 path.Child("value"), 393 string(jsonPatch.Value.Raw), 394 "jsonPatch Value is invalid JSON", 395 )) 396 } 397 } 398 if jsonPatch.ValueFrom != nil && jsonPatch.ValueFrom.Template == nil && jsonPatch.ValueFrom.Variable == nil { 399 allErrs = append(allErrs, 400 field.Invalid( 401 path.Child("valueFrom"), 402 prettyPrint(jsonPatch.ValueFrom), 403 "valueFrom must set either template or variable", 404 )) 405 } 406 if jsonPatch.ValueFrom != nil && jsonPatch.ValueFrom.Template != nil && jsonPatch.ValueFrom.Variable != nil { 407 allErrs = append(allErrs, 408 field.Invalid( 409 path.Child("valueFrom"), 410 prettyPrint(jsonPatch.ValueFrom), 411 "valueFrom can not set both template and variable", 412 )) 413 } 414 415 if jsonPatch.ValueFrom != nil && jsonPatch.ValueFrom.Template != nil { 416 // Error if template can not be parsed. 417 _, err := template.New("valueFrom.template").Funcs(sprig.HermeticTxtFuncMap()).Parse(*jsonPatch.ValueFrom.Template) 418 if err != nil { 419 allErrs = append(allErrs, 420 field.Invalid( 421 path.Child("valueFrom", "template"), 422 *jsonPatch.ValueFrom.Template, 423 fmt.Sprintf("template can not be parsed: %v", err), 424 )) 425 } 426 } 427 428 // If set validate that the variable is valid. 429 if jsonPatch.ValueFrom != nil && jsonPatch.ValueFrom.Variable != nil { 430 // If the variable is one of the list of builtin variables it's valid. 431 if strings.HasPrefix(*jsonPatch.ValueFrom.Variable, "builtin.") { 432 if _, ok := builtinVariables[*jsonPatch.ValueFrom.Variable]; !ok { 433 allErrs = append(allErrs, 434 field.Invalid( 435 path.Child("valueFrom", "variable"), 436 *jsonPatch.ValueFrom.Variable, 437 "not a defined builtin variable", 438 )) 439 } 440 } else { 441 // Note: We're only validating if the variable name exists without 442 // validating if the whole path is an existing variable. 443 // This could be done by re-using getVariableValue of the json patch 444 // generator but requires a refactoring first. 445 variableName := getVariableName(*jsonPatch.ValueFrom.Variable) 446 if _, ok := variableSet[variableName]; !ok { 447 allErrs = append(allErrs, 448 field.Invalid( 449 path.Child("valueFrom", "variable"), 450 *jsonPatch.ValueFrom.Variable, 451 fmt.Sprintf("variable with name %s cannot be found", *jsonPatch.ValueFrom.Variable), 452 )) 453 } 454 } 455 } 456 return allErrs 457 } 458 459 func getVariableName(variable string) string { 460 return strings.FieldsFunc(variable, func(r rune) bool { 461 return r == '[' || r == '.' 462 })[0] 463 } 464 465 // This contains a list of all of the valid builtin variables. 466 // TODO(killianmuldoon): Match this list to controllers/topology/internal/extensions/patches/variables as those structs become available across the code base i.e. public or top-level internal. 467 var builtinVariables = sets.Set[string]{}.Insert( 468 "builtin", 469 470 // Cluster builtins. 471 "builtin.cluster", 472 "builtin.cluster.name", 473 "builtin.cluster.namespace", 474 475 // ClusterTopology builtins. 476 "builtin.cluster.topology", 477 "builtin.cluster.topology.class", 478 "builtin.cluster.topology.version", 479 480 // ClusterNetwork builtins 481 "builtin.cluster.network", 482 "builtin.cluster.network.serviceDomain", 483 "builtin.cluster.network.services", 484 "builtin.cluster.network.pods", 485 "builtin.cluster.network.ipFamily", 486 487 // ControlPlane builtins. 488 "builtin.controlPlane", 489 "builtin.controlPlane.name", 490 "builtin.controlPlane.replicas", 491 "builtin.controlPlane.version", 492 // ControlPlane ref builtins. 493 "builtin.controlPlane.machineTemplate.infrastructureRef.name", 494 495 // MachineDeployment builtins. 496 "builtin.machineDeployment", 497 "builtin.machineDeployment.class", 498 "builtin.machineDeployment.name", 499 "builtin.machineDeployment.replicas", 500 "builtin.machineDeployment.topologyName", 501 "builtin.machineDeployment.version", 502 // MachineDeployment ref builtins. 503 "builtin.machineDeployment.bootstrap.configRef.name", 504 "builtin.machineDeployment.infrastructureRef.name", 505 506 // MachinePool builtins. 507 "builtin.machinePool", 508 "builtin.machinePool.class", 509 "builtin.machinePool.name", 510 "builtin.machinePool.replicas", 511 "builtin.machinePool.topologyName", 512 "builtin.machinePool.version", 513 // MachinePool ref builtins. 514 "builtin.machinePool.bootstrap.configRef.name", 515 "builtin.machinePool.infrastructureRef.name", 516 ) 517 518 // validateIndexAccess checks to see if the jsonPath is attempting to add an element in the array i.e. access by number 519 // If the operation is add an error is thrown if a number greater than 0 is used as an index. 520 // If the operation is replace an error is thrown if an index is used. 521 func validateIndexAccess(jsonPatch clusterv1.JSONPatch, path *field.Path) field.ErrorList { 522 var allErrs field.ErrorList 523 524 pathParts := strings.Split(jsonPatch.Path, "/") 525 for _, part := range pathParts { 526 // Check if the path segment is a valid number. If an error is thrown continue to the next segment. 527 index, err := strconv.Atoi(part) 528 if err != nil { 529 continue 530 } 531 532 // If the operation is add an error is thrown if a number greater than 0 is used as an index. 533 if jsonPatch.Op == "add" && index != 0 { 534 allErrs = append(allErrs, 535 field.Invalid(path, 536 jsonPatch.Path, 537 "arrays can only be accessed using \"0\" (prepend) or \"-\" (append)", 538 )) 539 } 540 541 // If the jsonPatch operation is replace or remove disallow any number as an element in the path. 542 if jsonPatch.Op == "replace" || jsonPatch.Op == "remove" { 543 allErrs = append(allErrs, 544 field.Invalid(path, 545 jsonPatch.Path, 546 fmt.Sprintf("elements in arrays can not be accessed in a %s operation", jsonPatch.Op), 547 )) 548 } 549 } 550 return allErrs 551 } 552 553 func prettyPrint(v interface{}) string { 554 b, err := json.MarshalIndent(v, "", " ") 555 if err != nil { 556 return errors.Wrapf(err, "failed to marshal field value").Error() 557 } 558 return string(b) 559 }