...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/krmtotf/fetchlivestate.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  	"context"
    19  	"fmt"
    20  
    21  	corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
    22  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/deepcopy"
    23  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    24  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/label"
    25  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/servicemapping/servicemappingloader"
    26  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/text"
    27  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util"
    28  
    29  	tfschema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    30  	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
    31  	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    32  	"sigs.k8s.io/controller-runtime/pkg/client"
    33  )
    34  
    35  // FetchLiveState is a combination of a resource import + read. It returns the state of the
    36  // underlying resource as seen by the TF provider.
    37  func FetchLiveState(ctx context.Context, resource *Resource, provider *tfschema.Provider, kubeClient client.Client, smLoader *servicemappingloader.ServiceMappingLoader) (*terraform.InstanceState, error) {
    38  	// Get the ID to pass to the import
    39  	id, err := resource.GetImportID(kubeClient, smLoader)
    40  	if err != nil {
    41  		if _, ok := k8s.AsServerGeneratedIDNotFoundError(err); ok {
    42  			// If the import ID cannot be determined because it requires a server-
    43  			// generated ID that has not been set, this means the resource has not
    44  			// yet been created. Return as if the read returned a non-existent
    45  			// resource.
    46  			state := &terraform.InstanceState{}
    47  			state = SetBlueprintAttribution(state, resource, provider)
    48  			return state, nil
    49  		}
    50  		return nil, fmt.Errorf("error getting ID for resource: %w", err)
    51  	}
    52  	return fetchLiveStateFromId(ctx, id, resource, provider, kubeClient, smLoader)
    53  
    54  }
    55  
    56  // Special handling for KMSCryptoKey that still lives after its parent KMSKeyRing is deleted.
    57  // We can import the tf state directly from itself instead of sourcing for its parent.
    58  // More info in b/279485255#comment14
    59  func shouldGetImportIDFromSelfForDelete(resource *Resource) bool {
    60  	return resource.Kind == "KMSCryptoKey"
    61  }
    62  
    63  func ShouldResolveParentForDelete(resource *Resource) bool {
    64  	return !shouldGetImportIDFromSelfForDelete(resource) || hasEmptySelfLink(resource)
    65  }
    66  
    67  func hasEmptySelfLink(resource *Resource) bool {
    68  	id, err := resource.SelfLinkAsID()
    69  	if err != nil || id == "" {
    70  		return true
    71  	}
    72  	return false
    73  }
    74  
    75  func FetchLiveStateForDelete(ctx context.Context, resource *Resource, provider *tfschema.Provider, kubeClient client.Client, smLoader *servicemappingloader.ServiceMappingLoader) (*terraform.InstanceState, error) {
    76  	if shouldGetImportIDFromSelfForDelete(resource) {
    77  		id, err := resource.SelfLinkAsID()
    78  		if err != nil {
    79  			return nil, err
    80  		}
    81  		if id != "" {
    82  			return fetchLiveStateFromId(ctx, id, resource, provider, kubeClient, smLoader)
    83  		}
    84  	}
    85  	return FetchLiveState(ctx, resource, provider, kubeClient, smLoader)
    86  }
    87  
    88  func fetchLiveStateFromId(ctx context.Context, id string, resource *Resource, provider *tfschema.Provider, kubeClient client.Client, smLoader *servicemappingloader.ServiceMappingLoader) (*terraform.InstanceState, error) {
    89  	// Get the imported resource
    90  	var state *terraform.InstanceState
    91  	var err error
    92  	if resource.ResourceConfig.SkipImport {
    93  		state = &terraform.InstanceState{ID: id}
    94  	} else {
    95  		state, err = ImportState(ctx, id, resource.TFInfo, provider)
    96  		if err != nil {
    97  			return nil, err
    98  		}
    99  	}
   100  
   101  	// Given that some fields are input-only or may only be returned on creation,
   102  	// e.g. private key, we need to stick with the previously captured values.
   103  	state, err = presetFieldsForRead(resource, state, kubeClient, smLoader)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  	state = SetBlueprintAttribution(state, resource, provider)
   108  	state, diagnostics := resource.TFResource.RefreshWithoutUpgrade(ctx, state, provider.Meta())
   109  	if err := NewErrorFromDiagnostics(diagnostics); err != nil {
   110  		return nil, fmt.Errorf("error reading underlying resource: %v", err)
   111  	}
   112  	// Set the blueprint attribution again in case the Refresh returns nil, which
   113  	// clears the previously set value.
   114  	state = SetBlueprintAttribution(state, resource, provider)
   115  	return state, nil
   116  }
   117  
   118  // FetchLiveStateForCreateAndUpdate is the same as FetchLiveState except for added special
   119  // handling for certain types of resources during resource creation and update.
   120  func FetchLiveStateForCreateAndUpdate(ctx context.Context, resource *Resource, provider *tfschema.Provider, kubeClient client.Client, smLoader *servicemappingloader.ServiceMappingLoader) (*terraform.InstanceState, error) {
   121  	// Special handling for resource which cannot be imported or read, has user-specified resource ID,
   122  	// and only contains top level fields that are immutable and/or computed.
   123  	// For such resource, its fetched live state will always be identical to the user config,
   124  	// regardless of the existence of the underlying GCP resource.
   125  	// We need to set the live state to empty so that the controller can retrieve the
   126  	// computed values via an explicit TF Apply() call.
   127  	//
   128  	// For example, ServiceIdentity is an unreadable resource with the user-specified
   129  	// resource ID, and all of its `spec` fields are immutable. An empty InstanceState
   130  	// ensures there can be a diff during the first reconciliation, so that TF
   131  	// controller can retrieve the computed value of the `status.email` field (the
   132  	// service identity) via the response of a TF Apply().
   133  	if resource.Unreadable() &&
   134  		resource.ResourceConfig.SkipImport &&
   135  		!resource.HasServerGeneratedIDField() &&
   136  		resource.AllTopLevelFieldsAreImmutableOrComputed() {
   137  		return &terraform.InstanceState{}, nil
   138  	}
   139  
   140  	return FetchLiveState(ctx, resource, provider, kubeClient, smLoader)
   141  }
   142  
   143  // ImportState parses the given id into a TF state. Note that this function
   144  // does not make any network calls; it simply does a best effort to determine
   145  // TF state by parsing the id.
   146  //
   147  // As a result of this being best-effort, the returned state may not have
   148  // every field required in a fully valid InstanceState.
   149  func ImportState(ctx context.Context, id string, tfInfo *terraform.InstanceInfo, provider *tfschema.Provider) (*terraform.InstanceState, error) {
   150  	importedResources, err := provider.ImportState(ctx, tfInfo, id)
   151  	if err != nil {
   152  		return nil, fmt.Errorf("error importing resource: %v", err)
   153  	}
   154  	if len(importedResources) != 1 {
   155  		return nil, fmt.Errorf("import corresponds to more than one resource")
   156  	}
   157  	return importedResources[0], nil
   158  }
   159  
   160  func presetFieldsForRead(r *Resource, imported *terraform.InstanceState, kubeClient client.Client, smLoader *servicemappingloader.ServiceMappingLoader) (*terraform.InstanceState, error) {
   161  	importedMap := InstanceStateToMap(r.TFResource, imported)
   162  	ret, err := WithFieldsPresetForRead(importedMap, r, kubeClient, smLoader)
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  	return MapToInstanceState(r.TFResource, ret), nil
   167  }
   168  
   169  func WithFieldsPresetForRead(imported map[string]interface{}, r *Resource, kubeClient client.Client, smLoader *servicemappingloader.ServiceMappingLoader) (map[string]interface{}, error) {
   170  	var config *terraform.ResourceConfig
   171  	var secretVersions map[string]string
   172  	var err error
   173  	// As we are directly calling the `krmResourceToTFResourceConfig`
   174  	// helper method instead of using the exported wrapping functions,
   175  	// define variables for all the arguments to improve readability.
   176  	mustResolveSensitiveFields := !k8s.IsDeleted(&r.ObjectMeta)
   177  	importedAsInstanceState := MapToInstanceState(r.TFResource, imported)
   178  	var jsonSchema *apiextensions.JSONSchemaProps = nil
   179  	config, secretVersions, err = KRMResourceToTFResourceConfigFull(
   180  		r, kubeClient, smLoader, importedAsInstanceState, jsonSchema, mustResolveSensitiveFields, label.GetDefaultLabels(),
   181  	)
   182  	if err != nil {
   183  		return nil, fmt.Errorf("error converting resource config: %w", err)
   184  	}
   185  
   186  	ret := withImmutableFields(imported, ResourceConfigToMap(config), r.TFResource.Schema)
   187  	ret, err = withMutableButUnreadableFields(ret, r, secretVersions, kubeClient)
   188  	if err != nil {
   189  		return nil, fmt.Errorf("error presetting mutable but unreadable fields for read: %v", err)
   190  	}
   191  	ret = withDirectives(ret, r)
   192  	ret, err = withStatusFields(ret, r, kubeClient, smLoader)
   193  	if err != nil {
   194  		return nil, fmt.Errorf("error presetting status fields for read: %v", err)
   195  	}
   196  	return ret, nil
   197  }
   198  
   199  func withImmutableFields(imported, config map[string]interface{}, schemas map[string]*tfschema.Schema) map[string]interface{} {
   200  	ret := deepcopy.MapStringInterface(imported)
   201  	if ret == nil {
   202  		ret = make(map[string]interface{})
   203  	}
   204  	for field, schema := range schemas {
   205  		configVal := config[field]
   206  		if schema.ForceNew {
   207  			if configVal == nil {
   208  				// If no value is specified by the user, prefill with the zero value.
   209  				// This happens due to pruning of default zero values returned from
   210  				// the read.
   211  				ret[field] = getZeroValueForType(schema.Type)
   212  			} else {
   213  				ret[field] = configVal
   214  			}
   215  			continue
   216  		}
   217  		if configVal == nil {
   218  			continue
   219  		}
   220  		// The current field is mutable, but may be a list of objects containing a field that
   221  		// is immutable.
   222  		switch schema.Type {
   223  		case tfschema.TypeList, tfschema.TypeSet:
   224  			switch elem := schema.Elem.(type) {
   225  			case *tfschema.Resource:
   226  				// Note: we assume that indexes in the list are preserved between the imported structure
   227  				// and the expanded config structure.
   228  				configList := configVal.([]interface{})
   229  				importedList, _ := imported[field].([]interface{})
   230  				retList := make([]interface{}, 0)
   231  				for idx, expandedItem := range configList {
   232  					expandedItem := expandedItem.(map[string]interface{})
   233  					var importedItem map[string]interface{}
   234  					if len(importedList) > idx {
   235  						importedItem = importedList[idx].(map[string]interface{})
   236  					}
   237  					retList = append(retList, withImmutableFields(importedItem, expandedItem, elem.Schema))
   238  				}
   239  				ret[field] = retList
   240  			}
   241  		}
   242  	}
   243  	return ret
   244  }
   245  
   246  func withMutableButUnreadableFields(imported map[string]interface{}, r *Resource, currSecretVersions map[string]string, kubeClient client.Client) (map[string]interface{}, error) {
   247  	if len(r.ResourceConfig.MutableButUnreadableFields) == 0 {
   248  		return imported, nil
   249  	}
   250  
   251  	mutableButUnreadableFields, err := getMutableButUnreadableFieldsFromAnnotations(r)
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  	if len(mutableButUnreadableFields) == 0 {
   256  		return imported, nil
   257  	}
   258  
   259  	secretVersions, err := k8s.GetSecretVersionsFromAnnotations(&r.Resource)
   260  	if err != nil {
   261  		return nil, err
   262  	}
   263  	// When secretVersions are not found in the annotations, there is a possibility
   264  	// that this is either (1) a resource acquisition or (2) a resource created
   265  	// before it supported the annotation. To avoid unnecessarily updating the
   266  	// resource in both cases, use the current Secret versions.
   267  	if secretVersions == nil {
   268  		secretVersions = currSecretVersions
   269  	}
   270  
   271  	return setMutableButUnreadableFields(imported, mutableButUnreadableFields["spec"].(map[string]interface{}), r.TFResource.Schema, secretVersions, r.GetNamespace(), kubeClient)
   272  }
   273  
   274  func setMutableButUnreadableFields(imported, mutableButUnreadableSpec map[string]interface{}, schemas map[string]*tfschema.Schema, secretVersions map[string]string, namespace string, kubeClient client.Client) (map[string]interface{}, error) {
   275  	ret := deepcopy.MapStringInterface(imported)
   276  	for k, v := range mutableButUnreadableSpec {
   277  		tfKey := text.AsSnakeCase(k)
   278  		schema, ok := schemas[tfKey]
   279  		if !ok {
   280  			return nil, fmt.Errorf("could not find a schema for field %v", tfKey)
   281  		}
   282  		switch schema.Type {
   283  		case tfschema.TypeString:
   284  			if !schema.Sensitive {
   285  				ret[tfKey] = v
   286  				continue
   287  			}
   288  
   289  			sensitiveField := corekccv1alpha1.SensitiveField{}
   290  			if err := util.Marshal(v, &sensitiveField); err != nil {
   291  				return nil, fmt.Errorf("error parsing %v onto a SensitiveField struct: %v", v, err)
   292  			}
   293  
   294  			if sensitiveField.Value != nil {
   295  				ret[tfKey] = *sensitiveField.Value
   296  				continue
   297  			}
   298  
   299  			secretKeyRef := sensitiveField.ValueFrom.SecretKeyRef
   300  			secretVal, secretVer, err := k8s.GetSecretVal(secretKeyRef, namespace, kubeClient)
   301  			if err != nil {
   302  				// If a previously referenced Secret cannot be resolved, it is
   303  				// possible it simply no longer exists. Don't error out in this
   304  				// case; just skip the presetting of the field in the live
   305  				// state so that a diff is generated if the field had been
   306  				// updated in the spec. If the field still points to the same
   307  				// Secret in the spec, then the KRM2TF conversion will
   308  				// appropriately error out due to the non-existent Secret.
   309  				continue
   310  			}
   311  			// Preset sensitive field only if we can be sure that the
   312  			// referenced Secret had not been changed.
   313  			prevSecretVer, ok := secretVersions[secretKeyRef.Name]
   314  			if ok && secretVer == prevSecretVer {
   315  				ret[tfKey] = secretVal
   316  			}
   317  		case tfschema.TypeBool, tfschema.TypeFloat, tfschema.TypeInt:
   318  			ret[tfKey] = v
   319  		case tfschema.TypeMap:
   320  			ret[tfKey] = deepcopy.DeepCopy(v)
   321  		case tfschema.TypeList, tfschema.TypeSet:
   322  			switch elem := schema.Elem.(type) {
   323  			// List/set of primitives
   324  			case *tfschema.Schema:
   325  				ret[tfKey] = deepcopy.DeepCopy(v)
   326  			// List/set of objects OR nested object
   327  			case *tfschema.Resource:
   328  				// List/set of objects
   329  				if schema.MaxItems != 1 {
   330  					panic(fmt.Errorf("error presetting field %v: presetting mutable-but-unreadable fields in objects contained in lists/sets is not yet supported", tfKey))
   331  				}
   332  				// Nested object
   333  				prevObj, ok := v.(map[string]interface{}) // Nested objects are represented as maps in KRM
   334  				if !ok {
   335  					return nil, fmt.Errorf("expected field %v in %v to be a map, but it is not", k, k8s.MutableButUnreadableFieldsAnnotation)
   336  				}
   337  				importedObj, err := getObjectAtFieldInState(imported, tfKey)
   338  				if err != nil {
   339  					return nil, fmt.Errorf("error getting object at field %v from state map: %v", tfKey, err)
   340  				}
   341  				obj, err := setMutableButUnreadableFields(importedObj, prevObj, elem.Schema, secretVersions, namespace, kubeClient)
   342  				if err != nil {
   343  					return nil, err
   344  				}
   345  				if len(obj) == 0 {
   346  					continue
   347  				}
   348  				ret[tfKey] = []interface{}{obj} // Nested objects are represented as lists with one item in TF
   349  			}
   350  		}
   351  	}
   352  	return ret, nil
   353  }
   354  
   355  // getObjectAtFieldInState gets the object at field 'tfKey' in the TF state map 'state'
   356  func getObjectAtFieldInState(state map[string]interface{}, tfKey string) (map[string]interface{}, error) {
   357  	v, ok := getNestedFieldFromState(state, tfKey)
   358  	if !ok {
   359  		return make(map[string]interface{}), nil
   360  	}
   361  	obj, ok := v.(map[string]interface{})
   362  	if !ok {
   363  		return nil, fmt.Errorf("expected field %v to be a nested object, but it is not", tfKey)
   364  	}
   365  	return obj, nil
   366  }
   367  
   368  func withDirectives(imported map[string]interface{}, r *Resource) map[string]interface{} {
   369  	ret := deepcopy.MapStringInterface(imported)
   370  	for _, d := range r.ResourceConfig.Directives {
   371  		key := k8s.FormatAnnotation(text.SnakeCaseToKebabCase(d))
   372  		if v, ok := k8s.GetAnnotation(key, r); ok {
   373  			ret[d] = v
   374  		} else {
   375  			if r.TFResource.Schema[d].Default != nil {
   376  				ret[d] = r.TFResource.Schema[d].Default
   377  			}
   378  		}
   379  	}
   380  	return ret
   381  }
   382  
   383  func withStatusFields(imported map[string]interface{}, r *Resource, kubeClient client.Client, smLoader *servicemappingloader.ServiceMappingLoader) (map[string]interface{}, error) {
   384  	ret := deepcopy.MapStringInterface(imported)
   385  	tfStatus, err := KRMObjectToTFObject(r.Status, r.TFResource)
   386  	if err != nil {
   387  		return nil, fmt.Errorf("error converting status object: %v", err)
   388  	}
   389  	for k, v := range tfStatus {
   390  		ret[k] = v
   391  	}
   392  
   393  	if SupportsResourceIDField(&r.ResourceConfig) && IsResourceIDFieldServerGenerated(&r.ResourceConfig) {
   394  		idInStatus, err := r.ConstructServerGeneratedIDInStatusFromResourceID(kubeClient, smLoader)
   395  		if err != nil {
   396  			return nil, fmt.Errorf("error syncing the server-generated ID: %v", err)
   397  		}
   398  		if idInStatus != "" {
   399  			ret[r.ResourceConfig.ServerGeneratedIDField] = idInStatus
   400  		}
   401  	}
   402  
   403  	return ret, nil
   404  }
   405  
   406  func getZeroValueForType(valueType tfschema.ValueType) interface{} {
   407  	switch valueType {
   408  	case tfschema.TypeBool:
   409  		return false
   410  	case tfschema.TypeFloat, tfschema.TypeInt:
   411  		return float64(0)
   412  	case tfschema.TypeString:
   413  		return ""
   414  	case tfschema.TypeList, tfschema.TypeMap, tfschema.TypeSet:
   415  		return nil
   416  	default:
   417  		panic(fmt.Sprintf("unknown value type %v", valueType))
   418  	}
   419  }
   420  

View as plain text