sigs.k8s.io/cluster-api-provider-azure@v1.17.0/pkg/mutators/mutator.go (about) 1 /* 2 Copyright 2024 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 mutators 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "strconv" 24 "strings" 25 26 "github.com/Azure/azure-service-operator/v2/pkg/common/annotations" 27 "github.com/go-logr/logr" 28 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 29 "k8s.io/apimachinery/pkg/runtime" 30 "sigs.k8s.io/cluster-api-provider-azure/util/tele" 31 "sigs.k8s.io/controller-runtime/pkg/reconcile" 32 ) 33 34 // ResourcesMutator mutates in-place a slice of ASO resources to be reconciled. These mutations make only the 35 // changes strictly necessary for CAPZ resources to play nice with Cluster API. Any mutations should be logged 36 // and mutations that conflict with user-defined values should be rejected by returning Incompatible. 37 type ResourcesMutator func(context.Context, []*unstructured.Unstructured) error 38 39 type mutation struct { 40 location string 41 val any 42 reason string 43 } 44 45 func logMutation(log logr.Logger, mutation mutation) { 46 log.V(4).Info(fmt.Sprintf("setting %s to %v %s", mutation.location, mutation.val, mutation.reason)) 47 } 48 49 // Incompatible describes an error where a piece of user-defined configuration does not match what CAPZ 50 // requires. 51 type Incompatible struct { 52 mutation 53 userVal any 54 } 55 56 func (e Incompatible) Error() string { 57 return fmt.Sprintf("incompatible value: value at %s set by user to %v but CAPZ must set it to %v %s. The user-defined value must not be defined, or must match CAPZ's desired value.", e.location, e.userVal, e.val, e.reason) 58 } 59 60 // ApplyMutators applies the given mutators to the given resources. 61 func ApplyMutators(ctx context.Context, resources []runtime.RawExtension, mutators ...ResourcesMutator) ([]*unstructured.Unstructured, error) { 62 us := []*unstructured.Unstructured{} 63 for _, resource := range resources { 64 u := &unstructured.Unstructured{} 65 if err := u.UnmarshalJSON(resource.Raw); err != nil { 66 return nil, fmt.Errorf("failed to unmarshal resource JSON: %w", err) 67 } 68 us = append(us, u) 69 } 70 for _, mutator := range mutators { 71 if err := mutator(ctx, us); err != nil { 72 err = fmt.Errorf("failed to run mutator: %w", err) 73 if errors.As(err, &Incompatible{}) { 74 err = reconcile.TerminalError(err) 75 } 76 return nil, err 77 } 78 } 79 return us, nil 80 } 81 82 // ToUnstructured converts the given resources to Unstructured. 83 func ToUnstructured(ctx context.Context, resources []runtime.RawExtension) ([]*unstructured.Unstructured, error) { 84 return ApplyMutators(ctx, resources) 85 } 86 87 // Pause sets the "skip" reconcile policy on all resources to facilitate a CAPI pause. 88 func Pause(ctx context.Context, resources []*unstructured.Unstructured) error { 89 _, log, done := tele.StartSpanWithLogger(ctx, "mutators.Pause") 90 defer done() 91 92 for i, resource := range resources { 93 resourcePath := "spec.resources[" + strconv.Itoa(i) + "]" 94 policyPath := []string{"metadata", "annotations", annotations.ReconcilePolicy} 95 capiPolicy := string(annotations.ReconcilePolicySkip) 96 userPolicy, userDefined := resource.GetAnnotations()[annotations.ReconcilePolicy] 97 98 setPolicy := mutation{ 99 location: resourcePath + "." + strings.Join(policyPath, "."), 100 val: capiPolicy, 101 reason: "because the CAPZ resource is paused", 102 } 103 if userDefined && userPolicy != capiPolicy { 104 return Incompatible{ 105 mutation: setPolicy, 106 userVal: userPolicy, 107 } 108 } 109 110 logMutation(log, setPolicy) 111 anns := resource.GetAnnotations() 112 if anns == nil { 113 anns = make(map[string]string) 114 } 115 anns[annotations.ReconcilePolicy] = capiPolicy 116 resource.SetAnnotations(anns) 117 } 118 119 return nil 120 }