github.com/crossplane/upjet@v1.3.0/pkg/resource/lateinit.go (about)

     1  // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package resource
     6  
     7  import (
     8  	"fmt"
     9  	"reflect"
    10  	"runtime/debug"
    11  	"strings"
    12  
    13  	xpmeta "github.com/crossplane/crossplane-runtime/pkg/meta"
    14  	xpresource "github.com/crossplane/crossplane-runtime/pkg/resource"
    15  	"github.com/pkg/errors"
    16  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    17  
    18  	"github.com/crossplane/upjet/pkg/config"
    19  )
    20  
    21  const (
    22  	// AnnotationKeyPrivateRawAttribute is the key that points to private attribute
    23  	// of the Terraform State. It's non-sensitive and used by provider to store
    24  	// arbitrary metadata, usually details about schema version.
    25  	AnnotationKeyPrivateRawAttribute = "upjet.crossplane.io/provider-meta"
    26  
    27  	// AnnotationKeyTestResource is used for marking an MR as test for automated tests
    28  	AnnotationKeyTestResource = "upjet.upbound.io/test"
    29  
    30  	// CNameWildcard can be used as the canonical name of a value filter option
    31  	// that will apply to all fields of a struct
    32  	CNameWildcard = ""
    33  )
    34  const (
    35  	// error messages
    36  	errFmtTypeMismatch        = "observed object's type %q does not match desired object's type %q"
    37  	errFmtPanic               = "recovered from panic: %v\n%s"
    38  	errFmtMapElemNotSupported = "map items of kind %q is not supported for canonical name: %s"
    39  	errFmtNotPtrToStruct      = "%s must be of a pointer to struct type: %#v"
    40  
    41  	fmtCanonical = "%s.%s"
    42  )
    43  
    44  // GenericLateInitializer performs late-initialization of a Terraformed resource.
    45  type GenericLateInitializer struct {
    46  	valueFilters []ValueFilter
    47  	nameFilters  []NameFilter
    48  }
    49  
    50  // SetCriticalAnnotations sets the critical annotations of the resource and reports
    51  // whether there has been a change.
    52  func SetCriticalAnnotations(tr metav1.Object, cfg *config.Resource, tfstate map[string]any, privateRaw string) (bool, error) {
    53  	name, err := cfg.ExternalName.GetExternalNameFn(tfstate)
    54  	if err != nil {
    55  		return false, errors.Wrap(err, "cannot get external name")
    56  	}
    57  	if tr.GetAnnotations()[AnnotationKeyPrivateRawAttribute] == privateRaw &&
    58  		tr.GetAnnotations()[xpmeta.AnnotationKeyExternalName] == name {
    59  		return false, nil
    60  	}
    61  	xpmeta.AddAnnotations(tr, map[string]string{
    62  		AnnotationKeyPrivateRawAttribute: privateRaw,
    63  		xpmeta.AnnotationKeyExternalName: name,
    64  	})
    65  	return true, nil
    66  }
    67  
    68  // GenericLateInitializerOption are options that control the late-initialization
    69  // behavior of a Terraformed resource.
    70  type GenericLateInitializerOption func(l *GenericLateInitializer)
    71  
    72  // NewGenericLateInitializer constructs a new GenericLateInitializer
    73  // with the supplied options
    74  func NewGenericLateInitializer(opts ...GenericLateInitializerOption) *GenericLateInitializer {
    75  	l := &GenericLateInitializer{}
    76  	for _, o := range opts {
    77  		o(l)
    78  	}
    79  	return l
    80  }
    81  
    82  // NameFilter defines a late-initialization filter on CR field canonical names.
    83  // Fields with matching cnames will not be processed during late-initialization
    84  type NameFilter func(string) bool
    85  
    86  // WithNameFilter returns a GenericLateInitializer that causes to
    87  // skip initialization of the field with the specified canonical name
    88  func WithNameFilter(cname string) GenericLateInitializerOption {
    89  	return func(l *GenericLateInitializer) {
    90  		l.nameFilters = append(l.nameFilters, nameFilter(cname))
    91  	}
    92  }
    93  
    94  func nameFilter(cname string) NameFilter {
    95  	return func(s string) bool {
    96  		return cname == CNameWildcard || s == cname
    97  	}
    98  }
    99  
   100  // ValueFilter defines a late-initialization filter on CR field values.
   101  // Fields with matching values will not be processed during late-initialization
   102  type ValueFilter func(string, reflect.StructField, reflect.Value) bool
   103  
   104  // WithZeroValueJSONOmitEmptyFilter returns a GenericLateInitializerOption that causes to
   105  // skip initialization of a zero-valued field that has omitempty JSON tag
   106  func WithZeroValueJSONOmitEmptyFilter(cName string) GenericLateInitializerOption {
   107  	return func(l *GenericLateInitializer) {
   108  		l.valueFilters = append(l.valueFilters, zeroValueJSONOmitEmptyFilter(cName))
   109  	}
   110  }
   111  
   112  // zeroValueJSONOmitEmptyFilter is a late-initialization ValueFilter that
   113  // skips initialization of a zero-valued field that has omitempty JSON tag
   114  //
   115  //nolint:gocyclo
   116  func zeroValueJSONOmitEmptyFilter(cName string) ValueFilter {
   117  	return func(cn string, f reflect.StructField, v reflect.Value) bool {
   118  		if cName != CNameWildcard && cName != cn {
   119  			return false
   120  		}
   121  
   122  		if !isZeroValueOmitted(f.Tag.Get("json")) {
   123  			return false
   124  		}
   125  
   126  		k := v.Kind()
   127  		switch {
   128  		case !v.IsValid():
   129  			return false
   130  		case v.IsZero():
   131  			return true
   132  		case (k == reflect.Slice || k == reflect.Map) && v.Len() == 0:
   133  			return true
   134  		case k == reflect.Ptr && v.Elem().IsZero():
   135  			return true
   136  		default:
   137  			return false
   138  		}
   139  	}
   140  }
   141  
   142  // WithZeroElemPtrFilter returns a GenericLateInitializerOption that causes to
   143  // skip initialization of a pointer field with a zero-valued element
   144  func WithZeroElemPtrFilter(cName string) GenericLateInitializerOption {
   145  	return func(l *GenericLateInitializer) {
   146  		l.valueFilters = append(l.valueFilters, zeroElemPtrFilter(cName))
   147  	}
   148  }
   149  
   150  // zeroElemPtrFilter is a late-initialization ValueFilter that
   151  // skips initialization of a pointer field with a zero-valued element
   152  func zeroElemPtrFilter(cName string) ValueFilter {
   153  	return func(cn string, f reflect.StructField, v reflect.Value) bool {
   154  		if cName != CNameWildcard && cName != cn {
   155  			return false
   156  		}
   157  
   158  		t := v.Type()
   159  		if t.Kind() != reflect.Ptr || v.IsNil() {
   160  			return false
   161  		}
   162  		if v.Elem().IsZero() {
   163  			return true
   164  		}
   165  		return false
   166  	}
   167  }
   168  
   169  func isZeroValueOmitted(tag string) bool {
   170  	for _, p := range strings.Split(tag, ",") {
   171  		if p == "omitempty" {
   172  			return true
   173  		}
   174  	}
   175  	return false
   176  }
   177  
   178  // LateInitialize Copy unset (nil) values from responseObject to crObject
   179  // Both crObject and responseObject must be pointers to structs.
   180  // Otherwise, an error will be returned. Returns `true` if at least one field has been stored
   181  // from source `responseObject` into a corresponding field of target `crObject`.
   182  //
   183  //nolint:gocyclo
   184  func (li *GenericLateInitializer) LateInitialize(desiredObject, observedObject any) (changed bool, err error) {
   185  	if desiredObject == nil || reflect.ValueOf(desiredObject).IsNil() ||
   186  		observedObject == nil || reflect.ValueOf(observedObject).IsNil() {
   187  		return false, nil
   188  	}
   189  
   190  	typeOfDesiredObject, typeOfObservedObject := reflect.TypeOf(desiredObject), reflect.TypeOf(observedObject)
   191  	if typeOfDesiredObject.Kind() != reflect.Ptr || typeOfDesiredObject.Elem().Kind() != reflect.Struct {
   192  		return false, errors.Errorf(errFmtNotPtrToStruct, "desiredObject", desiredObject)
   193  	}
   194  	if typeOfObservedObject.Kind() != reflect.Ptr || typeOfObservedObject.Elem().Kind() != reflect.Struct {
   195  		return false, errors.Errorf(errFmtNotPtrToStruct, "observedObject", observedObject)
   196  	}
   197  	if reflect.TypeOf(desiredObject) != reflect.TypeOf(observedObject) {
   198  		return false, errors.Errorf(errFmtTypeMismatch, reflect.TypeOf(desiredObject).String(), reflect.TypeOf(observedObject).String())
   199  	}
   200  	defer func() {
   201  		if r := recover(); r != nil {
   202  			err = errors.Errorf(errFmtPanic, r, debug.Stack())
   203  		}
   204  	}()
   205  	changed, err = li.handleStruct("", desiredObject, observedObject)
   206  	return
   207  }
   208  
   209  //nolint:gocyclo
   210  func (li *GenericLateInitializer) handleStruct(parentName string, desiredObject any, observedObject any) (bool, error) {
   211  	typeOfDesiredObject, typeOfObservedObject := reflect.TypeOf(desiredObject), reflect.TypeOf(observedObject)
   212  	valueOfDesiredObject, valueOfObservedObject := reflect.ValueOf(desiredObject), reflect.ValueOf(observedObject).Elem()
   213  	typeOfDesiredObject, typeOfObservedObject = typeOfDesiredObject.Elem(), typeOfObservedObject.Elem()
   214  	valueOfDesiredObject = valueOfDesiredObject.Elem()
   215  	fieldAssigned := false
   216  
   217  	for f := 0; f < typeOfDesiredObject.NumField(); f++ {
   218  		desiredStructField := typeOfDesiredObject.Field(f)
   219  		desiredFieldValue := valueOfDesiredObject.FieldByName(desiredStructField.Name)
   220  		cName := getCanonicalName(parentName, desiredStructField.Name)
   221  		filtered := false
   222  
   223  		for _, f := range li.nameFilters {
   224  			if f(cName) {
   225  				filtered = true
   226  				break
   227  			}
   228  		}
   229  		if filtered {
   230  			continue
   231  		}
   232  
   233  		observedStructField, _ := typeOfObservedObject.FieldByName(desiredStructField.Name)
   234  		observedFieldValue := valueOfObservedObject.FieldByName(desiredStructField.Name)
   235  		desiredKeepField := false
   236  		var err error
   237  
   238  		if !desiredFieldValue.IsZero() {
   239  			continue
   240  		}
   241  
   242  		for _, f := range li.valueFilters {
   243  			if f(cName, observedStructField, observedFieldValue) {
   244  				// corresponding field value is filtered
   245  				filtered = true
   246  				break
   247  			}
   248  		}
   249  		if filtered {
   250  			continue
   251  		}
   252  
   253  		switch desiredStructField.Type.Kind() { //nolint:exhaustive
   254  		// handle pointer struct field
   255  		case reflect.Ptr:
   256  			desiredKeepField, err = li.handlePtr(cName, desiredFieldValue, observedFieldValue)
   257  
   258  		case reflect.Slice:
   259  			desiredKeepField, err = li.handleSlice(cName, desiredFieldValue, observedFieldValue)
   260  
   261  		case reflect.Map:
   262  			desiredKeepField, err = li.handleMap(cName, desiredFieldValue, observedFieldValue)
   263  		}
   264  
   265  		if err != nil {
   266  			return false, err
   267  		}
   268  
   269  		fieldAssigned = fieldAssigned || desiredKeepField
   270  	}
   271  
   272  	return fieldAssigned, nil
   273  }
   274  
   275  func (li *GenericLateInitializer) handlePtr(cName string, desiredFieldValue, observedFieldValue reflect.Value) (bool, error) {
   276  	if observedFieldValue.IsNil() || !desiredFieldValue.IsNil() {
   277  		return false, nil
   278  	}
   279  	// initialize with a nil pointer
   280  	v := desiredFieldValue.Interface()
   281  	desiredFieldValue.Set(reflect.New(reflect.ValueOf(&v).Elem().Elem().Type().Elem()))
   282  	desiredKeepField := false
   283  
   284  	switch {
   285  	// if we are dealing with a struct type, recursively check fields
   286  	case observedFieldValue.Elem().Kind() == reflect.Struct:
   287  		desiredFieldValue.Set(reflect.New(desiredFieldValue.Type().Elem()))
   288  		nestedFieldAssigned, err := li.handleStruct(cName, desiredFieldValue.Interface(), observedFieldValue.Interface())
   289  		if err != nil {
   290  			return false, err
   291  		}
   292  		desiredKeepField = nestedFieldAssigned
   293  
   294  	default: // then cr object's field is not set but response object contains a value, carry it
   295  		if desiredFieldValue.Kind() == reflect.Ptr && desiredFieldValue.IsNil() {
   296  			desiredFieldValue.Set(reflect.New(desiredFieldValue.Type().Elem()))
   297  		}
   298  
   299  		// initialize new copy from response field
   300  		desiredFieldValue.Elem().Set(observedFieldValue.Elem())
   301  		desiredKeepField = true
   302  	}
   303  
   304  	return desiredKeepField, nil
   305  }
   306  
   307  func (li *GenericLateInitializer) handleSlice(cName string, desiredFieldValue, observedFieldValue reflect.Value) (bool, error) {
   308  	if observedFieldValue.IsNil() || !desiredFieldValue.IsNil() {
   309  		return false, nil
   310  	}
   311  	// initialize with an empty slice
   312  	v := desiredFieldValue.Interface()
   313  	desiredFieldValue.Set(reflect.MakeSlice(reflect.ValueOf(&v).Elem().Elem().Type(), 0, observedFieldValue.Len()))
   314  
   315  	// then cr object's field is not set but response object contains a value, carry it
   316  	// copy slice items from response field
   317  	for i := 0; i < observedFieldValue.Len(); i++ {
   318  		// allocate new items for the CR
   319  		item := reflect.New(desiredFieldValue.Type().Elem())
   320  		// error from processing the next element of the slice
   321  		var err error
   322  		// check slice item's kind (not slice type)
   323  		switch item.Elem().Kind() { //nolint:exhaustive
   324  		// if dealing with a slice of pointers
   325  		case reflect.Ptr:
   326  			_, err = li.handlePtr(cName, item.Elem(), observedFieldValue.Index(i))
   327  		case reflect.Struct:
   328  			_, err = li.handleStruct(cName, item.Interface(), observedFieldValue.Index(i).Addr().Interface())
   329  		case reflect.String, reflect.Bool, reflect.Int, reflect.Uint,
   330  			reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
   331  			reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
   332  			reflect.Float32, reflect.Float64:
   333  			// set primitive type
   334  			item.Elem().Set(observedFieldValue.Index(i))
   335  		// other slice item types are not supported
   336  		default:
   337  			return false, errors.Errorf("slice items of kind %q is not supported for canonical name: %s",
   338  				item.Elem().Kind().String(), cName)
   339  		}
   340  		// if a type is used at different paths, be sure to define separate filters on corresponding canonical names
   341  		if err != nil {
   342  			return false, err
   343  		}
   344  		// a new item has been allocated, expand the slice with it
   345  		desiredFieldValue.Set(reflect.Append(desiredFieldValue, item.Elem()))
   346  	}
   347  	return true, nil
   348  }
   349  
   350  func (li *GenericLateInitializer) handleMap(cName string, desiredFieldValue, observedFieldValue reflect.Value) (bool, error) {
   351  	if observedFieldValue.IsNil() || !desiredFieldValue.IsNil() {
   352  		return false, nil
   353  	}
   354  	// initialize with an empty map
   355  	v := desiredFieldValue.Interface()
   356  	desiredFieldValue.Set(reflect.MakeMap(reflect.ValueOf(&v).Elem().Elem().Type()))
   357  
   358  	// then cr object's field is not set but response object contains a value, carry it
   359  	// copy map items from response field
   360  	for _, k := range observedFieldValue.MapKeys() {
   361  		// allocate a new item for the CR
   362  		item := reflect.New(desiredFieldValue.Type().Elem())
   363  		// error from processing the next element of the map
   364  		var err error
   365  		// check map item's kind (not map type)
   366  		switch item.Elem().Kind() { //nolint:exhaustive
   367  		// if dealing with a slice of pointers
   368  		case reflect.Ptr:
   369  			_, err = li.handlePtr(cName, item.Elem(), observedFieldValue.MapIndex(k))
   370  		// else if dealing with a slice of slices
   371  		case reflect.Slice:
   372  			_, err = li.handleSlice(cName, item.Elem(), observedFieldValue.MapIndex(k))
   373  		case reflect.String, reflect.Bool, reflect.Int, reflect.Uint,
   374  			reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
   375  			reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
   376  			reflect.Float32, reflect.Float64:
   377  			// set primitive type
   378  			item.Elem().Set(observedFieldValue.MapIndex(k))
   379  		// other slice item types are not supported
   380  		default:
   381  			return false, errors.Errorf(errFmtMapElemNotSupported, item.Elem().Kind().String(), cName)
   382  		}
   383  		if err != nil {
   384  			return false, err
   385  		}
   386  		// set value at current key
   387  		desiredFieldValue.SetMapIndex(k, item.Elem())
   388  	}
   389  
   390  	return true, nil
   391  }
   392  
   393  func getCanonicalName(parent, child string) string {
   394  	if parent == "" {
   395  		return child
   396  	}
   397  
   398  	return fmt.Sprintf(fmtCanonical, parent, child)
   399  }
   400  
   401  // IsTest returns true if the managed resource has upjet.upbound.io/test= "true" annotation
   402  func IsTest(mg xpresource.Managed) bool {
   403  	return mg.GetAnnotations()[AnnotationKeyTestResource] == "true"
   404  }