...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/krmtotf/krmtotf.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/deepcopy"
    23  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/gcp"
    24  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    25  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/label"
    26  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/servicemapping/servicemappingloader"
    27  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/text"
    28  	tfresource "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/tf/resource"
    29  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util"
    30  
    31  	tfschema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    32  	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
    33  	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    34  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    35  	"sigs.k8s.io/controller-runtime/pkg/client"
    36  )
    37  
    38  // KRMResourceToTFResourceConfig converts a KCC KRM resource to a Terraform
    39  // resource config. Note: this function does not fully validate the input KRM
    40  // config or output TF config to ensure that they correspond to valid GCP
    41  // resources (e.g. if the input KRM config is missing a required field, the
    42  // function won't complain and just output a TF config without that field).
    43  // This function just converts one abstract data structure to another;
    44  // validation of either the input KRM or output TF is left as the
    45  // responsibility of other layers (e.g. webhooks, CRD schemas, GCP API, etc.)
    46  func KRMResourceToTFResourceConfig(r *Resource, c client.Client, smLoader *servicemappingloader.ServiceMappingLoader) (tfConfig *terraform.ResourceConfig, secretVersions map[string]string, err error) {
    47  	return KRMResourceToTFResourceConfigFull(r, c, smLoader, nil, nil, true, label.GetDefaultLabels())
    48  }
    49  
    50  // KRMResourceToTFResourceConfigFull is a more flexible version of KRMResourceToTFResourceConfig,
    51  // including the following additional flags:
    52  //   - liveState: if set, these values will be used as the default values of the returned tfConfig, subject to
    53  //     be overriden by r.spec, etc.
    54  //   - jsonSchema: if set, externally managed fields will be populated.
    55  //   - mustResolveSensitiveFields: if set, sensitive fields will be resolved.
    56  //   - defaultLabels: if set, these labels will be added to tfConfig.
    57  func KRMResourceToTFResourceConfigFull(r *Resource, c client.Client, smLoader *servicemappingloader.ServiceMappingLoader,
    58  	liveState *terraform.InstanceState, jsonSchema *apiextensions.JSONSchemaProps, mustResolveSensitiveFields bool, defaultLabels map[string]string) (tfConfig *terraform.ResourceConfig, secretVersions map[string]string, err error) {
    59  	config := deepcopy.MapStringInterface(r.Spec)
    60  	if config == nil {
    61  		config = make(map[string]interface{})
    62  	}
    63  	if jsonSchema != nil {
    64  		if err := ResolveLegacyGCPManagedFields(r, liveState, config); err != nil {
    65  			return nil, nil, fmt.Errorf("error resolving legacy GCP-managed fields: %w", err)
    66  		}
    67  		config, err = resolveUnmanagedFields(config, r, liveState, jsonSchema)
    68  		if err != nil {
    69  			return nil, nil, fmt.Errorf("error resolving externally-managed fields: %w", err)
    70  		}
    71  	}
    72  	if err := handleUserSpecifiedID(config, r, smLoader, c); err != nil {
    73  		return nil, nil, err
    74  	}
    75  	if r.ResourceConfig.MetadataMapping.Labels != "" {
    76  		path := text.SnakeCaseToLowerCamelCase(r.ResourceConfig.MetadataMapping.Labels)
    77  		labels := label.NewGCPLabelsFromK8SLabels(r.GetLabels(), defaultLabels)
    78  		if err := setValue(config, path, labels); err != nil {
    79  			return nil, nil, fmt.Errorf("error mapping 'metadata.labels': %v", err)
    80  		}
    81  	}
    82  	if r.ResourceConfig.Locationality != "" {
    83  		switch r.ResourceConfig.Locationality {
    84  		case gcp.Global:
    85  			delete(config, "location")
    86  		case gcp.Regional:
    87  			config["region"] = config["location"]
    88  			delete(config, "location")
    89  		case gcp.Zonal:
    90  			config["zone"] = config["location"]
    91  			delete(config, "location")
    92  		default:
    93  			return nil, nil, fmt.Errorf("INTERNAL_ERROR: %v locationality is not supported", r.ResourceConfig.Locationality)
    94  		}
    95  	}
    96  	for _, refConfig := range r.ResourceConfig.ResourceReferences {
    97  		if err := handleResourceReference(config, refConfig, r, c, smLoader); err != nil {
    98  			return nil, nil, err
    99  		}
   100  	}
   101  	config, secretVersions, err = resolveSensitiveFields(config, r.TFResource, r.GetNamespace(), c, mustResolveSensitiveFields)
   102  	if err != nil {
   103  		return nil, nil, err
   104  	}
   105  	config, err = KRMObjectToTFObjectWithConfigurableFieldsOnly(config, r.TFResource)
   106  	if err != nil {
   107  		return nil, nil, fmt.Errorf("error converting to config: %v", err)
   108  	}
   109  	for _, d := range r.ResourceConfig.Directives {
   110  		key := k8s.FormatAnnotation(text.SnakeCaseToKebabCase(d))
   111  		if val, ok := k8s.GetAnnotation(key, r); ok {
   112  			if val == "" {
   113  				return nil, nil, fmt.Errorf("the value for directive '%v' must not be empty", key)
   114  			}
   115  			if err := setValue(config, d, val); err != nil {
   116  				return nil, nil, fmt.Errorf("error mapping directive '%v': %v", d, err)
   117  			}
   118  		}
   119  	}
   120  	if err := resolveContainerValue(config, r, c, smLoader); err != nil {
   121  		return nil, nil, fmt.Errorf("error resolving container value: %v", err)
   122  	}
   123  	config, err = withCustomFlatteners(config, r.Kind)
   124  	if err != nil {
   125  		return nil, nil, fmt.Errorf("error running custom flatteners: %w", err)
   126  	}
   127  	state := InstanceStateToMap(r.TFResource, liveState)
   128  	config, err = withResourceCustomResolvers(config, state, r.Kind, r.TFResource)
   129  	if err != nil {
   130  		return nil, nil, fmt.Errorf("error running resource custom resolver: %w", err)
   131  	}
   132  	return MapToResourceConfig(r.TFResource, config), secretVersions, nil
   133  }
   134  
   135  func KRMObjectToTFObject(obj map[string]interface{}, resource *tfschema.Resource) (map[string]interface{}, error) {
   136  	return krmObjectToTFObject(obj, resource, false)
   137  }
   138  
   139  func KRMObjectToTFObjectWithConfigurableFieldsOnly(obj map[string]interface{}, resource *tfschema.Resource) (map[string]interface{}, error) {
   140  	return krmObjectToTFObject(obj, resource, true)
   141  }
   142  
   143  func krmObjectToTFObject(obj map[string]interface{}, resource *tfschema.Resource, includeConfigurableFieldsOnly bool) (map[string]interface{}, error) {
   144  	var err error
   145  	if obj == nil {
   146  		return nil, nil
   147  	}
   148  	ret := make(map[string]interface{})
   149  	for k, v := range obj {
   150  		tfKey := text.AsSnakeCase(k)
   151  		schema, ok := resource.Schema[tfKey]
   152  		if !ok {
   153  			// TODO(b/239223470): We want to error out explicity if certain field from spec
   154  			// cannot be mapped to TFObject, instead of silently swallow the error.
   155  			continue
   156  		}
   157  		if includeConfigurableFieldsOnly && !tfresource.IsConfigurableField(schema) {
   158  			continue
   159  		}
   160  		ret[tfKey], err = convertToTF(v, schema, includeConfigurableFieldsOnly)
   161  		if err != nil {
   162  			return nil, fmt.Errorf("error converting '%v': %v", k, err)
   163  		}
   164  	}
   165  	return ret, nil
   166  }
   167  
   168  func convertToTF(obj interface{}, schema *tfschema.Schema, includeConfigurableFieldsOnly bool) (interface{}, error) {
   169  	switch schema.Type {
   170  	case tfschema.TypeBool, tfschema.TypeFloat, tfschema.TypeString, tfschema.TypeInt:
   171  		// Treat these values as primitives
   172  		return obj, nil
   173  	case tfschema.TypeMap:
   174  		// Maps are kept identical to the input
   175  		return deepcopy.DeepCopy(obj), nil
   176  	case tfschema.TypeList, tfschema.TypeSet:
   177  		items, err := toList(obj, schema)
   178  		if err != nil {
   179  			return nil, err
   180  		}
   181  		retList := make([]interface{}, 0)
   182  		for _, item := range items {
   183  			var processedItem interface{}
   184  			switch elem := schema.Elem.(type) {
   185  			case *tfschema.Schema:
   186  				processedItem, err = convertToTF(item, elem, includeConfigurableFieldsOnly)
   187  				if err != nil {
   188  					return nil, fmt.Errorf("error converting list item: %v", err)
   189  				}
   190  			case *tfschema.Resource:
   191  				itemAsMap, ok := item.(map[string]interface{})
   192  				if !ok {
   193  					return nil, fmt.Errorf("expected list item to be map but was not")
   194  				}
   195  				processedItem, err = krmObjectToTFObject(itemAsMap, elem, includeConfigurableFieldsOnly)
   196  				if err != nil {
   197  					return nil, fmt.Errorf("error converting map list item: %v", err)
   198  				}
   199  			default:
   200  				return nil, fmt.Errorf("unknown elem type")
   201  			}
   202  			retList = append(retList, processedItem)
   203  		}
   204  		return retList, nil
   205  	case tfschema.TypeInvalid:
   206  		return nil, fmt.Errorf("schema type is invalid")
   207  	default:
   208  		return nil, fmt.Errorf("unrecognized schema type %v", schema.Type)
   209  	}
   210  }
   211  
   212  // handleUserSpecifiedID takes the resource's user-specified ID (if it supports
   213  // one and has one) and places it into the config object. If the resource
   214  // doesn't support user-specified IDs (e.g. supports server-generated IDs
   215  // instead), then this function is a no-op. If the resource does support
   216  // user-specified IDs, then this function tries to get it from the resource's
   217  // spec.resourceID first if specified, and then metadata.name if specified.
   218  func handleUserSpecifiedID(config map[string]interface{}, r *Resource, smLoader *servicemappingloader.ServiceMappingLoader, c client.Client) error {
   219  	if SupportsResourceIDField(&r.ResourceConfig) && !IsResourceIDFieldServerGenerated(&r.ResourceConfig) && r.HasResourceIDField() {
   220  		path := text.SnakeCaseToLowerCamelCase(r.ResourceConfig.ResourceID.TargetField)
   221  		resourceID, err := resolveResourceID(r, c, smLoader)
   222  		if err != nil {
   223  			return fmt.Errorf("error resolving resource ID: %v", err)
   224  		}
   225  		if err := setValue(config, path, resourceID); err != nil {
   226  			return fmt.Errorf("error mapping user-specified %v: %v", k8s.ResourceIDFieldPath, err)
   227  		}
   228  	} else if r.ResourceConfig.MetadataMapping.Name != "" && r.GetName() != "" {
   229  		path := text.SnakeCaseToLowerCamelCase(r.ResourceConfig.MetadataMapping.Name)
   230  		name, err := resolveNameMetadataMapping(r, c, smLoader)
   231  		if err != nil {
   232  			return fmt.Errorf("error resolving metadata.name mapping: %v", err)
   233  		}
   234  		if err := setValue(config, path, name); err != nil {
   235  			return fmt.Errorf("error mapping metadata.name: %v", err)
   236  		}
   237  	}
   238  	return nil
   239  }
   240  
   241  func resolveSensitiveFields(config map[string]interface{}, resource *tfschema.Resource, namespace string, c client.Client, mustResolveSensitiveFields bool) (resolvedConfig map[string]interface{}, secretVersions map[string]string, err error) {
   242  	resolvedConfig = deepcopy.MapStringInterface(config)
   243  	secretVersions = make(map[string]string)
   244  	for k, v := range config {
   245  		tfKey := text.AsSnakeCase(k)
   246  		schema, ok := resource.Schema[tfKey]
   247  		if !ok {
   248  			continue
   249  		}
   250  		switch schema.Type {
   251  		case tfschema.TypeString:
   252  			if !tfresource.IsSensitiveConfigurableField(schema) {
   253  				continue
   254  			}
   255  
   256  			field := corekccv1alpha1.SensitiveField{}
   257  			if err := util.Marshal(v, &field); err != nil {
   258  				return nil, nil, fmt.Errorf("error parsing %v onto a SensitiveField struct: %v", v, err)
   259  			}
   260  
   261  			if field.Value != nil {
   262  				resolvedConfig[k] = *field.Value
   263  				continue
   264  			}
   265  
   266  			secretKeyRef := field.ValueFrom.SecretKeyRef
   267  			secretVal, secretVer, err := k8s.GetSecretVal(secretKeyRef, namespace, c)
   268  			if err != nil {
   269  				if mustResolveSensitiveFields {
   270  					return nil, nil, err
   271  				}
   272  				delete(resolvedConfig, k)
   273  				continue
   274  			}
   275  			resolvedConfig[k] = secretVal
   276  			secretVersions[secretKeyRef.Name] = secretVer
   277  		default:
   278  			resolvedObj, secretVers, err := resolveSensitiveFieldsInObj(v, schema, namespace, c, mustResolveSensitiveFields)
   279  			if err != nil {
   280  				return nil, nil, err
   281  			}
   282  			resolvedConfig[k] = resolvedObj
   283  			secretVersions = addToMap(secretVersions, secretVers)
   284  		}
   285  	}
   286  	return resolvedConfig, secretVersions, nil
   287  }
   288  
   289  func resolveSensitiveFieldsInObj(obj interface{}, schema *tfschema.Schema, namespace string, c client.Client, mustResolveSensitiveFields bool) (resolvedObj interface{}, secretVersions map[string]string, err error) {
   290  	secretVersions = make(map[string]string)
   291  	switch schema.Type {
   292  	case tfschema.TypeList, tfschema.TypeSet:
   293  		items, err := toList(obj, schema)
   294  		if err != nil {
   295  			return nil, nil, err
   296  		}
   297  		resolvedItems := make([]interface{}, 0)
   298  		for _, item := range items {
   299  			var resolvedItem interface{}
   300  			var secretVers map[string]string
   301  			var err error
   302  
   303  			switch elem := schema.Elem.(type) {
   304  			case *tfschema.Schema:
   305  				resolvedItem, secretVers, err = resolveSensitiveFieldsInObj(item, elem, namespace, c, mustResolveSensitiveFields)
   306  				if err != nil {
   307  					return nil, nil, err
   308  				}
   309  			case *tfschema.Resource:
   310  				itemAsMap, ok := item.(map[string]interface{})
   311  				if !ok {
   312  					return nil, nil, fmt.Errorf("expected list item to be map but was not")
   313  				}
   314  				resolvedItem, secretVers, err = resolveSensitiveFields(itemAsMap, elem, namespace, c, mustResolveSensitiveFields)
   315  				if err != nil {
   316  					return nil, nil, err
   317  				}
   318  			}
   319  
   320  			resolvedItems = append(resolvedItems, resolvedItem)
   321  			secretVersions = addToMap(secretVersions, secretVers)
   322  		}
   323  		return resolvedItems, secretVersions, nil
   324  	default:
   325  		return obj, secretVersions, nil
   326  	}
   327  }
   328  
   329  func toList(obj interface{}, schema *tfschema.Schema) ([]interface{}, error) {
   330  	if obj == nil {
   331  		return nil, nil
   332  	}
   333  	switch obj := obj.(type) {
   334  	case []interface{}:
   335  		return obj, nil
   336  	case map[string]interface{}:
   337  		// An object nested in a KRM field can be interpreted as a list if the
   338  		// corresponding TF field is a list with MaxItems == 1. This is due to
   339  		// limitations with TF schemas.
   340  		if schema.MaxItems == 1 {
   341  			return []interface{}{obj}, nil
   342  		}
   343  		return nil, fmt.Errorf("cannot interpret map as list without maxItems == 1")
   344  	default:
   345  		return nil, fmt.Errorf("cannot interpret non-list %T as list", obj)
   346  	}
   347  }
   348  
   349  func setValue(m map[string]interface{}, path string, value interface{}) error {
   350  	return unstructured.SetNestedField(m, value, strings.Split(path, ".")...)
   351  }
   352  
   353  // addToMap adds all the key-value pairs from the 'right' map onto the 'left'
   354  // map. If the key already existed in the 'left' map, then it is overriden by
   355  // the value in the 'right' map.
   356  func addToMap(left map[string]string, right map[string]string) map[string]string {
   357  	left = deepcopy.StringStringMap(left)
   358  	for k, v := range right {
   359  		left[k] = v
   360  	}
   361  	return left
   362  }
   363  
   364  func resolveContainerValue(config map[string]interface{}, r *Resource, c client.Client, smLoader *servicemappingloader.ServiceMappingLoader) error {
   365  	if len(r.ResourceConfig.Containers) == 0 {
   366  		return nil
   367  	}
   368  	if SupportsHierarchicalReferences(&r.ResourceConfig) {
   369  		// If resource supports hierarchical references, use those references
   370  		// instead to set the parent fields in the underlying resource.
   371  		// TODO(b/193177782): Delete this function once all resources support
   372  		// hierarchical references.
   373  		return nil
   374  	}
   375  	for _, container := range r.ResourceConfig.Containers {
   376  		val, ok := k8s.GetAnnotation(k8s.GetAnnotationForContainerType(container.Type), r)
   377  		if !ok {
   378  			continue
   379  		}
   380  		val, err := ResolveValueTemplate(container.ValueTemplate, val, r, c, smLoader)
   381  		if err != nil {
   382  			return fmt.Errorf("error resolving templated value: %v", err)
   383  		}
   384  		if err := setValue(config, container.TFField, val); err != nil {
   385  			return fmt.Errorf("error setting container value: %v", err)
   386  		}
   387  		return nil
   388  	}
   389  	return fmt.Errorf("no annotation found that matches one of the required containers")
   390  }
   391  
   392  func resolveNameMetadataMapping(r *Resource, c client.Client, smLoader *servicemappingloader.ServiceMappingLoader) (string, error) {
   393  	name := r.GetName()
   394  	if name == "" {
   395  		return "", fmt.Errorf("invalid empty value for name")
   396  	}
   397  	return ResolveValueTemplate(r.ResourceConfig.MetadataMapping.NameValueTemplate, name, r, c, smLoader)
   398  }
   399  
   400  func resolveResourceID(r *Resource, c client.Client, smLoader *servicemappingloader.ServiceMappingLoader) (string, error) {
   401  	resourceID, err := r.GetResourceID()
   402  	if err != nil {
   403  		return "", err
   404  	}
   405  
   406  	return ResolveValueTemplate(r.ResourceConfig.ResourceID.ValueTemplate, resourceID, r, c, smLoader)
   407  }
   408  

View as plain text