...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/krmtotf/managedfields.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  	"strings"
    20  
    21  	corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
    22  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    23  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/text"
    24  	tfresource "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/tf/resource"
    25  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/slice"
    26  
    27  	tfschema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    28  	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
    29  	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    30  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    31  )
    32  
    33  // resolveUnmanagedFields sets fields which no other server-side apply manager aside from
    34  // KCC has expressed an opinion on to their live state value.
    35  func resolveUnmanagedFields(spec map[string]interface{}, r *Resource, liveState *terraform.InstanceState,
    36  	jsonSchema *apiextensions.JSONSchemaProps) (map[string]interface{}, error) {
    37  	if r.ManagedFields == nil {
    38  		// If this resource does not have SSA enabled, there is no way to distinguish
    39  		// fields that are k8s-managed from fields that are externally-managed.
    40  		// In this case, treat the whole spec in etcd as the desired state.
    41  		return spec, nil
    42  	}
    43  
    44  	var stateAsKRM map[string]interface{}
    45  	var err error
    46  
    47  	if liveState.Empty() {
    48  		stateAsKRM = make(map[string]interface{})
    49  	} else {
    50  		stateAsKRM, _ = GetSpecAndStatusFromState(r, liveState)
    51  	}
    52  
    53  	switch r.Kind {
    54  	// TODO(b/223303389): Roll out ability to switch between conflicting fields
    55  	// to remaining resources.
    56  	case "BigtableAppProfile", "CloudBuildTrigger", "ResourceManagerPolicy":
    57  		if err = RemoveFieldsFromStateThatConflictWithSpec(stateAsKRM, spec, r.ResourceConfig, []string{}, r.TFResource.Schema); err != nil {
    58  			return nil, fmt.Errorf("error stripping fields from state that conflict with fields already in spec: %w", err)
    59  		}
    60  	}
    61  
    62  	return k8s.OverlayManagedFieldsOntoState(spec, stateAsKRM, r.ManagedFields, jsonSchema, r.ResourceConfig.HierarchicalReferences)
    63  }
    64  
    65  // RemoveFieldsFromStateThatConflictWithSpec removes fields from 'state' that
    66  // conflict with any of the fields found in 'spec'. This is useful for when we
    67  // want to overlay 'state' onto 'spec' without ending up with an invalid
    68  // resource configuration.
    69  func RemoveFieldsFromStateThatConflictWithSpec(state map[string]interface{}, spec map[string]interface{},
    70  	rc corekccv1alpha1.ResourceConfig, tfPath []string, schemaMap map[string]*tfschema.Schema) error {
    71  	if len(state) == 0 || len(spec) == 0 {
    72  		return nil
    73  	}
    74  	for k, s := range schemaMap {
    75  		tfPath := append(tfPath, k)
    76  		tfField := strings.Join(tfPath, ".")
    77  		krmPath := text.SnakeCaseStrsToLowerCamelCaseStrs(tfPath)
    78  		if ok, refConfig := IsReferenceField(tfField, &rc); ok {
    79  			krmPath = getPathToReferenceKey(refConfig)
    80  		}
    81  
    82  		_, found, err := unstructured.NestedFieldNoCopy(state, krmPath...)
    83  		if err != nil {
    84  			return fmt.Errorf("error checking for existence of field with path %v in state: %w", krmPath, err)
    85  		}
    86  		if !found {
    87  			continue
    88  		}
    89  
    90  		// Determine the keys which conflict with the current key. Note:
    91  		// ConflictsWith and ExactlyOneOf specify keys in format
    92  		// "parent_field.0.child_field.0.grandchild_field".
    93  		conflictingTFKeys := slice.RemoveStringFromStringSlice(
    94  			slice.ConcatStringSlices(s.ConflictsWith, s.ExactlyOneOf),
    95  			strings.Join(tfPath, ".0."), // Remove current key which is included in ExactlyOneOf.
    96  		)
    97  		for _, conflictingTFKey := range conflictingTFKeys {
    98  			conflictingTFPath := strings.Split(conflictingTFKey, ".0.")
    99  			conflictingTFField := strings.Join(conflictingTFPath, ".")
   100  			conflictingKRMPath := text.SnakeCaseStrsToLowerCamelCaseStrs(conflictingTFPath)
   101  			if ok, refConfig := IsReferenceField(conflictingTFField, &rc); ok {
   102  				conflictingKRMPath = getPathToReferenceKey(refConfig)
   103  			}
   104  
   105  			_, found, err := unstructured.NestedFieldNoCopy(spec, conflictingKRMPath...)
   106  			if err != nil {
   107  				return fmt.Errorf("error checking for existence of conflicting field with path %v in spec: %w", krmPath, err)
   108  			}
   109  			if !found {
   110  				continue
   111  			}
   112  
   113  			if err := removeFieldAndAnyEmptyAncestorsFromObject(krmPath, state); err != nil {
   114  				return fmt.Errorf("error removing field at path %v from state: %w", krmPath, err)
   115  			}
   116  		}
   117  
   118  		// If field is an object, check if any of its subfields need to be
   119  		// removed. Note: ConflictsWith and ExactlyOneOf can only point to
   120  		// top-level fields or fields nested in objects. They cannot point to
   121  		// fields in objects in lists/sets.
   122  		if !tfresource.IsObjectField(s) {
   123  			continue
   124  		}
   125  		if err := RemoveFieldsFromStateThatConflictWithSpec(state, spec, rc, tfPath, s.Elem.(*tfschema.Resource).Schema); err != nil {
   126  			return err
   127  		}
   128  	}
   129  	return nil
   130  }
   131  
   132  // removeFieldAndAnyEmptyAncestorsFromObject removes the field at 'path' from
   133  // 'obj'. And, if removing the field results in an empty object at the parent
   134  // field, the function removes the parent field too, and then the grandparent
   135  // field, and so on.
   136  func removeFieldAndAnyEmptyAncestorsFromObject(path []string, obj map[string]interface{}) error {
   137  	unstructured.RemoveNestedField(obj, path...)
   138  
   139  	// If the field is a top-level field, we can stop now since there are no
   140  	// ancestor fields to potentially remove.
   141  	if len(path) <= 1 {
   142  		return nil
   143  	}
   144  
   145  	// Remove ancestor fields that are now empty, starting from the parent
   146  	// field and then going up the tree.
   147  	for i := len(path) - 2; i >= 0; i-- {
   148  		ancestorPath := path[0 : i+1]
   149  		val, found, err := unstructured.NestedFieldNoCopy(obj, ancestorPath...)
   150  		if err != nil {
   151  			return fmt.Errorf("error checking for existence of field ancestor at path: %v: %w", ancestorPath, err)
   152  		}
   153  		if !found {
   154  			return fmt.Errorf("unexpectedly failed to find field ancestor at path %v", ancestorPath)
   155  		}
   156  		valAsObj, ok := val.(map[string]interface{})
   157  		if !ok {
   158  			return fmt.Errorf("field ancestor at path %v is unexpectedly not a map[string]interface{}", ancestorPath)
   159  		}
   160  		if len(valAsObj) > 0 {
   161  			return nil
   162  		}
   163  		unstructured.RemoveNestedField(obj, ancestorPath...)
   164  	}
   165  	return nil
   166  }
   167  

View as plain text