...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/webhook/immutable_fields_validator.go

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

     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 webhook
    16  
    17  import (
    18  	"container/list"
    19  	"context"
    20  	"fmt"
    21  	"net/http"
    22  	"reflect"
    23  	"regexp"
    24  	"strings"
    25  
    26  	corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
    27  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl"
    28  	dclextension "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/extension"
    29  	dclcontainer "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/extension/container"
    30  	dclmetadata "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/metadata"
    31  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/schema/dclschemaloader"
    32  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    33  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/krmtotf"
    34  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/servicemapping/servicemappingloader"
    35  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/text"
    36  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/pathslice"
    37  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/typeutil"
    38  
    39  	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    40  	"github.com/hashicorp/terraform-provider-google-beta/google-beta"
    41  	"github.com/nasa9084/go-openapi"
    42  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    43  	"k8s.io/apimachinery/pkg/runtime"
    44  	"k8s.io/apimachinery/pkg/runtime/serializer"
    45  	"k8s.io/klog/v2"
    46  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    47  )
    48  
    49  var (
    50  	scheme           = runtime.NewScheme()
    51  	codecs           = serializer.NewCodecFactory(scheme)
    52  	TFSchemaNotFound = fmt.Errorf("schema does not exist")
    53  )
    54  
    55  type immutableFieldsValidatorHandler struct {
    56  	smLoader              *servicemappingloader.ServiceMappingLoader
    57  	tfResourceMap         map[string]*schema.Resource
    58  	dclSchemaLoader       dclschemaloader.DCLSchemaLoader
    59  	serviceMetadataLoader dclmetadata.ServiceMetadataLoader
    60  }
    61  
    62  var (
    63  	allowedResponse = admission.ValidationResponse(true, "admission controller passed")
    64  )
    65  
    66  func NewImmutableFieldsValidatorHandler(smLoader *servicemappingloader.ServiceMappingLoader, dclSchemaLoader dclschemaloader.DCLSchemaLoader, serviceMetadataLoader dclmetadata.ServiceMetadataLoader) *immutableFieldsValidatorHandler {
    67  	return &immutableFieldsValidatorHandler{
    68  		smLoader:              smLoader,
    69  		tfResourceMap:         google.ResourceMap(),
    70  		dclSchemaLoader:       dclSchemaLoader,
    71  		serviceMetadataLoader: serviceMetadataLoader,
    72  	}
    73  }
    74  
    75  func (a *immutableFieldsValidatorHandler) Handle(ctx context.Context, req admission.Request) admission.Response {
    76  	if regexp.MustCompile(ControllerManagerServiceAccountRegex).MatchString(req.AdmissionRequest.UserInfo.Username) {
    77  		return admission.ValidationResponse(true, "ignore non-user requests")
    78  	}
    79  
    80  	// decode the existing object and the updated object
    81  	deserializer := codecs.UniversalDeserializer()
    82  	obj := &unstructured.Unstructured{}
    83  	if _, _, err := deserializer.Decode(req.AdmissionRequest.Object.Raw, nil, obj); err != nil {
    84  		klog.Error(err)
    85  		return admission.Errored(http.StatusBadRequest,
    86  			fmt.Errorf("error decoding object: %v", err))
    87  	}
    88  	oldObj := &unstructured.Unstructured{}
    89  	if _, _, err := deserializer.Decode(req.AdmissionRequest.OldObject.Raw, nil, oldObj); err != nil {
    90  		klog.Error(err)
    91  		return admission.Errored(http.StatusBadRequest,
    92  			fmt.Errorf("error decoding old object: %v", err))
    93  	}
    94  
    95  	spec, ok := obj.Object["spec"].(map[string]interface{})
    96  	if obj.Object["spec"] != nil && !ok {
    97  		return admission.Errored(http.StatusBadRequest,
    98  			fmt.Errorf("the type of spec field is not map[string]interface{}"))
    99  	}
   100  	oldSpec, ok := oldObj.Object["spec"].(map[string]interface{})
   101  	if oldObj.Object["spec"] != nil && !ok {
   102  		return admission.Errored(http.StatusBadRequest,
   103  			fmt.Errorf("the type of spec field is not map[string]interface{}"))
   104  	}
   105  
   106  	if isIAMResource(oldObj) {
   107  		return validateImmutableFieldsForIAMResource(oldObj, oldSpec, spec)
   108  	}
   109  
   110  	if err := validateImmutableStateIntoSpecAnnotation(obj, oldObj); err != nil {
   111  		return admission.Errored(http.StatusForbidden, err)
   112  	}
   113  
   114  	if dclmetadata.IsDCLBasedResourceKind(obj.GroupVersionKind(), a.serviceMetadataLoader) {
   115  		return validateImmutableFieldsForDCLBasedResource(obj, oldObj, spec, oldSpec, a.dclSchemaLoader, a.serviceMetadataLoader)
   116  	}
   117  	return validateImmutableFieldsForTFBasedResource(obj, oldObj, spec, oldSpec, a.smLoader, a.tfResourceMap)
   118  }
   119  
   120  func validateImmutableStateIntoSpecAnnotation(obj, oldObj *unstructured.Unstructured) error {
   121  	val, found := k8s.GetAnnotation(k8s.StateIntoSpecAnnotation, obj)
   122  	prevVal, prevFound := k8s.GetAnnotation(k8s.StateIntoSpecAnnotation, oldObj)
   123  	if found != prevFound || val != prevVal {
   124  		return fmt.Errorf("annotation %v is immutable", k8s.StateIntoSpecAnnotation)
   125  	}
   126  	return nil
   127  }
   128  
   129  func validateImmutableFieldsForDCLBasedResource(obj, oldObj *unstructured.Unstructured, spec, oldSpec map[string]interface{}, dclSchemaLoader dclschemaloader.DCLSchemaLoader, serviceMetadataLoader dclmetadata.ServiceMetadataLoader) admission.Response {
   130  	gvk := obj.GroupVersionKind()
   131  	schema, err := dclschemaloader.GetDCLSchemaForGVK(gvk, serviceMetadataLoader, dclSchemaLoader)
   132  	if err != nil {
   133  		return admission.Errored(http.StatusInternalServerError,
   134  			fmt.Errorf("error getting the DCL Schema for GroupVersionKind %v: %w", gvk, err))
   135  	}
   136  	containers, err := dclcontainer.GetContainersForGVK(gvk, serviceMetadataLoader, dclSchemaLoader)
   137  	if err != nil {
   138  		return admission.Errored(http.StatusInternalServerError,
   139  			fmt.Errorf("error getting containers supported by GroupVersionKind %v: %v", gvk, err))
   140  	}
   141  	hierarchicalRefs, err := dcl.GetHierarchicalReferencesForGVK(gvk, serviceMetadataLoader, dclSchemaLoader)
   142  	if err != nil {
   143  		return admission.Errored(http.StatusInternalServerError,
   144  			fmt.Errorf("error getting hierarchical references supported by GroupVersionKind %v: %v", gvk, err))
   145  	}
   146  	if err := validateContainerAnnotationsForResource(gvk.Kind, obj.GetAnnotations(), oldObj.GetAnnotations(), containers, hierarchicalRefs); err != nil {
   147  		return admission.Errored(http.StatusBadRequest,
   148  			fmt.Errorf("error validating container annotations: %v", err))
   149  	}
   150  	if isResourceIDModified(spec, oldSpec) {
   151  		return admission.Errored(http.StatusForbidden,
   152  			k8s.NewImmutableFieldsMutationError([]string{k8s.ResourceIDFieldPath}))
   153  	}
   154  	res, err := getChangesOnImmutableFields(spec, oldSpec, []string{"spec"}, []string{}, schema, hierarchicalRefs)
   155  	if err != nil {
   156  		return admission.Errored(http.StatusInternalServerError,
   157  			fmt.Errorf("unexpected error: %w", err))
   158  	}
   159  	if len(res) != 0 {
   160  		return admission.Errored(http.StatusForbidden,
   161  			k8s.NewImmutableFieldsMutationError(res))
   162  	}
   163  	return allowedResponse
   164  }
   165  
   166  func getChangesOnImmutableFields(spec, oldSpec map[string]interface{}, krmPath, dclPath []string, schema *openapi.Schema, hierarchicalRefs []corekccv1alpha1.HierarchicalReference) ([]string, error) {
   167  	if schema.Type != "object" {
   168  		return nil, fmt.Errorf("expect the schame type to be 'object', but got %v", schema.Type)
   169  	}
   170  	ret := make([]string, 0)
   171  	for f, s := range schema.Properties {
   172  		if s.ReadOnly {
   173  			continue
   174  		}
   175  		isImmutable, err := dclextension.IsImmutableField(s)
   176  		if err != nil {
   177  			return nil, fmt.Errorf("error determining if field %v is immutable", f)
   178  		}
   179  		if dclextension.IsReferenceField(s) {
   180  			if !isImmutable {
   181  				continue
   182  			}
   183  			// If parent reference field is immutable and resource supports
   184  			// multiple parent types, changes to either the hierarchical
   185  			// reference key or value should be rejected.
   186  			if dcl.IsMultiTypeParentReferenceField(append(dclPath, f)) {
   187  				for _, h := range hierarchicalRefs {
   188  					if !reflect.DeepEqual(oldSpec[h.Key], spec[h.Key]) {
   189  						krmPathToField := pathslice.ToString(append(krmPath, h.Key))
   190  						ret = append(ret, krmPathToField)
   191  					}
   192  				}
   193  				continue
   194  			}
   195  			refField, err := dclextension.GetReferenceFieldName(append(dclPath, f), s)
   196  			if err != nil {
   197  				return nil, err
   198  			}
   199  			if !reflect.DeepEqual(oldSpec[refField], spec[refField]) {
   200  				krmPathToField := pathslice.ToString(append(krmPath, refField))
   201  				ret = append(ret, krmPathToField)
   202  			}
   203  			continue
   204  		}
   205  		krmPathToField := pathslice.ToString(append(krmPath, f))
   206  		oldVal := oldSpec[f]
   207  		newVal := spec[f]
   208  		if oldVal == nil && newVal == nil {
   209  			continue
   210  		}
   211  		if isImmutable && (oldVal == nil || newVal == nil) {
   212  			ret = append(ret, krmPathToField)
   213  			continue
   214  		}
   215  		switch s.Type {
   216  		case "object":
   217  			var v1 map[string]interface{}
   218  			var v2 map[string]interface{}
   219  			if oldVal != nil {
   220  				v1 = oldVal.(map[string]interface{})
   221  			}
   222  			if newVal != nil {
   223  				v2 = newVal.(map[string]interface{})
   224  			}
   225  			// Field is a map of key-value pairs
   226  			if s.AdditionalProperties != nil {
   227  				// If map field is immutable, reject any mutations.
   228  				if isImmutable {
   229  					if !reflect.DeepEqual(v1, v2) {
   230  						ret = append(ret, krmPathToField)
   231  					}
   232  					continue
   233  				}
   234  				if typeutil.IsPrimitiveType(s.AdditionalProperties.Type) {
   235  					continue
   236  				}
   237  				// If map field is mutable, but its key-value pairs have
   238  				// non-primitive values (e.g. objects, arrays of objects), the
   239  				// values themselves may have immutable fields. For now, let
   240  				// such cases pass through the webhook, and let DCL or the GCP
   241  				// API handle them instead (see b/216381382).
   242  				continue
   243  			}
   244  			// Field is an object
   245  			nestedFields, err := getChangesOnImmutableFields(v1, v2, append(krmPath, f), append(dclPath, f), s, hierarchicalRefs)
   246  			if err != nil {
   247  				return nil, err
   248  			}
   249  			ret = append(ret, nestedFields...)
   250  		case "array":
   251  			if typeutil.IsPrimitiveType(s.Items.Type) {
   252  				if isImmutable && !reflect.DeepEqual(oldVal, newVal) {
   253  					ret = append(ret, krmPathToField)
   254  				}
   255  				continue
   256  			}
   257  			// Kubernetes considers all lists of objects to be atomic, and so all subsequent
   258  			// applies will currently wipe out defaulted immutable fields. Temporarily delegate validation to DCL.
   259  		case "string", "boolean", "number", "integer":
   260  			if isImmutable && !reflect.DeepEqual(oldSpec[f], spec[f]) {
   261  				ret = append(ret, krmPathToField)
   262  			}
   263  		default:
   264  			return nil, fmt.Errorf("unknown schema type %v", s.Type)
   265  		}
   266  	}
   267  	return ret, nil
   268  }
   269  
   270  func getQualifiedFieldName(prefix string, fieldName string) string {
   271  	qualifiedName := fieldName
   272  	if prefix != "" {
   273  		qualifiedName = prefix + "." + fieldName
   274  	}
   275  	return qualifiedName
   276  }
   277  
   278  func validateImmutableFieldsForTFBasedResource(obj, oldObj *unstructured.Unstructured, spec, oldSpec map[string]interface{}, smLoader *servicemappingloader.ServiceMappingLoader, tfResourceMap map[string]*schema.Resource) admission.Response {
   279  	rc, err := smLoader.GetResourceConfig(obj)
   280  	if err != nil {
   281  		return admission.Errored(http.StatusBadRequest,
   282  			fmt.Errorf("couldn't get ResourceConfig for kind %v: %v", obj.GetKind(), err))
   283  	}
   284  
   285  	if err := validateContainerAnnotationsForResource(obj.GetKind(), obj.GetAnnotations(), oldObj.GetAnnotations(), rc.Containers, rc.HierarchicalReferences); err != nil {
   286  		return admission.Errored(http.StatusBadRequest,
   287  			fmt.Errorf("error validating container annotations: %v", err))
   288  	}
   289  
   290  	r, ok := tfResourceMap[rc.Name]
   291  	if !ok {
   292  		return admission.Errored(http.StatusInternalServerError,
   293  			fmt.Errorf("unknown resource %v", rc.Name))
   294  	}
   295  
   296  	if findChangesOnImmutableResourceIDField(spec, oldSpec, rc) {
   297  		return admission.Errored(http.StatusForbidden,
   298  			k8s.NewImmutableFieldsMutationError([]string{k8s.ResourceIDFieldPath}))
   299  	}
   300  
   301  	if findChangesOnImmutableLocationField(spec, oldSpec, rc) {
   302  		return admission.Errored(http.StatusForbidden,
   303  			k8s.NewImmutableFieldsMutationError([]string{"spec.location"}))
   304  	}
   305  
   306  	fields := list.New()
   307  	compareAndFindChangesOnImmutableFields(spec, oldSpec, r.Schema, "", rc, getIgnoredFields(rc), fields)
   308  	if fields.Len() != 0 {
   309  		res := make([]string, 0)
   310  		for e := fields.Front(); e != nil; e = e.Next() {
   311  			res = append(res, constructCamelCasePath(e.Value.(string)))
   312  		}
   313  		return admission.Errored(http.StatusBadRequest,
   314  			k8s.NewImmutableFieldsMutationError(res))
   315  	}
   316  
   317  	return allowedResponse
   318  }
   319  
   320  func validateContainerAnnotationsForResource(kind string, annotations, oldAnnotations map[string]string, containers []corekccv1alpha1.Container, hierarchicalRefs []corekccv1alpha1.HierarchicalReference) error {
   321  	// TODO(b/193177782): Delete this if-block once all resources support
   322  	// hierarchical references.
   323  	if len(hierarchicalRefs) == 0 {
   324  		return validateContainerAnnotations(kind, annotations, oldAnnotations, containers)
   325  	}
   326  	return validateDeprecatedContainerAnnotations(annotations, oldAnnotations, containers, hierarchicalRefs)
   327  }
   328  
   329  func validateContainerAnnotations(kind string, annotations, oldAnnotations map[string]string, containers []corekccv1alpha1.Container) error {
   330  	for _, c := range containers {
   331  		a := k8s.GetAnnotationForContainerType(c.Type)
   332  
   333  		// No changes to the container annotation.
   334  		if oldAnnotations[a] == annotations[a] {
   335  			continue
   336  		}
   337  
   338  		// Reject changes to container annotations except for Projects and
   339  		// Folders which rely on container annotation updates to allow for
   340  		// migrations across different parent Folders and Organizations.
   341  		if kind != "Project" && kind != "Folder" {
   342  			return fmt.Errorf("cannot make changes to container annotation %v", a)
   343  		}
   344  
   345  		// Reject changes from one container annotation type to another.
   346  		for _, otherC := range containers {
   347  			if c == otherC {
   348  				continue
   349  			}
   350  			otherA := k8s.GetAnnotationForContainerType(otherC.Type)
   351  			_, ok := oldAnnotations[a]
   352  			_, otherOk := annotations[otherA]
   353  			if ok && otherOk {
   354  				return fmt.Errorf("cannot change from container annotation %v to container annotation %v", a, otherA)
   355  			}
   356  		}
   357  	}
   358  	return nil
   359  }
   360  
   361  func validateDeprecatedContainerAnnotations(annotations, oldAnnotations map[string]string, containers []corekccv1alpha1.Container, hierarchicalRefs []corekccv1alpha1.HierarchicalReference) error {
   362  	for _, c := range containers {
   363  		a := k8s.GetAnnotationForContainerType(c.Type)
   364  
   365  		// No changes to the container annotation.
   366  		if oldAnnotations[a] == annotations[a] {
   367  			continue
   368  		}
   369  
   370  		// Container annotation was removed. This is a change that we allow to
   371  		// give users the ability to "clean" their configurations now that
   372  		// container annotations have been deprecated for this resource.
   373  		if annotations[a] == "" {
   374  			continue
   375  		}
   376  
   377  		// Container annotation was either added or changed.
   378  		possibleFields := k8s.HierarchicalReferencesToFields(hierarchicalRefs)
   379  		return fmt.Errorf("cannot add/change container annotation %v as it is no longer supported by the resource; set one of [%v] instead.", a, strings.Join(possibleFields, ", "))
   380  	}
   381  	return nil
   382  }
   383  
   384  func validateImmutableFieldsForIAMResource(oldObj *unstructured.Unstructured, oldSpec, newSpec map[string]interface{}) admission.Response {
   385  	if isIAMPolicy(oldObj) {
   386  		return handleIAMPolicy(oldSpec, newSpec)
   387  	}
   388  	if isIAMPartialPolicy(oldObj) {
   389  		return handleIAMPartialPolicy(oldSpec, newSpec)
   390  	}
   391  	if isIAMPolicyMember(oldObj) {
   392  		return handleIAMPolicyMember(oldSpec, newSpec)
   393  	}
   394  	if isIAMAuditConfig(oldObj) {
   395  		return handleIAMAuditConfig(oldSpec, newSpec)
   396  	}
   397  	return admission.ValidationResponse(false, fmt.Sprintf("unknown IAM resource type: %v", oldObj.GroupVersionKind()))
   398  }
   399  
   400  func handleIAMPolicy(oldSpec, newSpec map[string]interface{}) admission.Response {
   401  	if isIAMResourceReferenceModified(oldSpec, newSpec) {
   402  		msg := fmt.Sprintf("the IAMPolicy's spec.resourceRef is immutable")
   403  		return admission.ValidationResponse(false, msg)
   404  	}
   405  	return allowedResponse
   406  }
   407  
   408  func handleIAMPartialPolicy(oldSpec, newSpec map[string]interface{}) admission.Response {
   409  	if isIAMResourceReferenceModified(oldSpec, newSpec) {
   410  		msg := fmt.Sprintf("the IAMPartialPolicy's spec.resourceRef is immutable")
   411  		return admission.ValidationResponse(false, msg)
   412  	}
   413  	return allowedResponse
   414  }
   415  
   416  func handleIAMPolicyMember(oldSpec, newSpec map[string]interface{}) admission.Response {
   417  	if isIAMSpecModified(oldSpec, newSpec) {
   418  		msg := fmt.Sprintf("the IAMPolicyMember's spec is immutable")
   419  		return admission.ValidationResponse(false, msg)
   420  	}
   421  	return allowedResponse
   422  }
   423  
   424  func handleIAMAuditConfig(oldSpec, newSpec map[string]interface{}) admission.Response {
   425  	if isIAMResourceReferenceModified(oldSpec, newSpec) {
   426  		msg := fmt.Sprintf("the IAMAuditConfig's spec.resourceRef is immutable")
   427  		return admission.ValidationResponse(false, msg)
   428  	}
   429  	if isIAMAuditConfigServiceModified(oldSpec, newSpec) {
   430  		msg := fmt.Sprintf("the IAMAuditConfig's spec.service is immutable")
   431  		return admission.ValidationResponse(false, msg)
   432  	}
   433  	return allowedResponse
   434  }
   435  
   436  func findChangesOnImmutableResourceIDField(spec, oldSpec map[string]interface{}, rc *corekccv1alpha1.ResourceConfig) bool {
   437  	if rc.ResourceID.TargetField == "" {
   438  		return false
   439  	}
   440  
   441  	return isResourceIDModified(spec, oldSpec)
   442  }
   443  
   444  func isResourceIDModified(spec, oldSpec map[string]interface{}) bool {
   445  	return !reflect.DeepEqual(spec[k8s.ResourceIDFieldName], oldSpec[k8s.ResourceIDFieldName])
   446  }
   447  
   448  func findChangesOnImmutableLocationField(obj map[string]interface{}, oldObj map[string]interface{}, rc *corekccv1alpha1.ResourceConfig) bool {
   449  	if rc.Locationality == "" {
   450  		return false
   451  	}
   452  	// Location is immutable by default as it's part of the URL in underlying api.
   453  	return !reflect.DeepEqual(obj["location"], oldObj["location"])
   454  }
   455  
   456  // TODO: get rid of list.List by changing the function to return a []string recursively
   457  func compareAndFindChangesOnImmutableFields(obj map[string]interface{}, oldObj map[string]interface{}, schemaMap map[string]*schema.Schema, prefix string, resourceConfig *corekccv1alpha1.ResourceConfig, ignoredFields map[string]bool, fields *list.List) {
   458  	for k, s := range schemaMap {
   459  		qualifiedName := getQualifiedFieldName(prefix, k)
   460  		if ignoredFields[qualifiedName] {
   461  			continue
   462  		}
   463  
   464  		if ok, refConfig := krmtotf.IsReferenceField(qualifiedName, resourceConfig); ok {
   465  			if !s.ForceNew {
   466  				continue
   467  			}
   468  			modified, refKey := isReferenceValRawModified(obj, oldObj, refConfig)
   469  			if modified {
   470  				refKey = getQualifiedFieldName(prefix, refKey)
   471  				fields.PushBack(refKey)
   472  			}
   473  			continue
   474  		}
   475  
   476  		camelCaseKey := text.SnakeCaseToLowerCamelCase(k)
   477  		v1 := obj[camelCaseKey]
   478  		v2 := oldObj[camelCaseKey]
   479  		if v1 == nil && v2 == nil {
   480  			continue
   481  		}
   482  		if (v1 == nil || v2 == nil) && s.ForceNew {
   483  			fields.PushBack(qualifiedName)
   484  			continue
   485  		}
   486  
   487  		switch s.Type {
   488  		// TODO: terraform schema doc says that TypeMap only support Elem to be a *Schema with a Type that is one of the primitives
   489  		// Is there any edge cases to handle?
   490  		case schema.TypeBool, schema.TypeFloat, schema.TypeString, schema.TypeInt, schema.TypeMap:
   491  			if s.ForceNew && !reflect.DeepEqual(v1, v2) {
   492  				fields.PushBack(qualifiedName)
   493  			}
   494  		case schema.TypeList, schema.TypeSet:
   495  			switch s.Elem.(type) {
   496  			case *schema.Schema:
   497  				// it's a list of primitives
   498  				if s.ForceNew && !reflect.DeepEqual(v1, v2) {
   499  					fields.PushBack(qualifiedName)
   500  				}
   501  			case *schema.Resource:
   502  				if s.MaxItems == 1 {
   503  					// A list with MaxItems == 1 is actually a nested object due to limitations with TF schemas.
   504  					tfObjSchemaMap := s.Elem.(*schema.Resource).Schema
   505  					var o1 map[string]interface{}
   506  					var o2 map[string]interface{}
   507  					if v1 != nil {
   508  						o1 = v1.(map[string]interface{})
   509  					}
   510  					if v2 != nil {
   511  						o2 = v2.(map[string]interface{})
   512  					}
   513  					compareAndFindChangesOnImmutableFields(o1, o2, tfObjSchemaMap, qualifiedName, resourceConfig, ignoredFields, fields)
   514  				} else {
   515  					// TODO(kcc-eng): Kubernetes considers all lists of objects to be atomic, and so all subsequent
   516  					//  applies will currently wipe out defaulted immutable fields. Temporarily delegate validation
   517  					//  to the controller, which will determine via comparing the config with calculated fields in
   518  					//  the live state to detect if a diff in immutable fields is present.
   519  				}
   520  			}
   521  		}
   522  	}
   523  }
   524  
   525  func isReferenceValRawModified(obj map[string]interface{}, oldObj map[string]interface{}, refConfig *corekccv1alpha1.ReferenceConfig) (bool, string) {
   526  	// currently we choose to treat switching between different reference approaches as modification, e.g. change from referencing to ComputeAddress to directly specifying ip address
   527  	referenceFieldKey := krmtotf.GetKeyForReferenceField(refConfig)
   528  	return !reflect.DeepEqual(obj[referenceFieldKey], oldObj[referenceFieldKey]), referenceFieldKey
   529  }
   530  
   531  func constructCamelCasePath(path string) string {
   532  	segs := make([]string, 0)
   533  	for _, f := range strings.Split(path, ".") {
   534  		segs = append(segs, text.SnakeCaseToLowerCamelCase(f))
   535  	}
   536  	return strings.Join(segs, ".")
   537  }
   538  
   539  func getIgnoredFields(rc *corekccv1alpha1.ResourceConfig) map[string]bool {
   540  	ignoredFields := make(map[string]bool)
   541  	for _, f := range rc.IgnoredFields {
   542  		ignoredFields[f] = true
   543  	}
   544  
   545  	// metadata mapping can be ignored because there are only two k8s metadata fields mapping to TF fields: name and label
   546  	// k8s object names are unique identifiers and labels are totally mutable by default
   547  	ignoredFields[rc.MetadataMapping.Name] = true
   548  	ignoredFields[rc.MetadataMapping.Labels] = true
   549  	return ignoredFields
   550  }
   551  

View as plain text