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  }