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  }