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