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 }