...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/krmtotf/tftokrm.go

Documentation: github.com/GoogleCloudPlatform/k8s-config-connector/pkg/krmtotf

     1  // Copyright 2022 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package krmtotf
    16  
    17  import (
    18  	"fmt"
    19  	"reflect"
    20  	"strings"
    21  
    22  	corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
    23  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/deepcopy"
    24  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/gcp"
    25  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    26  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/text"
    27  	tfresource "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/tf/resource"
    28  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util"
    29  
    30  	tfschema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    31  	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
    32  	"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
    33  )
    34  
    35  // ResolveSpecAndStatus returns the resolved spec and status in different formats
    36  // gated by the 'state-into-spec' annotation.
    37  //
    38  // If the annotation takes the 'merge' value, the function returns spec as a mix of k8s user managed fields and defaulted state from APIs
    39  // and returns status with the legacy format containing observed state for output-only fields only.
    40  //
    41  // If the annotation takes the 'absent' value, the function will delegate to resolveDesiredStateInSpecAndObservedStateInStatus() to resolve
    42  // the spec and the status.
    43  func ResolveSpecAndStatus(resource *Resource, state *terraform.InstanceState) (
    44  	spec map[string]interface{}, status map[string]interface{}) {
    45  	val, found := k8s.GetAnnotation(k8s.StateIntoSpecAnnotation, resource)
    46  	if !found || val == k8s.StateMergeIntoSpec {
    47  		return GetSpecAndStatusFromState(resource, state)
    48  	}
    49  	return resolveDesiredStateInSpecAndObservedStateInStatus(resource, state)
    50  }
    51  
    52  // GetSpecAndStatusFromState converts state into separate, KRM-compatible spec and status
    53  // objects.
    54  //
    55  // This function can handle partial state structs (ones that may fail if applied with terraform).
    56  // The resource.Spec that is passed is assumed to be the desired state of the user, and as such
    57  // fields that are specified by Kubernetes to be managed by Config Connector will use the values in
    58  // resource.Spec rather than those in state in the returned spec and status. That said, this function
    59  // returns spec as a mix of k8s user managed fields and defaulted state from APIs
    60  // and returns status with the legacy format containing observed state for output-only fields only.
    61  //
    62  // See ConvertTFObjToKCCObj for a complete description of the merging behavior of
    63  // state and resource.Spec (passed as prevSpec)
    64  func GetSpecAndStatusFromState(resource *Resource, state *terraform.InstanceState) (
    65  	spec map[string]interface{}, status map[string]interface{}) {
    66  	unmodifiedState := InstanceStateToMap(resource.TFResource, state)
    67  	krmState := ConvertTFObjToKCCObj(unmodifiedState, resource.Spec, resource.TFResource.Schema,
    68  		&resource.ResourceConfig, "", resource.ManagedFields)
    69  	krmState = withCustomExpanders(krmState, resource, resource.Kind)
    70  	spec = make(map[string]interface{})
    71  	status = make(map[string]interface{})
    72  	for field, fieldSchema := range resource.TFResource.Schema {
    73  		key := text.SnakeCaseToLowerCamelCase(field)
    74  		if ok, refConfig := IsReferenceField(field, &resource.ResourceConfig); ok && refConfig.Key != "" {
    75  			key = refConfig.Key
    76  		}
    77  		val := krmState[key]
    78  		if val == nil {
    79  			continue
    80  		}
    81  		target := &spec
    82  		if !fieldSchema.Required && !fieldSchema.Optional {
    83  			target = &status
    84  			key = renameStatusFieldIfNeeded(resource.ResourceConfig.Name, key)
    85  		}
    86  		(*target)[key] = val
    87  	}
    88  	if location, ok := getLocationValueFromResourceOrState(resource, unmodifiedState); ok {
    89  		spec["location"] = location
    90  	}
    91  	if conditions, ok := resource.Status["conditions"]; ok {
    92  		status["conditions"] = deepcopy.DeepCopy(conditions)
    93  	}
    94  	if observedGeneration, ok := resource.Status["observedGeneration"]; ok {
    95  		status["observedGeneration"] = deepcopy.DeepCopy(observedGeneration)
    96  	}
    97  	if len(spec) == 0 {
    98  		spec = nil
    99  	}
   100  	if len(status) == 0 {
   101  		status = nil
   102  	}
   103  	return spec, status
   104  }
   105  
   106  // ResolveSpecAndStatusWithResourceID returns the resolved spec and status with the `resourceID`
   107  // field is populated in the KRM spec.
   108  func ResolveSpecAndStatusWithResourceID(resource *Resource, state *terraform.InstanceState) (
   109  	spec map[string]interface{}, status map[string]interface{}) {
   110  	spec, status = ResolveSpecAndStatus(resource, state)
   111  
   112  	resourceID, ok := getResourceIDIfSupported(resource, status)
   113  	if !ok {
   114  		return spec, status
   115  	}
   116  
   117  	if spec == nil {
   118  		spec = make(map[string]interface{})
   119  	}
   120  	spec[k8s.ResourceIDFieldName] = resourceID
   121  	return spec, status
   122  }
   123  
   124  // resolveDesiredStateInSpecAndObservedStateInStatus resolves spec as desired state and persists observed state in status.
   125  // TODO(b/193928224): persist the full observed state including both configurable fields and output-only fields in status.
   126  func resolveDesiredStateInSpecAndObservedStateInStatus(resource *Resource, state *terraform.InstanceState) (
   127  	spec map[string]interface{}, status map[string]interface{}) {
   128  	spec = deepcopy.MapStringInterface(resource.Spec)
   129  	_, status = GetSpecAndStatusFromState(resource, state)
   130  	return spec, status
   131  }
   132  
   133  // There are three scenarios for which we get the location value
   134  //  1. It is in the spec.location and has been supplied by the customer and is likely an "easy" value like us-central1
   135  //  2. It is in the state after a terraform import, i.e. the resource name was converted to TF state, and again it has an easy value like #1
   136  //  3. It is in the state after a terraform Read, in which case it likely came from in a GET response from the given service
   137  //     and is likely the fully qualified region / zone URL
   138  //
   139  // It is desired that we retain the 'easy' names from #1 and #2 so those are given precedence
   140  func getLocationValueFromResourceOrState(resource *Resource, state map[string]interface{}) (interface{}, bool) {
   141  	// the value for 'zone' returned by GCP APIs is the full ResourceName, i.e.
   142  	//   https://www.googleapis.com/compute/v1/projects/project-id/zones/us-west2-a
   143  	// to prevent overwriting the 'easier' zone value that most people use in their configs, return the previous
   144  	// value if it is in the spec
   145  	if location, ok := resource.Spec["location"]; ok {
   146  		return location, true
   147  	}
   148  	if resource.ResourceConfig.Locationality == "" {
   149  		return "", false
   150  	}
   151  	switch resource.ResourceConfig.Locationality {
   152  	case gcp.Global:
   153  		return "global", true
   154  	case gcp.Regional, gcp.Zonal:
   155  		locationFieldName := getTFFieldNameForLocation(resource.ResourceConfig.Locationality)
   156  		value, ok := state[locationFieldName]
   157  		if !ok {
   158  			return nil, false
   159  		}
   160  		return value, true
   161  	}
   162  	panic(fmt.Errorf("unknown location type: %v", resource.ResourceConfig.Locationality))
   163  }
   164  
   165  func getTFFieldNameForLocation(locType string) string {
   166  	switch locType {
   167  	case gcp.Regional:
   168  		return "region"
   169  	case gcp.Zonal:
   170  		return "zone"
   171  	case gcp.Global:
   172  		return "global"
   173  	}
   174  	panic(fmt.Errorf("unknown location type: %v", locType))
   175  }
   176  
   177  func getResourceIDIfSupported(resource *Resource, status map[string]interface{}) (interface{}, bool) {
   178  	if !SupportsResourceIDField(&resource.ResourceConfig) {
   179  		return nil, false
   180  	}
   181  
   182  	if resourceID, ok := resource.Spec[k8s.ResourceIDFieldName]; ok {
   183  		return resourceID, true
   184  	}
   185  
   186  	if IsResourceIDFieldServerGenerated(&resource.ResourceConfig) {
   187  		serverGeneratedIDFromStatus, exists, err :=
   188  			getServerGeneratedIDFromStatus(&resource.ResourceConfig, status)
   189  		if !exists || err != nil {
   190  			panic(fmt.Errorf("server-generated resource ID not "+
   191  				"returned for resource Kind '%s', Name '%s', Namespace '%s'",
   192  				resource.Kind, resource.Name, resource.Namespace))
   193  		}
   194  
   195  		resourceID, err := extractValueSegmentFromIDInStatus(
   196  			serverGeneratedIDFromStatus,
   197  			resource.ResourceConfig.ResourceID.ValueTemplate)
   198  		if err != nil {
   199  			panic(fmt.Errorf("incorrect format of server-generated "+
   200  				"resource ID for resource Kind '%s', Name '%s', Namespace "+
   201  				"'%s'", resource.Kind, resource.Name, resource.Namespace))
   202  		}
   203  
   204  		return resourceID, true
   205  	}
   206  
   207  	resourceID := resource.GetName()
   208  	if resourceID == "" {
   209  		panic(fmt.Errorf("user-specified resource ID not found for resource "+
   210  			"Kind '%s', Name '%s', Namespace '%s'", resource.Kind, resource.Name,
   211  			resource.Namespace))
   212  	}
   213  
   214  	return resourceID, true
   215  }
   216  
   217  func GetLabelsFromState(resource *Resource, rawState *terraform.InstanceState) map[string]string {
   218  	state := InstanceStateToMap(resource.TFResource, rawState)
   219  	labelsValue, ok := getNestedMapFromState(state, strings.Split(resource.ResourceConfig.MetadataMapping.Labels, ".")...)
   220  	if !ok {
   221  		return make(map[string]string)
   222  	}
   223  	result := make(map[string]string, len(labelsValue))
   224  	for k, v := range labelsValue {
   225  		result[k] = v.(string)
   226  	}
   227  	return result
   228  }
   229  
   230  func GetEtagFromState(resource *Resource, rawState *terraform.InstanceState) string {
   231  	state := InstanceStateToMap(resource.TFResource, rawState)
   232  	etagValue, ok := getNestedFieldFromState(state, "etag")
   233  	if ok && etagValue != nil {
   234  		return etagValue.(string)
   235  	}
   236  	return ""
   237  }
   238  
   239  func GetNameFromState(resource *Resource, rawState *terraform.InstanceState) string {
   240  	if resource.ResourceConfig.MetadataMapping.Name == "" {
   241  		return ""
   242  	}
   243  	state := InstanceStateToMap(resource.TFResource, rawState)
   244  	nameValue, ok := getNestedFieldFromState(state, strings.Split(resource.ResourceConfig.MetadataMapping.Name, ".")...)
   245  	if ok && nameValue != nil {
   246  		return nameValue.(string)
   247  	}
   248  	return ""
   249  }
   250  
   251  // returns a nested map from within the state by traversing down the state with the path defined by the 'fields' list parameter
   252  // if no such field path exists then the second parameter is false
   253  // if there is a type mismatch then panic
   254  func getNestedMapFromState(state map[string]interface{}, fields ...string) (map[string]interface{}, bool) {
   255  	value, ok := getNestedFieldFromState(state, fields...)
   256  	if !ok || value == nil {
   257  		return nil, false
   258  	}
   259  	result, ok := value.(map[string]interface{})
   260  	if !ok {
   261  		panic(fmt.Sprintf("expected type '%v' instead got '%v'", reflect.TypeOf(make(map[string]interface{})).Name(),
   262  			reflect.TypeOf(value).Name()))
   263  	}
   264  	return result, true
   265  }
   266  
   267  // returns a nested field from within the state by traversing down the state with the path defined by the 'fields' list parameter,
   268  // stripping out the lists of length one that terraform inserts
   269  // if no such field path exists then the second parameter is false
   270  // if a field within the path is not a map then panic
   271  func getNestedFieldFromState(state map[string]interface{}, fields ...string) (interface{}, bool) {
   272  	var result interface{}
   273  	result = state
   274  	for i := 0; i < len(fields); i++ {
   275  		subMap, ok := result.(map[string]interface{})
   276  		if !ok {
   277  			panic(formatUnexpectedValueTypeInStateMessage(result, i-1, fields...))
   278  		}
   279  		result, ok = getFieldFromStateMap(subMap, fields[i])
   280  		// an 'ok' value of false indicates no value, but there are cases where the stored value is 'nil' so we need to check for that as well
   281  		if !ok || result == nil {
   282  			return nil, false
   283  		}
   284  	}
   285  	return result, true
   286  }
   287  
   288  func formatUnexpectedValueTypeInStateMessage(value interface{}, fieldNum int, fields ...string) string {
   289  	expectedType := reflect.TypeOf(make(map[string]interface{})).Name()
   290  	actualType := reflect.TypeOf(value).Name()
   291  	if fieldNum < 0 {
   292  		return fmt.Sprintf("expected type '%v' instead got '%v'", expectedType, actualType)
   293  	}
   294  	return fmt.Sprintf("expected '%v' to be of type '%v' instead got '%v'", fields[fieldNum], expectedType, actualType)
   295  }
   296  
   297  func getFieldFromStateMap(state map[string]interface{}, field string) (interface{}, bool) {
   298  	value, ok := state[field]
   299  	if !ok {
   300  		return nil, false
   301  	}
   302  	// the response returned by terraform will insert a list of size 1 for nested fields
   303  	if listVal, ok := value.([]interface{}); ok {
   304  		return listVal[0], true
   305  	}
   306  	return value, true
   307  }
   308  
   309  // Get the directives and container annotation(s) from the state
   310  func GetAnnotationsFromState(resource *Resource, rawState *terraform.InstanceState) map[string]string {
   311  	annotations := make(map[string]string, len(resource.ResourceConfig.Directives)+1)
   312  	state := InstanceStateToMap(resource.TFResource, rawState)
   313  	for _, directive := range resource.ResourceConfig.Directives {
   314  		if isIgnoredField(directive, &resource.ResourceConfig) {
   315  			continue
   316  		}
   317  		value, ok := getValueFromState(state, directive)
   318  		if !ok {
   319  			continue
   320  		}
   321  		key := k8s.FormatAnnotation(text.SnakeCaseToKebabCase(directive))
   322  		annotations[key] = value
   323  	}
   324  	if !SupportsHierarchicalReferences(&resource.ResourceConfig) {
   325  		// TODO(b/193177782): Delete this if-block once all resources support
   326  		// hierarchical references.
   327  		for _, c := range resource.ResourceConfig.Containers {
   328  			value, ok := getValueFromState(state, c.TFField)
   329  			if !ok {
   330  				continue
   331  			}
   332  			if valueMatchesTemplate(c.ValueTemplate, value) {
   333  				key := k8s.GetAnnotationForContainerType(c.Type)
   334  				annotations[key] = value
   335  			}
   336  		}
   337  	}
   338  	return annotations
   339  }
   340  
   341  func getValueFromState(state map[string]interface{}, key string) (string, bool) {
   342  	value, ok := state[key]
   343  	// the state map contains all possible keys with 'nil' for missing values
   344  	if !ok || value == nil {
   345  		return "", false
   346  	}
   347  	stringValue := fmt.Sprintf("%v", value)
   348  	if stringValue == "" {
   349  		return "", false
   350  	}
   351  	return stringValue, true
   352  }
   353  
   354  // ConvertTFObjToKCCObj takes the state (which should be a Terraform resource),
   355  // and returns a map that is formatted to KCC's custom resource schema for the
   356  // appropriate Kind.
   357  //
   358  // prevSpec is used for multiple purposes:
   359  //   - ensures the returned result has a similar order for objects in lists, reducing
   360  //     the percieved diff when applied.
   361  //   - if server-side apply is used, the prevSpec value for a field will be used over
   362  //     the value in state if it is managed by KCC.
   363  //   - for sets (which are represented as lists), the result is a merger of both the
   364  //     state and the prevSpec.
   365  func ConvertTFObjToKCCObj(state map[string]interface{}, prevSpec map[string]interface{},
   366  	schemas map[string]*tfschema.Schema, rc *corekccv1alpha1.ResourceConfig, prefix string,
   367  	managedFields *fieldpath.Set) map[string]interface{} {
   368  	raw := convertTFMapToKCCMap(state, prevSpec, schemas, rc, prefix, managedFields)
   369  	// Round-trip via JSON in order to ensure consistency with unstructured.Unstructured's Object type.
   370  	var ret map[string]interface{}
   371  	if err := util.Marshal(raw, &ret); err != nil {
   372  		panic(fmt.Errorf("error normalizing KRM-ified object: %v", err))
   373  	}
   374  	return ret
   375  }
   376  
   377  func convertTFMapToKCCMap(state map[string]interface{}, prevSpec map[string]interface{},
   378  	schemas map[string]*tfschema.Schema, rc *corekccv1alpha1.ResourceConfig, prefix string,
   379  	managedFields *fieldpath.Set) map[string]interface{} {
   380  	ret := make(map[string]interface{})
   381  	for field, schema := range schemas {
   382  		qualifiedName := field
   383  		if prefix != "" {
   384  			qualifiedName = prefix + "." + field
   385  		}
   386  		if isOverriddenField(qualifiedName, rc) {
   387  			continue
   388  		}
   389  		if ok, refConfig := IsReferenceField(qualifiedName, rc); ok {
   390  			key := GetKeyForReferenceField(refConfig)
   391  			if val := convertTFReferenceToKCCReference(field, key, state, prevSpec, refConfig); val != nil {
   392  				ret[key] = val
   393  			}
   394  			continue
   395  		}
   396  		key := text.SnakeCaseToLowerCamelCase(field)
   397  		stateVal := state[field]
   398  		prevSpecVal := prevSpec[key]
   399  		if stateVal == nil {
   400  			// Since partial terraform state are supported, if the next state is nil, we can
   401  			// omit including them in the returned value.
   402  			//
   403  			// The one exception is if the field is managed by KCC. In this case,
   404  			// it is assumed that "prevSpec" is the desired specification by the user,
   405  			// and we replicate the managedField check that occurs when stateVal is non-nil.
   406  			if prevSpecVal != nil && k8s.IsK8sManaged(key, prevSpec, managedFields) {
   407  				ret[key] = prevSpecVal
   408  			}
   409  			continue
   410  		}
   411  		if isGCPManagedField(rc.Kind, qualifiedName) {
   412  			ret[key] = stateVal
   413  			continue
   414  		}
   415  		switch schema.Type {
   416  		// Note:
   417  		// - The provider will add defaulted "zero" values to any unset fields:
   418  		// 	 https://github.com/hashicorp/terraform/blob/f9f73204383953e1b7fb91af6c56573cc0be2c02/helper/schema/field_reader.go#L287
   419  		//   This adds a lot of noise to the KRM resource, so we prune these if they were not explicitly set
   420  		//   by the user and does not have an explicit default (in which case the zero value would imply it was set explicitly).
   421  		// - Certain APIs allow fields to be input in multiple different formats, and expand to
   422  		//   a canonical form on the server. We want to keep the format that the user specified.
   423  		case tfschema.TypeBool:
   424  			if k8s.IsK8sManaged(key, prevSpec, managedFields) {
   425  				ret[key] = prevSpecVal
   426  			} else if schema.Required || stateVal.(bool) || (schema.Default != nil && schema.Default != stateVal) {
   427  				ret[key] = stateVal
   428  			}
   429  		case tfschema.TypeFloat, tfschema.TypeInt:
   430  			// The conversion from cty.Value to map[string]interface{} via JSON marshaling
   431  			// will cause all numeric values to be of type float64.
   432  			if k8s.IsK8sManaged(key, prevSpec, managedFields) {
   433  				ret[key] = prevSpecVal
   434  			} else if schema.Required || stateVal.(float64) != 0 || (schema.Default != nil && schema.Default != stateVal) {
   435  				ret[key] = stateVal
   436  			}
   437  		case tfschema.TypeString:
   438  			if k8s.IsK8sManaged(key, prevSpec, managedFields) {
   439  				ret[key] = prevSpecVal
   440  			} else {
   441  				if stateVal.(string) == "" {
   442  					continue
   443  				}
   444  				if tfresource.IsSensitiveConfigurableField(schema) {
   445  					val := stateVal.(string)
   446  					ret[key] = corekccv1alpha1.SensitiveField{
   447  						Value: &val,
   448  					}
   449  				} else {
   450  					ret[key] = stateVal
   451  				}
   452  			}
   453  		case tfschema.TypeList, tfschema.TypeSet:
   454  			list, ok := stateVal.([]interface{})
   455  			if !ok {
   456  				panic(fmt.Sprintf("interface conversion for field %v in resource %v: interface {} is %T, not []interface {}. prevSpecVal: %v, stateVal: %v", qualifiedName, rc.Name, stateVal, prevSpecVal, stateVal))
   457  			}
   458  			if len(list) == 0 {
   459  				continue
   460  			}
   461  			if schema.MaxItems == 1 {
   462  				// A list with MaxItems == 1 is actually a nested object due to limitations with TF schemas.
   463  				tfObjMap := list[0].(map[string]interface{})
   464  				tfObjSchema := schema.Elem.(*tfschema.Resource).Schema
   465  				prevObjMap, _ := prevSpecVal.(map[string]interface{})
   466  				var nestedManagedFields *fieldpath.Set
   467  				if managedFields != nil {
   468  					pe := fieldpath.PathElement{FieldName: &key}
   469  					var found bool
   470  					nestedManagedFields, found = managedFields.Children.Get(pe)
   471  					if !found {
   472  						nestedManagedFields = fieldpath.NewSet()
   473  					}
   474  				}
   475  				if val := convertTFMapToKCCMap(tfObjMap, prevObjMap, tfObjSchema, rc, qualifiedName, nestedManagedFields); val != nil {
   476  					ret[key] = val
   477  				}
   478  				continue
   479  			}
   480  			if schema.Type == tfschema.TypeSet {
   481  				// Sets in the spec require extra care in mapping elements from the previous spec
   482  				// to the new one, as the ordering may have changed in the returned state. Sets in
   483  				// the status can be treated the same as lists, as the new state is the definitive
   484  				// source of truth and there is no reference resolution.
   485  				if schema.Required || schema.Optional {
   486  					retObj := convertTFSetToKCCSet(stateVal, prevSpecVal, schema, rc, qualifiedName)
   487  					if retObj != nil {
   488  						ret[key] = retObj
   489  					}
   490  					continue
   491  				}
   492  			}
   493  			// A list may be either a list of primitives or a list of resources.
   494  			switch schema.Elem.(type) {
   495  			case *tfschema.Schema:
   496  				// If it's a list of primitives, there is no conversion required
   497  				ret[key] = deepcopy.DeepCopy(list)
   498  			case *tfschema.Resource:
   499  				// It's a list of the same type of resource. Convert each one using the same schema.
   500  				prevList, _ := prevSpecVal.([]interface{})
   501  				tfObjSchema := schema.Elem.(*tfschema.Resource).Schema
   502  				retObjList := make([]interface{}, 0)
   503  				for idx, elem := range list {
   504  					tfObjMap := elem.(map[string]interface{})
   505  					var prevObjMap map[string]interface{}
   506  					if idx < len(prevList) {
   507  						prevObjMap, _ = prevList[idx].(map[string]interface{})
   508  					}
   509  					if val := convertTFMapToKCCMap(tfObjMap, prevObjMap, tfObjSchema, rc, qualifiedName, nil); val != nil {
   510  						retObjList = append(retObjList, val)
   511  					}
   512  				}
   513  				if len(retObjList) == 0 {
   514  					continue
   515  				}
   516  				ret[key] = retObjList
   517  			}
   518  		case tfschema.TypeMap:
   519  			if k8s.IsK8sManaged(key, prevSpec, managedFields) {
   520  				ret[key] = prevSpecVal
   521  				continue
   522  			}
   523  			m := stateVal.(map[string]interface{})
   524  			// Prune empty maps defaulted by the provider
   525  			if len(m) == 0 {
   526  				continue
   527  			}
   528  			// In this case, we do not convert from snake_case to camelCase, as the
   529  			// keys here are user-provided
   530  			ret[key] = deepcopy.DeepCopy(m)
   531  		case tfschema.TypeInvalid:
   532  			panic("invalid schema type")
   533  		default:
   534  			panic(fmt.Errorf("unrecognized schema type %v", schema.Type))
   535  		}
   536  	}
   537  	if len(ret) == 0 {
   538  		return nil
   539  	}
   540  	return ret
   541  }
   542  
   543  // convertTFReferenceToKCCReference converts the value of a TF reference field
   544  // to a KCC reference value. The value of a TF reference field can either be a
   545  // string or a list of strings. This function handles both cases.
   546  func convertTFReferenceToKCCReference(tfField, specKey string, state map[string]interface{}, prevSpec map[string]interface{}, refConfig *corekccv1alpha1.ReferenceConfig) interface{} {
   547  	if prevSpecVal, ok := prevSpec[specKey]; ok {
   548  		// The user already specified a value for the KCC reference field in
   549  		// the previous spec. Preserve it.
   550  		return prevSpecVal
   551  	}
   552  
   553  	if state[tfField] == nil {
   554  		return nil
   555  	}
   556  
   557  	// The user did not specify a value for the KCC reference field in the
   558  	// previous spec, but the TF state has a value for the TF reference field.
   559  	// Convert the value of the TF reference field to a KCC reference field
   560  	// value.
   561  	switch stateVal := state[tfField].(type) {
   562  	case string:
   563  		if stateVal == "" {
   564  			return nil
   565  		}
   566  		if len(refConfig.Types) > 0 {
   567  			// Get the first item in the list of types -- for now this is the defaulted ref
   568  			defaultType := refConfig.Types[0]
   569  			if defaultType.JSONSchemaType != "" {
   570  				return map[string]interface{}{
   571  					defaultType.Key: stateVal,
   572  				}
   573  			} else {
   574  				return map[string]interface{}{
   575  					defaultType.Key: corekccv1alpha1.ResourceReference{
   576  						External: stateVal,
   577  					},
   578  				}
   579  			}
   580  		}
   581  		return corekccv1alpha1.ResourceReference{
   582  			External: stateVal,
   583  		}
   584  	case []interface{}:
   585  		if len(stateVal) == 0 {
   586  			return nil
   587  		}
   588  		refs := make([]interface{}, 0)
   589  		for _, elem := range stateVal {
   590  			var newRef interface{}
   591  			newRef = corekccv1alpha1.ResourceReference{
   592  				External: elem.(string),
   593  			}
   594  			// this is a repeat of the same short-term fix made above for the string case when Types is an array
   595  			if len(refConfig.Types) > 0 {
   596  				newRef = map[string]interface{}{
   597  					refConfig.Types[0].Key: newRef,
   598  				}
   599  			}
   600  			refs = append(refs, newRef)
   601  		}
   602  		return refs
   603  	default:
   604  		panic(fmt.Errorf("value of TF reference field '%v' was neither a string nor a list", tfField))
   605  	}
   606  }
   607  
   608  // convertTFSetToKCCSet converts a set object in Terraform to a KCC set object
   609  func convertTFSetToKCCSet(stateVal, prevSpecVal interface{}, schema *tfschema.Schema, rc *corekccv1alpha1.ResourceConfig, prefix string) interface{} {
   610  	if containsReferenceField(prefix, rc) {
   611  		// TODO(kcc-eng): Support the case where the hashing function depends on resolved values from
   612  		//  resource references. For the time being, fall back to the declared state.
   613  		return prevSpecVal
   614  	}
   615  	list := stateVal.([]interface{})
   616  	if len(list) == 0 {
   617  		return nil
   618  	}
   619  	// Get the hash for each of the values in our new state.
   620  	hashFunc := getHashFuncForSchema(schema)
   621  	stateHashMap := make(map[int]interface{})
   622  	for _, val := range list {
   623  		stateHashMap[hashFunc(asHashable(val, schema.Elem))] = val
   624  	}
   625  	// convert each element from the state, but adhering to the ordering from the user-defined spec in order to
   626  	// keep consistency when the user applies their config
   627  	prevList, _ := prevSpecVal.([]interface{})
   628  	retObjList := make([]interface{}, 0)
   629  	for _, prevElem := range prevList {
   630  		if prevElem == nil {
   631  			retObjList = append(retObjList, nil)
   632  			continue
   633  		}
   634  		var prevHashable interface{}
   635  		switch schemaElem := schema.Elem.(type) {
   636  		case *tfschema.Schema:
   637  			prevHashable = asHashable(prevElem, schemaElem)
   638  		case *tfschema.Resource:
   639  			// convert the KRM previous spec object to a TF object so that we can calculate the correct hash
   640  			prevElemAsTFObject, err := KRMObjectToTFObject(prevElem.(map[string]interface{}), schemaElem)
   641  			if err != nil {
   642  				panic(fmt.Errorf("error converting set object: %v", err))
   643  			}
   644  			prevHashable = asHashable(prevElemAsTFObject, schemaElem)
   645  		default:
   646  			panic(fmt.Errorf("unknown schema element type %v", schemaElem))
   647  		}
   648  		hash := hashFunc(prevHashable)
   649  		stateElem, ok := stateHashMap[hash]
   650  		// if the value is not in the stateHashMap, then it has been removed from the
   651  		// new spec.
   652  		if ok {
   653  			delete(stateHashMap, hash)
   654  		} else {
   655  			stateElem = map[string]interface{}{}
   656  		}
   657  		retObjList = append(retObjList,
   658  			convertTFElemToKCCElem(schema.Elem, stateElem, prevElem, rc, prefix))
   659  	}
   660  	// append any new elements in the list to the end
   661  	for _, newElem := range stateHashMap {
   662  		retObjList = append(retObjList,
   663  			convertTFElemToKCCElem(schema.Elem, newElem, nil, rc, prefix))
   664  	}
   665  	if len(retObjList) == 0 {
   666  		return nil
   667  	}
   668  	return retObjList
   669  }
   670  
   671  func getHashFuncForSchema(schema *tfschema.Schema) tfschema.SchemaSetFunc {
   672  	// Determine the hashing function. If none is provided by the provider, then the defaults are used.
   673  	hashFunc := schema.Set
   674  	if hashFunc == nil {
   675  		switch schemaElem := schema.Elem.(type) {
   676  		case *tfschema.Schema:
   677  			hashFunc = tfschema.HashSchema(schemaElem)
   678  		case *tfschema.Resource:
   679  			hashFunc = tfschema.HashResource(schemaElem)
   680  		}
   681  	}
   682  	return hashFunc
   683  }
   684  
   685  func asHashable(o, schemaElem interface{}) interface{} {
   686  	if o == nil {
   687  		return nil
   688  	}
   689  	switch schemaElem := schemaElem.(type) {
   690  	case *tfschema.Schema:
   691  		// There is no reader available except for a map reader, so for primitives we convert the field
   692  		// to a map with just one element
   693  		key := "k"
   694  		reader := tfschema.MapFieldReader{
   695  			Map:    tfschema.BasicMapReader(map[string]string{key: fmt.Sprintf("%v", o)}),
   696  			Schema: map[string]*tfschema.Schema{key: schemaElem},
   697  		}
   698  		val, err := reader.ReadField([]string{key})
   699  		if err != nil {
   700  			panic(fmt.Errorf("unable to convert field to hashable: %v", err))
   701  		}
   702  		var ret interface{}
   703  		if val.Exists {
   704  			ret = val.Value
   705  		}
   706  		return ret
   707  	case *tfschema.Resource:
   708  		// In order to hash an object in a set, we must have the object represented in a form that
   709  		// can be parsed by the Terraform hashing functions. This structure is exactly like our
   710  		// map[string]interface{} representations of TF objects, but substitutes any sets represented
   711  		// by []interface{} into a *tfschema.Set.
   712  		m := o.(map[string]interface{})
   713  		reader := tfschema.MapFieldReader{
   714  			Map:    tfschema.BasicMapReader(MapToInstanceState(schemaElem, m).Attributes),
   715  			Schema: schemaElem.Schema,
   716  		}
   717  		res := make(map[string]interface{})
   718  		for k, s := range schemaElem.Schema {
   719  			val, err := reader.ReadField([]string{k})
   720  			if err != nil {
   721  				panic(fmt.Errorf("unable to read field %v: %v", k, err))
   722  			}
   723  			if val.Exists {
   724  				res[k] = val.Value
   725  			} else {
   726  				res[k] = getDefaultValueForTFType(s.Type)
   727  			}
   728  		}
   729  		return res
   730  	default:
   731  		panic(fmt.Errorf("unknown schema element type %v", schemaElem))
   732  	}
   733  }
   734  
   735  func getDefaultValueForTFType(tfType tfschema.ValueType) interface{} {
   736  	switch tfType {
   737  	case tfschema.TypeBool:
   738  		return false
   739  	case tfschema.TypeString:
   740  		return ""
   741  	case tfschema.TypeFloat:
   742  		return 0.0
   743  	case tfschema.TypeInt:
   744  		return 0
   745  	case tfschema.TypeList:
   746  		return make([]interface{}, 0)
   747  	case tfschema.TypeSet:
   748  		return &tfschema.Set{}
   749  	case tfschema.TypeMap:
   750  		return make(map[string]interface{})
   751  	case tfschema.TypeInvalid:
   752  		panic("schema type is invalid")
   753  	default:
   754  		panic(fmt.Errorf("unrecognized schema type %v", tfType))
   755  	}
   756  }
   757  
   758  func convertTFElemToKCCElem(elemSchema, tfObj, prevSpecObj interface{}, rc *corekccv1alpha1.ResourceConfig, prefix string) interface{} {
   759  	switch elemSchema.(type) {
   760  	case *tfschema.Schema:
   761  		if prevSpecObj != nil {
   762  			return prevSpecObj
   763  		}
   764  		return tfObj
   765  	case *tfschema.Resource:
   766  		tfObjSchema := elemSchema.(*tfschema.Resource).Schema
   767  		tfObjMap, _ := tfObj.(map[string]interface{})
   768  		prevObjMap, _ := prevSpecObj.(map[string]interface{})
   769  		return convertTFMapToKCCMap(tfObjMap, prevObjMap, tfObjSchema, rc, prefix, nil)
   770  	default:
   771  		return prevSpecObj
   772  	}
   773  }
   774  
   775  func isOverriddenField(field string, rc *corekccv1alpha1.ResourceConfig) bool {
   776  	if field == rc.MetadataMapping.Name || field == rc.MetadataMapping.Labels {
   777  		return true
   778  	}
   779  	if rc.Locationality != "" && (field == "zone" || field == "region") {
   780  		return true
   781  	}
   782  	if isIgnoredField(field, rc) {
   783  		return true
   784  	}
   785  	for _, f := range rc.Directives {
   786  		if field == f {
   787  			return true
   788  		}
   789  	}
   790  	if !SupportsHierarchicalReferences(rc) {
   791  		// TODO(b/193177782): Delete this if-block once all resources support
   792  		// hierarchical references.
   793  		for _, c := range rc.Containers {
   794  			if field == c.TFField {
   795  				return true
   796  			}
   797  		}
   798  
   799  	}
   800  	return false
   801  }
   802  
   803  func isIgnoredField(field string, rc *corekccv1alpha1.ResourceConfig) bool {
   804  	for _, f := range rc.IgnoredFields {
   805  		if field == f {
   806  			return true
   807  		}
   808  	}
   809  	return false
   810  }
   811  
   812  func renameStatusFieldIfNeeded(tfResourceName, key string) string {
   813  	reservedNames := k8s.ReservedStatusFieldNames()
   814  	if _, found := reservedNames[key]; found {
   815  		return k8s.RenameStatusFieldWithReservedNameIfResourceNotExcluded(tfResourceName, key)
   816  	}
   817  	return key
   818  }
   819  

View as plain text