sigs.k8s.io/cluster-api@v1.7.1/exp/runtime/topologymutation/walker.go (about)

     1  /*
     2  Copyright 2022 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 topologymutation
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  
    23  	mergepatch "github.com/evanphx/json-patch/v5"
    24  	"github.com/pkg/errors"
    25  	"gomodules.xyz/jsonpatch/v2"
    26  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/runtime/schema"
    29  	ctrl "sigs.k8s.io/controller-runtime"
    30  
    31  	runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
    32  )
    33  
    34  // WalkTemplatesOption is some configuration that modifies WalkTemplates behavior.
    35  type WalkTemplatesOption interface {
    36  	// ApplyToWalkTemplates applies this configuration to the given WalkTemplatesOptions.
    37  	ApplyToWalkTemplates(*WalkTemplatesOptions)
    38  }
    39  
    40  // WalkTemplatesOptions contains options for WalkTemplates behavior.
    41  type WalkTemplatesOptions struct {
    42  	failForUnknownTypes bool
    43  	patchFormat         runtimehooksv1.PatchType
    44  	// TODO: add the possibility to set patchFormat for single patches only, eg. via a func(requestItem) format.
    45  }
    46  
    47  // newWalkTemplatesOptions returns a WalkTemplatesOptions with default values.
    48  func newWalkTemplatesOptions() *WalkTemplatesOptions {
    49  	return &WalkTemplatesOptions{
    50  		patchFormat: runtimehooksv1.JSONPatchType,
    51  	}
    52  }
    53  
    54  // FailForUnknownTypes defines if WalkTemplates should fail when processing unknown types.
    55  // If not set unknown types will be silently ignored, which allows to the WalkTemplates decoder to
    56  // be configured only the API types it cares about.
    57  type FailForUnknownTypes struct{}
    58  
    59  // ApplyToWalkTemplates applies this configuration to the given WalkTemplatesOptions.
    60  func (f FailForUnknownTypes) ApplyToWalkTemplates(in *WalkTemplatesOptions) {
    61  	in.failForUnknownTypes = true
    62  }
    63  
    64  // PatchFormat defines the patch format that WalkTemplates should generate.
    65  // If not set, JSONPatchType will be used.
    66  type PatchFormat struct {
    67  	Format runtimehooksv1.PatchType
    68  }
    69  
    70  // ApplyToWalkTemplates applies this configuration to the given WalkTemplatesOptions.
    71  func (d PatchFormat) ApplyToWalkTemplates(in *WalkTemplatesOptions) {
    72  	in.patchFormat = d.Format
    73  }
    74  
    75  // WalkTemplates walks through all templates of a GeneratePatchesRequest and calls the mutateFunc.
    76  // By using walk templates it is possible to implement patches using typed API objects, which makes code
    77  // easier to read and less error prone than using unstructured or working with raw json/yaml.
    78  // Also, by using this func it is possible to ignore most of the details of the GeneratePatchesRequest
    79  // and GeneratePatchesResponse messages format and focus on writing patches/modifying the templates.
    80  func WalkTemplates(ctx context.Context, decoder runtime.Decoder, req *runtimehooksv1.GeneratePatchesRequest,
    81  	resp *runtimehooksv1.GeneratePatchesResponse, mutateFunc func(ctx context.Context, obj runtime.Object,
    82  		variables map[string]apiextensionsv1.JSON, holderRef runtimehooksv1.HolderReference) error, opts ...WalkTemplatesOption) {
    83  	log := ctrl.LoggerFrom(ctx)
    84  	globalVariables := ToMap(req.Variables)
    85  
    86  	options := newWalkTemplatesOptions()
    87  	for _, o := range opts {
    88  		o.ApplyToWalkTemplates(options)
    89  	}
    90  
    91  	// For all the templates in a request.
    92  	// TODO: add a notion of ordering the patch implementers can rely on. Ideally ordering could be pluggable via options.
    93  	for _, requestItem := range req.Items {
    94  		// Computes the variables that apply to the template, by merging global and template variables.
    95  		templateVariables, err := MergeVariableMaps(globalVariables, ToMap(requestItem.Variables))
    96  		if err != nil {
    97  			resp.Status = runtimehooksv1.ResponseStatusFailure
    98  			resp.Message = err.Error()
    99  			return
   100  		}
   101  
   102  		// Convert the template object into a typed object.
   103  		original, _, err := decoder.Decode(requestItem.Object.Raw, nil, requestItem.Object.Object)
   104  		if err != nil {
   105  			if options.failForUnknownTypes {
   106  				resp.Status = runtimehooksv1.ResponseStatusFailure
   107  				resp.Message = err.Error()
   108  				return
   109  			}
   110  			// Continue, object has a type which hasn't been registered with the scheme.
   111  			continue
   112  		}
   113  
   114  		// Setup contextual logging for the requestItem.
   115  		holderRefGV, err := schema.ParseGroupVersion(requestItem.HolderReference.APIVersion)
   116  		if err != nil {
   117  			resp.Status = runtimehooksv1.ResponseStatusFailure
   118  			resp.Message = errors.Wrapf(err, "error generating patches - HolderReference apiVersion %q is not in valid format", requestItem.HolderReference.APIVersion).Error()
   119  			return
   120  		}
   121  		requestItemLog := log.WithValues(
   122  			"template", logRef{
   123  				Group:   original.GetObjectKind().GroupVersionKind().Group,
   124  				Version: original.GetObjectKind().GroupVersionKind().Version,
   125  				Kind:    original.GetObjectKind().GroupVersionKind().Kind,
   126  			},
   127  			"holder", logRef{
   128  				Group:     holderRefGV.Group,
   129  				Version:   holderRefGV.Version,
   130  				Kind:      requestItem.HolderReference.Kind,
   131  				Namespace: requestItem.HolderReference.Namespace,
   132  				Name:      requestItem.HolderReference.Name,
   133  			},
   134  		)
   135  		requestItemCtx := ctrl.LoggerInto(ctx, requestItemLog)
   136  
   137  		// Calls the mutateFunc.
   138  		requestItemLog.V(4).Info("Generating patch for template")
   139  		modified := original.DeepCopyObject()
   140  		if err := mutateFunc(requestItemCtx, modified, templateVariables, requestItem.HolderReference); err != nil {
   141  			resp.Status = runtimehooksv1.ResponseStatusFailure
   142  			resp.Message = err.Error()
   143  			return
   144  		}
   145  
   146  		// Generate the Patch by comparing original and modified object.
   147  		var patch []byte
   148  		switch options.patchFormat {
   149  		case runtimehooksv1.JSONPatchType:
   150  			patch, err = createJSONPatch(original, modified)
   151  			if err != nil {
   152  				resp.Status = runtimehooksv1.ResponseStatusFailure
   153  				resp.Message = err.Error()
   154  				return
   155  			}
   156  		case runtimehooksv1.JSONMergePatchType:
   157  			patch, err = createJSONMergePatch(original, modified)
   158  			if err != nil {
   159  				resp.Status = runtimehooksv1.ResponseStatusFailure
   160  				resp.Message = err.Error()
   161  				return
   162  			}
   163  		}
   164  
   165  		resp.Items = append(resp.Items, runtimehooksv1.GeneratePatchesResponseItem{
   166  			UID:       requestItem.UID,
   167  			PatchType: options.patchFormat,
   168  			Patch:     patch,
   169  		})
   170  		requestItemLog.V(5).Info("Generated patch", "uid", requestItem.UID, "patch", string(patch))
   171  	}
   172  
   173  	resp.Status = runtimehooksv1.ResponseStatusSuccess
   174  }
   175  
   176  // createJSONPatch creates a RFC 6902 JSON patch from the original and the modified object.
   177  func createJSONPatch(original, modified runtime.Object) ([]byte, error) {
   178  	marshalledOriginal, err := json.Marshal(original)
   179  	if err != nil {
   180  		return nil, errors.Errorf("failed to marshal original object: %v", err)
   181  	}
   182  
   183  	marshalledModified, err := json.Marshal(modified)
   184  	if err != nil {
   185  		return nil, errors.Errorf("failed to marshal modified object: %v", err)
   186  	}
   187  
   188  	patch, err := jsonpatch.CreatePatch(marshalledOriginal, marshalledModified)
   189  	if err != nil {
   190  		return nil, errors.Errorf("failed to create patch: %v", err)
   191  	}
   192  
   193  	patchBytes, err := json.Marshal(patch)
   194  	if err != nil {
   195  		return nil, errors.Errorf("failed to marshal patch: %v", err)
   196  	}
   197  
   198  	return patchBytes, nil
   199  }
   200  
   201  // createJSONMergePatch creates a RFC 7396 JSON merge patch from the original and the modified object.
   202  func createJSONMergePatch(original, modified runtime.Object) ([]byte, error) {
   203  	marshalledOriginal, err := json.Marshal(original)
   204  	if err != nil {
   205  		return nil, errors.Errorf("failed to marshal original object: %v", err)
   206  	}
   207  
   208  	marshalledModified, err := json.Marshal(modified)
   209  	if err != nil {
   210  		return nil, errors.Errorf("failed to marshal modified object: %v", err)
   211  	}
   212  
   213  	patch, err := mergepatch.CreateMergePatch(marshalledOriginal, marshalledModified)
   214  	if err != nil {
   215  		return nil, errors.Errorf("failed to create patch: %v", err)
   216  	}
   217  
   218  	return patch, nil
   219  }