sigs.k8s.io/cluster-api@v1.6.3/internal/controllers/topology/cluster/patches/inline/json_patch_generator.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 inline implements the inline JSON patch generator.
    18  package inline
    19  
    20  import (
    21  	"bytes"
    22  	"context"
    23  	"encoding/json"
    24  	"strconv"
    25  	"strings"
    26  	"text/template"
    27  
    28  	"github.com/Masterminds/sprig/v3"
    29  	"github.com/pkg/errors"
    30  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    31  	kerrors "k8s.io/apimachinery/pkg/util/errors"
    32  	"sigs.k8s.io/controller-runtime/pkg/client"
    33  	"sigs.k8s.io/yaml"
    34  
    35  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    36  	runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
    37  	"sigs.k8s.io/cluster-api/internal/contract"
    38  	"sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/patches/api"
    39  	patchvariables "sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/patches/variables"
    40  )
    41  
    42  // jsonPatchGenerator generates JSON patches for a GeneratePatchesRequest based on a ClusterClassPatch.
    43  type jsonPatchGenerator struct {
    44  	patch *clusterv1.ClusterClassPatch
    45  }
    46  
    47  // NewGenerator returns a new inline Generator from a given ClusterClassPatch object.
    48  func NewGenerator(patch *clusterv1.ClusterClassPatch) api.Generator {
    49  	return &jsonPatchGenerator{
    50  		patch: patch,
    51  	}
    52  }
    53  
    54  // Generate generates JSON patches for the given GeneratePatchesRequest based on a ClusterClassPatch.
    55  func (j *jsonPatchGenerator) Generate(_ context.Context, _ client.Object, req *runtimehooksv1.GeneratePatchesRequest) (*runtimehooksv1.GeneratePatchesResponse, error) {
    56  	resp := &runtimehooksv1.GeneratePatchesResponse{}
    57  
    58  	globalVariables := patchvariables.ToMap(req.Variables)
    59  
    60  	// Loop over all templates.
    61  	errs := []error{}
    62  	for i := range req.Items {
    63  		item := &req.Items[i]
    64  		objectKind := item.Object.Object.GetObjectKind().GroupVersionKind().Kind
    65  
    66  		templateVariables := patchvariables.ToMap(item.Variables)
    67  
    68  		// Calculate the list of patches which match the current template.
    69  		matchingPatches := []clusterv1.PatchDefinition{}
    70  		for _, patch := range j.patch.Definitions {
    71  			// Add the patch to the list, if it matches the template.
    72  			if matchesSelector(item, templateVariables, patch.Selector) {
    73  				matchingPatches = append(matchingPatches, patch)
    74  			}
    75  		}
    76  
    77  		// Continue if there are no matching patches.
    78  		if len(matchingPatches) == 0 {
    79  			continue
    80  		}
    81  
    82  		// Merge template-specific and global variables.
    83  		variables, err := patchvariables.MergeVariableMaps(globalVariables, templateVariables)
    84  		if err != nil {
    85  			errs = append(errs, errors.Wrapf(err, "failed to merge global and template-specific variables for %q", objectKind))
    86  			continue
    87  		}
    88  
    89  		enabled, err := patchIsEnabled(j.patch.EnabledIf, variables)
    90  		if err != nil {
    91  			errs = append(errs, errors.Wrapf(err, "failed to calculate if patch is enabled for %q", objectKind))
    92  			continue
    93  		}
    94  		if !enabled {
    95  			// Continue if patch is not enabled.
    96  			continue
    97  		}
    98  
    99  		// Loop over all PatchDefinitions.
   100  		for _, patch := range matchingPatches {
   101  			// Generate JSON patches.
   102  			jsonPatches, err := generateJSONPatches(patch.JSONPatches, variables)
   103  			if err != nil {
   104  				errs = append(errs, errors.Wrapf(err, "failed to generate JSON patches for %q", objectKind))
   105  				continue
   106  			}
   107  
   108  			// Add jsonPatches to the response.
   109  			resp.Items = append(resp.Items, runtimehooksv1.GeneratePatchesResponseItem{
   110  				UID:       item.UID,
   111  				Patch:     jsonPatches,
   112  				PatchType: runtimehooksv1.JSONPatchType,
   113  			})
   114  		}
   115  	}
   116  
   117  	if err := kerrors.NewAggregate(errs); err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	return resp, nil
   122  }
   123  
   124  // matchesSelector returns true if the GeneratePatchesRequestItem matches the selector.
   125  func matchesSelector(req *runtimehooksv1.GeneratePatchesRequestItem, templateVariables map[string]apiextensionsv1.JSON, selector clusterv1.PatchSelector) bool {
   126  	gvk := req.Object.Object.GetObjectKind().GroupVersionKind()
   127  
   128  	// Check if the apiVersion and kind are matching.
   129  	if gvk.GroupVersion().String() != selector.APIVersion {
   130  		return false
   131  	}
   132  	if gvk.Kind != selector.Kind {
   133  		return false
   134  	}
   135  
   136  	// Check if the request is for an InfrastructureCluster.
   137  	if selector.MatchResources.InfrastructureCluster {
   138  		// Cluster.spec.infrastructureRef holds the InfrastructureCluster.
   139  		if req.HolderReference.Kind == "Cluster" && req.HolderReference.FieldPath == "spec.infrastructureRef" {
   140  			return true
   141  		}
   142  	}
   143  
   144  	// Check if the request is for a ControlPlane or the InfrastructureMachineTemplate of a ControlPlane.
   145  	if selector.MatchResources.ControlPlane {
   146  		// Cluster.spec.controlPlaneRef holds the ControlPlane.
   147  		if req.HolderReference.Kind == "Cluster" && req.HolderReference.FieldPath == "spec.controlPlaneRef" {
   148  			return true
   149  		}
   150  		// *.spec.machineTemplate.infrastructureRef holds the InfrastructureMachineTemplate of a ControlPlane.
   151  		// Note: this field path is only used in this context.
   152  		if req.HolderReference.FieldPath == strings.Join(contract.ControlPlane().MachineTemplate().InfrastructureRef().Path(), ".") {
   153  			return true
   154  		}
   155  	}
   156  
   157  	// Check if the request is for a BootstrapConfigTemplate or an InfrastructureMachineTemplate
   158  	// of one of the configured MachineDeploymentClasses.
   159  	if selector.MatchResources.MachineDeploymentClass != nil {
   160  		// MachineDeployment.spec.template.spec.bootstrap.configRef or
   161  		// MachineDeployment.spec.template.spec.infrastructureRef holds the BootstrapConfigTemplate or
   162  		// InfrastructureMachineTemplate.
   163  		if req.HolderReference.Kind == "MachineDeployment" &&
   164  			(req.HolderReference.FieldPath == "spec.template.spec.bootstrap.configRef" ||
   165  				req.HolderReference.FieldPath == "spec.template.spec.infrastructureRef") {
   166  			// Read the builtin.machineDeployment.class variable.
   167  			templateMDClassJSON, err := patchvariables.GetVariableValue(templateVariables, "builtin.machineDeployment.class")
   168  
   169  			// If the builtin variable could be read.
   170  			if err == nil {
   171  				// If templateMDClass matches one of the configured MachineDeploymentClasses.
   172  				for _, mdClass := range selector.MatchResources.MachineDeploymentClass.Names {
   173  					// We have to quote mdClass as templateMDClassJSON is a JSON string (e.g. "default-worker").
   174  					if mdClass == "*" || string(templateMDClassJSON.Raw) == strconv.Quote(mdClass) {
   175  						return true
   176  					}
   177  					unquoted, _ := strconv.Unquote(string(templateMDClassJSON.Raw))
   178  					if strings.HasPrefix(mdClass, "*") && strings.HasSuffix(unquoted, strings.TrimPrefix(mdClass, "*")) {
   179  						return true
   180  					}
   181  					if strings.HasSuffix(mdClass, "*") && strings.HasPrefix(unquoted, strings.TrimSuffix(mdClass, "*")) {
   182  						return true
   183  					}
   184  				}
   185  			}
   186  		}
   187  	}
   188  
   189  	// Check if the request is for a BootstrapConfigTemplate or an InfrastructureMachinePoolTemplate
   190  	// of one of the configured MachinePoolClasses.
   191  	if selector.MatchResources.MachinePoolClass != nil {
   192  		if req.HolderReference.Kind == "MachinePool" &&
   193  			(req.HolderReference.FieldPath == "spec.template.spec.bootstrap.configRef" ||
   194  				req.HolderReference.FieldPath == "spec.template.spec.infrastructureRef") {
   195  			// Read the builtin.machinePool.class variable.
   196  			templateMPClassJSON, err := patchvariables.GetVariableValue(templateVariables, "builtin.machinePool.class")
   197  
   198  			// If the builtin variable could be read.
   199  			if err == nil {
   200  				// If templateMPClass matches one of the configured MachinePoolClasses.
   201  				for _, mpClass := range selector.MatchResources.MachinePoolClass.Names {
   202  					// We have to quote mpClass as templateMPClassJSON is a JSON string (e.g. "default-worker").
   203  					if mpClass == "*" || string(templateMPClassJSON.Raw) == strconv.Quote(mpClass) {
   204  						return true
   205  					}
   206  					unquoted, _ := strconv.Unquote(string(templateMPClassJSON.Raw))
   207  					if strings.HasPrefix(mpClass, "*") && strings.HasSuffix(unquoted, strings.TrimPrefix(mpClass, "*")) {
   208  						return true
   209  					}
   210  					if strings.HasSuffix(mpClass, "*") && strings.HasPrefix(unquoted, strings.TrimSuffix(mpClass, "*")) {
   211  						return true
   212  					}
   213  				}
   214  			}
   215  		}
   216  	}
   217  
   218  	return false
   219  }
   220  
   221  func patchIsEnabled(enabledIf *string, variables map[string]apiextensionsv1.JSON) (bool, error) {
   222  	// If enabledIf is not set, patch is enabled.
   223  	if enabledIf == nil {
   224  		return true, nil
   225  	}
   226  
   227  	// Rendered template.
   228  	value, err := renderValueTemplate(*enabledIf, variables)
   229  	if err != nil {
   230  		return false, errors.Wrapf(err, "failed to calculate value for enabledIf")
   231  	}
   232  
   233  	// Patch is enabled if the rendered template value is `true`.
   234  	return bytes.Equal(value.Raw, []byte(`true`)), nil
   235  }
   236  
   237  // jsonPatchRFC6902 is used to render the generated JSONPatches.
   238  type jsonPatchRFC6902 struct {
   239  	Op    string                `json:"op"`
   240  	Path  string                `json:"path"`
   241  	Value *apiextensionsv1.JSON `json:"value,omitempty"`
   242  }
   243  
   244  // generateJSONPatches generates JSON patches based on the given JSONPatches and variables.
   245  func generateJSONPatches(jsonPatches []clusterv1.JSONPatch, variables map[string]apiextensionsv1.JSON) ([]byte, error) {
   246  	res := []jsonPatchRFC6902{}
   247  
   248  	for _, jsonPatch := range jsonPatches {
   249  		var value *apiextensionsv1.JSON
   250  		if jsonPatch.Op == "add" || jsonPatch.Op == "replace" {
   251  			var err error
   252  			value, err = calculateValue(jsonPatch, variables)
   253  			if err != nil {
   254  				return nil, err
   255  			}
   256  		}
   257  
   258  		res = append(res, jsonPatchRFC6902{
   259  			Op:    jsonPatch.Op,
   260  			Path:  jsonPatch.Path,
   261  			Value: value,
   262  		})
   263  	}
   264  
   265  	// Render JSON Patches.
   266  	resJSON, err := json.Marshal(res)
   267  	if err != nil {
   268  		return nil, errors.Wrapf(err, "failed to marshal JSON Patch %v", jsonPatches)
   269  	}
   270  
   271  	return resJSON, nil
   272  }
   273  
   274  // calculateValue calculates a value for a JSON patch.
   275  func calculateValue(patch clusterv1.JSONPatch, variables map[string]apiextensionsv1.JSON) (*apiextensionsv1.JSON, error) {
   276  	// Return if values are set incorrectly.
   277  	if patch.Value == nil && patch.ValueFrom == nil {
   278  		return nil, errors.Errorf("failed to calculate value: neither .value nor .valueFrom are set")
   279  	}
   280  	if patch.Value != nil && patch.ValueFrom != nil {
   281  		return nil, errors.Errorf("failed to calculate value: both .value and .valueFrom are set")
   282  	}
   283  	if patch.ValueFrom != nil && patch.ValueFrom.Variable == nil && patch.ValueFrom.Template == nil {
   284  		return nil, errors.Errorf("failed to calculate value: .valueFrom is set, but neither .valueFrom.variable nor .valueFrom.template are set")
   285  	}
   286  	if patch.ValueFrom != nil && patch.ValueFrom.Variable != nil && patch.ValueFrom.Template != nil {
   287  		return nil, errors.Errorf("failed to calculate value: .valueFrom is set, but both .valueFrom.variable and .valueFrom.template are set")
   288  	}
   289  
   290  	// Return raw value.
   291  	if patch.Value != nil {
   292  		return patch.Value, nil
   293  	}
   294  
   295  	// Return variable.
   296  	if patch.ValueFrom.Variable != nil {
   297  		value, err := patchvariables.GetVariableValue(variables, *patch.ValueFrom.Variable)
   298  		if err != nil {
   299  			return nil, errors.Wrapf(err, "failed to calculate value")
   300  		}
   301  		return value, nil
   302  	}
   303  
   304  	// Return rendered value template.
   305  	value, err := renderValueTemplate(*patch.ValueFrom.Template, variables)
   306  	if err != nil {
   307  		return nil, errors.Wrapf(err, "failed to calculate value for template")
   308  	}
   309  	return value, nil
   310  }
   311  
   312  // renderValueTemplate renders a template with the given variables as data.
   313  func renderValueTemplate(valueTemplate string, variables map[string]apiextensionsv1.JSON) (*apiextensionsv1.JSON, error) {
   314  	// Parse the template.
   315  	tpl, err := template.New("tpl").Funcs(sprig.HermeticTxtFuncMap()).Parse(valueTemplate)
   316  	if err != nil {
   317  		return nil, errors.Wrapf(err, "failed to parse template: %q", valueTemplate)
   318  	}
   319  
   320  	// Convert the flat variables map in a nested map, so that variables can be
   321  	// consumed in templates like this: `{{ .builtin.cluster.name }}`
   322  	// NOTE: Variable values are also converted to their Go types as
   323  	// they cannot be directly consumed as byte arrays.
   324  	data, err := calculateTemplateData(variables)
   325  	if err != nil {
   326  		return nil, errors.Wrap(err, "failed to calculate template data")
   327  	}
   328  
   329  	// Render the template.
   330  	var buf bytes.Buffer
   331  	if err := tpl.Execute(&buf, data); err != nil {
   332  		return nil, errors.Wrapf(err, "failed to render template: %q", valueTemplate)
   333  	}
   334  
   335  	// Unmarshal the rendered template.
   336  	// NOTE: The YAML library is used for unmarshalling, to be able to handle YAML and JSON.
   337  	value := apiextensionsv1.JSON{}
   338  	if err := yaml.Unmarshal(buf.Bytes(), &value); err != nil {
   339  		return nil, errors.Wrapf(err, "failed to unmarshal rendered template: %q", buf.String())
   340  	}
   341  
   342  	return &value, nil
   343  }
   344  
   345  // calculateTemplateData calculates data for the template, by converting
   346  // the variables to their Go types.
   347  // Example:
   348  //   - Input:
   349  //     map[string]apiextensionsv1.JSON{
   350  //     "builtin": {Raw: []byte(`{"cluster":{"name":"cluster-name"}}`},
   351  //     "integerVariable": {Raw: []byte("4")},
   352  //     "numberVariable": {Raw: []byte("2.5")},
   353  //     "booleanVariable": {Raw: []byte("true")},
   354  //     }
   355  //   - Output:
   356  //     map[string]interface{}{
   357  //     "builtin": map[string]interface{}{
   358  //     "cluster": map[string]interface{}{
   359  //     "name": <string>"cluster-name"
   360  //     }
   361  //     },
   362  //     "integerVariable": <float64>4,
   363  //     "numberVariable": <float64>2.5,
   364  //     "booleanVariable": <bool>true,
   365  //     }
   366  func calculateTemplateData(variables map[string]apiextensionsv1.JSON) (map[string]interface{}, error) {
   367  	res := make(map[string]interface{}, len(variables))
   368  
   369  	// Marshal the variables into a byte array.
   370  	tmp, err := json.Marshal(variables)
   371  	if err != nil {
   372  		return nil, errors.Wrapf(err, "failed to convert variables: failed to marshal variables")
   373  	}
   374  
   375  	// Unmarshal the byte array back.
   376  	// NOTE: This converts the "leaf nodes" of the nested map
   377  	// from apiextensionsv1.JSON to their Go types.
   378  	if err := json.Unmarshal(tmp, &res); err != nil {
   379  		return nil, errors.Wrapf(err, "failed to convert variables: failed to unmarshal variables")
   380  	}
   381  
   382  	return res, nil
   383  }