...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/kcclite/conversion.go

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

     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 kcclite
    16  
    17  import (
    18  	"fmt"
    19  	"strings"
    20  
    21  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
    22  	corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
    23  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl"
    24  	dclextension "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/extension"
    25  	dclmetadata "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/metadata"
    26  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/schema/dclschemaloader"
    27  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/deepcopy"
    28  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    29  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/servicemapping/servicemappingloader"
    30  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util"
    31  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/pathslice"
    32  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/typeutil"
    33  
    34  	"github.com/nasa9084/go-openapi"
    35  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    36  	"sigs.k8s.io/controller-runtime/pkg/client"
    37  	"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
    38  )
    39  
    40  // ToKCCLite will convert the KRM representation to a state that all fields that can be used without using the API server.
    41  // More specifically speaking, it will
    42  // 1) resolve the resource reference and store the value in 'external' field
    43  // 2) resolve the secret reference and store the value in 'value' field
    44  func ToKCCLite(resource *dcl.Resource, smLoader dclmetadata.ServiceMetadataLoader,
    45  	schemaLoader dclschemaloader.DCLSchemaLoader, serviceMappingLoader *servicemappingloader.ServiceMappingLoader,
    46  	kubeClient client.Client) (*unstructured.Unstructured, error) {
    47  	kccLite, _, err := convertToKCCLite(resource, smLoader, schemaLoader, serviceMappingLoader, kubeClient, true)
    48  	return kccLite, err
    49  }
    50  
    51  func ToKCCLiteBestEffort(resource *dcl.Resource, smLoader dclmetadata.ServiceMetadataLoader,
    52  	schemaLoader dclschemaloader.DCLSchemaLoader, serviceMappingLoader *servicemappingloader.ServiceMappingLoader,
    53  	kubeClient client.Client) (*unstructured.Unstructured, error) {
    54  	kccLite, _, err := convertToKCCLite(resource, smLoader, schemaLoader, serviceMappingLoader, kubeClient, false)
    55  	return kccLite, err
    56  }
    57  
    58  func ToKCCLiteAndSecretVersions(resource *dcl.Resource, smLoader dclmetadata.ServiceMetadataLoader,
    59  	schemaLoader dclschemaloader.DCLSchemaLoader, serviceMappingLoader *servicemappingloader.ServiceMappingLoader,
    60  	kubeClient client.Client) (kccLite *unstructured.Unstructured, secretVersions map[string]string, err error) {
    61  	return convertToKCCLite(resource, smLoader, schemaLoader, serviceMappingLoader, kubeClient, true)
    62  }
    63  
    64  func convertToKCCLite(resource *dcl.Resource, smLoader dclmetadata.ServiceMetadataLoader,
    65  	schemaLoader dclschemaloader.DCLSchemaLoader, serviceMappingLoader *servicemappingloader.ServiceMappingLoader,
    66  	kubeClient client.Client, mustResolveAllFields bool) (kccLite *unstructured.Unstructured, secretVersions map[string]string, err error) {
    67  	lite, err := resource.MarshalAsUnstructured()
    68  	if err != nil {
    69  		return nil, nil, err
    70  	}
    71  	config, found, err := unstructured.NestedFieldNoCopy(lite.Object, "spec")
    72  	if err != nil {
    73  		return nil, nil, err
    74  	}
    75  	if !found || config == nil {
    76  		return lite, nil, nil
    77  	}
    78  
    79  	secretVersions = make(map[string]string)
    80  	convertedSpec, err := convertConfig(config.(map[string]interface{}), []string{}, resource.Schema, smLoader, schemaLoader, serviceMappingLoader, resource.GetNamespace(), kubeClient, mustResolveAllFields, secretVersions)
    81  
    82  	if err != nil {
    83  		return nil, nil, err
    84  	}
    85  	if err := unstructured.SetNestedMap(lite.Object, convertedSpec, "spec"); err != nil {
    86  		return nil, nil, err
    87  	}
    88  	return lite, secretVersions, nil
    89  }
    90  
    91  func convertConfig(config map[string]interface{}, path []string, schema *openapi.Schema, smLoader dclmetadata.ServiceMetadataLoader, schemaLoader dclschemaloader.DCLSchemaLoader, serviceMappingLoader *servicemappingloader.ServiceMappingLoader, namespace string, kubeClient client.Client, mustResolveAllFields bool, secretVersions map[string]string) (map[string]interface{}, error) {
    92  	if len(config) == 0 {
    93  		return config, nil
    94  	}
    95  	if schema.Type != "object" {
    96  		return nil, fmt.Errorf("expect the schame type to be 'object', but got %v", schema.Type)
    97  	}
    98  	for f, s := range schema.Properties {
    99  		if dclextension.IsReferenceField(s) {
   100  			if err := handleReferenceField(append(path, f), config, s, smLoader, schemaLoader, serviceMappingLoader, kubeClient, namespace, mustResolveAllFields); err != nil {
   101  				return nil, fmt.Errorf("error resolving reference field %v: %w", f, err)
   102  			}
   103  			continue
   104  		}
   105  		if config[f] != nil {
   106  			convertedVal, err := convertVal(config[f], append(path, f), s, smLoader, schemaLoader, serviceMappingLoader, namespace, kubeClient, mustResolveAllFields, secretVersions)
   107  			if err != nil {
   108  				return nil, err
   109  			}
   110  			delete(config, f)
   111  			// It's possible that convertVal() returns nil value (e.g. when a Secret
   112  			// is not found) when mustResolveAllFields is false. We should ignore
   113  			// unresolved field.
   114  			if convertedVal != nil {
   115  				dcl.AddToMap(f, convertedVal, config)
   116  			}
   117  		}
   118  	}
   119  	return config, nil
   120  }
   121  
   122  func convertVal(val interface{}, path []string, schema *openapi.Schema, smLoader dclmetadata.ServiceMetadataLoader, schemaLoader dclschemaloader.DCLSchemaLoader, serviceMappingLoader *servicemappingloader.ServiceMappingLoader, namespace string, kubeClient client.Client, mustResolveAllFields bool, secretVersions map[string]string) (interface{}, error) {
   123  	switch schema.Type {
   124  	case "object":
   125  		obj, ok := val.(map[string]interface{})
   126  		if !ok {
   127  			return nil, fmt.Errorf("expected the value to be map[string]interface{} but was actually %T", val)
   128  		}
   129  		if schema.AdditionalProperties != nil {
   130  			if typeutil.IsPrimitiveType(schema.AdditionalProperties.Type) {
   131  				return val, nil
   132  			}
   133  			if schema.AdditionalProperties.Type == "object" {
   134  				res := make(map[string]interface{})
   135  				for k, v := range obj {
   136  					convertedVal, err := convertVal(v, append(path, k), schema.AdditionalProperties, smLoader, schemaLoader, serviceMappingLoader, namespace, kubeClient, mustResolveAllFields, secretVersions)
   137  					if err != nil {
   138  						return nil, fmt.Errorf("error converting the object value for key %v: %w", k, err)
   139  					}
   140  					res[k] = convertedVal
   141  				}
   142  				return res, nil
   143  			}
   144  			return nil, fmt.Errorf("not supported type for AdditionalProperties %v", schema.AdditionalProperties.Type)
   145  		}
   146  		return convertConfig(obj, path, schema, smLoader, schemaLoader, serviceMappingLoader, namespace, kubeClient, mustResolveAllFields, secretVersions)
   147  	case "array":
   148  		if typeutil.IsPrimitiveType(schema.Items.Type) {
   149  			return val, nil
   150  		}
   151  		items, ok := val.([]interface{})
   152  		if !ok {
   153  			return nil, fmt.Errorf("expected the value to be []interface{} but was actually %T", val)
   154  		}
   155  		res := make([]interface{}, 0)
   156  		for _, item := range items {
   157  			processedItem, err := convertVal(item, path, schema.Items, smLoader, schemaLoader, serviceMappingLoader, namespace, kubeClient, mustResolveAllFields, secretVersions)
   158  			if err != nil {
   159  				return nil, fmt.Errorf("error converting list item: %w", err)
   160  			}
   161  			res = append(res, processedItem)
   162  		}
   163  		return res, nil
   164  	case "string":
   165  		if ok, _ := dclextension.IsSensitiveField(schema); ok {
   166  			field := corekccv1alpha1.SensitiveField{}
   167  			if err := util.Marshal(val, &field); err != nil {
   168  				return nil, fmt.Errorf("error parsing %v onto a SensitiveField struct: %w", val, err)
   169  			}
   170  
   171  			if field.Value != nil {
   172  				return map[string]interface{}{"value": *field.Value}, nil
   173  			}
   174  
   175  			secretKeyRef := field.ValueFrom.SecretKeyRef
   176  			secretVal, secretVer, err := k8s.GetSecretVal(secretKeyRef, namespace, kubeClient)
   177  			if err != nil {
   178  				if mustResolveAllFields {
   179  					return nil, err
   180  				}
   181  
   182  				// If the secret can't be found but it is not required to be resolved, then
   183  				// return nil secret value and nil error.
   184  				return nil, nil
   185  			}
   186  			secretVersions[secretKeyRef.Name] = secretVer
   187  			return map[string]interface{}{"value": secretVal}, nil
   188  		}
   189  		return val, nil
   190  	case "boolean", "number", "integer":
   191  		return val, nil
   192  	default:
   193  		return nil, fmt.Errorf("unknown schema type %v", schema.Type)
   194  	}
   195  }
   196  
   197  func handleReferenceField(path []string, config map[string]interface{}, schema *openapi.Schema, smLoader dclmetadata.ServiceMetadataLoader,
   198  	schemaLoader dclschemaloader.DCLSchemaLoader, serviceMappingLoader *servicemappingloader.ServiceMappingLoader,
   199  	kubeClient client.Client, namespace string, mustResolveAllFields bool) error {
   200  	if dcl.IsMultiTypeParentReferenceField(path) {
   201  		return handleMultiTypeParentReferenceField(config, schema, smLoader, schemaLoader, kubeClient, namespace, mustResolveAllFields)
   202  	}
   203  	if schema.Type == "array" {
   204  		return handleListOfReferencesField(path, config, schema, smLoader, schemaLoader, serviceMappingLoader, kubeClient, namespace, mustResolveAllFields)
   205  	}
   206  	return handleRegularReferenceField(path, config, schema, smLoader, schemaLoader, serviceMappingLoader, kubeClient, namespace, mustResolveAllFields)
   207  }
   208  
   209  func handleMultiTypeParentReferenceField(config map[string]interface{}, schema *openapi.Schema, smLoader dclmetadata.ServiceMetadataLoader,
   210  	schemaLoader dclschemaloader.DCLSchemaLoader, kubeClient client.Client, namespace string, mustResolveAllFields bool) error {
   211  	rawVal, tc, err := dcl.GetHierarchicalRefFromConfigForMultiParentResource(config, schema, smLoader)
   212  	if err != nil {
   213  		return fmt.Errorf("error getting hierarchical reference from config for multi-parent resource: %w", err)
   214  	}
   215  	if rawVal == nil {
   216  		return nil
   217  	}
   218  	refField := tc.Key
   219  	refObj, ok := rawVal.(map[string]interface{})
   220  	if !ok {
   221  		return fmt.Errorf("expected the value to be map[string]interface{} for reference field %v but was actually %T", refField, rawVal)
   222  	}
   223  	val, err := resolveHierarchicalReferenceForMultiParentResource(refObj, tc, namespace, kubeClient, smLoader, schemaLoader)
   224  	if err != nil {
   225  		if mustResolveAllFields {
   226  			return err
   227  		}
   228  		delete(config, refField)
   229  		return nil
   230  	}
   231  	delete(config, refField)
   232  	dcl.AddToMap(refField, val, config)
   233  	return nil
   234  }
   235  
   236  func handleListOfReferencesField(path []string, config map[string]interface{}, schema *openapi.Schema, smLoader dclmetadata.ServiceMetadataLoader,
   237  	schemaLoader dclschemaloader.DCLSchemaLoader, serviceMappingLoader *servicemappingloader.ServiceMappingLoader,
   238  	kubeClient client.Client, namespace string, mustResolveAllFields bool) error {
   239  	refField, err := dclextension.GetReferenceFieldName(path, schema)
   240  	if err != nil {
   241  		return fmt.Errorf("error getting the reference field name %w", err)
   242  	}
   243  	if config[refField] == nil {
   244  		return nil
   245  	}
   246  	rawVal := config[refField]
   247  	items, ok := rawVal.([]interface{})
   248  	if !ok {
   249  		return fmt.Errorf("expected the value to be []interface{} for reference field %v but was actually %T", refField, rawVal)
   250  	}
   251  	res := make([]interface{}, 0)
   252  	for _, item := range items {
   253  		refObj, ok := item.(map[string]interface{})
   254  		if !ok {
   255  			return fmt.Errorf("expected the value for item reference to be map[string]interface{}, but was actually %T", item)
   256  		}
   257  		refVal, err := resolveResourceReference(refObj, schema.Items, smLoader, schemaLoader, serviceMappingLoader, kubeClient, namespace)
   258  		if err != nil {
   259  			if mustResolveAllFields {
   260  				return err
   261  			}
   262  			continue
   263  		}
   264  		res = append(res, refVal)
   265  	}
   266  	delete(config, refField)
   267  	if len(res) != 0 {
   268  		config[refField] = res
   269  	}
   270  	return nil
   271  }
   272  
   273  func handleRegularReferenceField(path []string, config map[string]interface{}, schema *openapi.Schema, smLoader dclmetadata.ServiceMetadataLoader,
   274  	schemaLoader dclschemaloader.DCLSchemaLoader, serviceMappingLoader *servicemappingloader.ServiceMappingLoader,
   275  	kubeClient client.Client, namespace string, mustResolveAllFields bool) error {
   276  	refField, err := dclextension.GetReferenceFieldName(path, schema)
   277  	if err != nil {
   278  		return fmt.Errorf("error getting the reference field name %w", err)
   279  	}
   280  	if config[refField] == nil {
   281  		return nil
   282  	}
   283  	rawVal := config[refField]
   284  	refObj, ok := rawVal.(map[string]interface{})
   285  	if !ok {
   286  		return fmt.Errorf("expected the value to be map[string]interface{} for reference field %v but was actually %T", refField, rawVal)
   287  	}
   288  	val, err := resolveResourceReference(refObj, schema, smLoader, schemaLoader, serviceMappingLoader, kubeClient, namespace)
   289  	if err != nil {
   290  		if mustResolveAllFields {
   291  			return err
   292  		}
   293  		delete(config, refField)
   294  		return nil
   295  	}
   296  	delete(config, refField)
   297  	dcl.AddToMap(refField, val, config)
   298  	return nil
   299  }
   300  
   301  func resolveHierarchicalReferenceForMultiParentResource(resourceRefValRaw map[string]interface{}, tc *corekccv1alpha1.TypeConfig, ns string,
   302  	kubeClient client.Client, smLoader dclmetadata.ServiceMetadataLoader, schemaLoader dclschemaloader.DCLSchemaLoader) (map[string]interface{}, error) {
   303  	val, err := resolveReferenceObject(resourceRefValRaw, tc, ns, kubeClient)
   304  	if err != nil {
   305  		return nil, err
   306  	}
   307  	// Use the original resolved value of the reference object here. That is,
   308  	// don't do any canonicalization even if the target field is the referenced
   309  	// resource's name (i.e. what we do for other resource references in
   310  	// resolveResourceReference()). This is because multi-type parent reference
   311  	// fields in DCL expect a different format from the one provided by the
   312  	// referenced resource's x-dcl-id (e.g. "projects/{project_id}",
   313  	// "folders/{folder_id}", etc.). Since we handle the formatting of
   314  	// multi-type parent reference fields at the KCCLite->DCL layer (e.g.
   315  	// convert "project-id" to "projects/project-id"), let us just use the
   316  	// original resolved value of the reference object here at the KCC->KCCLite
   317  	// layer (e.g. "project-id").
   318  	return map[string]interface{}{
   319  		"external": val,
   320  	}, nil
   321  }
   322  
   323  func resolveResourceReference(resourceRefValRaw map[string]interface{}, schema *openapi.Schema, smLoader dclmetadata.ServiceMetadataLoader,
   324  	schemaLoader dclschemaloader.DCLSchemaLoader, serviceMappingLoader *servicemappingloader.ServiceMappingLoader,
   325  	kubeClient client.Client, ns string) (map[string]interface{}, error) {
   326  	tcs, err := dcl.GetReferenceTypeConfigs(schema, smLoader)
   327  	if err != nil {
   328  		return nil, err
   329  	}
   330  
   331  	// If the reference object was originally an external reference, don't do
   332  	// anything extra.
   333  	if _, ok := resourceRefValRaw["external"]; ok {
   334  		return resourceRefValRaw, nil
   335  	}
   336  
   337  	tc, refResource, err := getMultiTypeReferencedResource(resourceRefValRaw, tcs, ns, kubeClient)
   338  	if err != nil {
   339  		return nil, err
   340  	}
   341  	val, err := resolveTargetFieldValue(refResource, tc)
   342  	if err != nil {
   343  		return nil, fmt.Errorf("error resolving target field value for referenced resource %v with GroupVersionKind %v: %w",
   344  			k8s.GetNamespacedName(refResource), refResource.GroupVersionKind(), err)
   345  	}
   346  	// Canonicalize the resolved target field value if the target field value
   347  	// is the referenced resource's name.
   348  	if tc.TargetField == "name" {
   349  		s, err := dclschemaloader.GetDCLSchemaForGVK(tc.GVK, smLoader, schemaLoader)
   350  		if err != nil {
   351  			return nil, fmt.Errorf("error getting DCL schema for referenced GroupVersionKind %v: %w", tc.GVK, err)
   352  		}
   353  		template, err := dclextension.GetNameValueTemplate(s)
   354  		if err != nil {
   355  			return nil, fmt.Errorf("error getting name value template for referenced GroupVersionKind %v: %w", tc.GVK, err)
   356  		}
   357  		canonicalizedVal, err := CanonicalizeReferencedResourceName(val, template, refResource, smLoader, schemaLoader, serviceMappingLoader, kubeClient)
   358  		if err != nil {
   359  			return nil, fmt.Errorf("error canonicalizing name of referenced resource %v with GroupVersionKind %v: %w",
   360  				k8s.GetNamespacedName(refResource), refResource.GroupVersionKind(), err)
   361  		}
   362  		return map[string]interface{}{
   363  			"external": canonicalizedVal,
   364  		}, nil
   365  	}
   366  	return map[string]interface{}{
   367  		"external": val,
   368  	}, nil
   369  }
   370  
   371  func resolveReferenceObject(resourceRefValRaw map[string]interface{}, tc *corekccv1alpha1.TypeConfig,
   372  	ns string, kubeClient client.Client) (string, error) {
   373  	if rawVal, ok := resourceRefValRaw["external"]; ok {
   374  		val, ok := rawVal.(string)
   375  		if !ok {
   376  			return "", fmt.Errorf("expected the value of 'external' in the resource reference object to be string, but was actually %T", rawVal)
   377  		}
   378  		return val, nil
   379  	}
   380  	refResource, err := getReferencedResource(resourceRefValRaw, tc, ns, kubeClient)
   381  	if err != nil {
   382  		return "", err
   383  	}
   384  	val, err := resolveTargetFieldValue(refResource, tc)
   385  	if err != nil {
   386  		return "", fmt.Errorf("error resolving target field value for referenced resource %v with GroupVersionKind %v: %w",
   387  			k8s.GetNamespacedName(refResource), refResource.GroupVersionKind(), err)
   388  	}
   389  	return val, nil
   390  }
   391  
   392  func getMultiTypeReferencedResource(resourceRefValRaw map[string]interface{}, tcs []corekccv1alpha1.TypeConfig,
   393  	ns string, kubeClient client.Client) (*corekccv1alpha1.TypeConfig, *k8s.Resource, error) {
   394  
   395  	if len(tcs) == 0 {
   396  		return nil, nil, fmt.Errorf("error resolving resource reference, no resource type information found")
   397  	}
   398  
   399  	if rawVal, ok := resourceRefValRaw["kind"]; ok {
   400  		// "kind" is specified in rawVal
   401  		kind, ok := rawVal.(string)
   402  		if !ok {
   403  			return nil, nil, fmt.Errorf("expected the value of 'kind' in the resource reference object to be string, but was actually %T", rawVal)
   404  		}
   405  		if len(tcs) == 1 {
   406  			// "single-kind" resource ref should not have "kind" in rawVal
   407  			return nil, nil, fmt.Errorf("'kind' is found in the single-type resource reference")
   408  		} else {
   409  			// "multi-kind" resource ref looks for matching "kind" in tcs
   410  			for i := range tcs {
   411  				tc := &tcs[i]
   412  				if kind == tc.GVK.Kind {
   413  					refResource, err := getReferencedResource(resourceRefValRaw, tc, ns, kubeClient)
   414  					return tc, refResource, err
   415  				}
   416  			}
   417  			return nil, nil, fmt.Errorf("the value of 'kind': '%v' is not supported in the resource reference", kind)
   418  		}
   419  	} else {
   420  		// "kind" is not specified in rawVal
   421  		if len(tcs) == 1 {
   422  			// "single-kind" resource ref uses default kind in tcs[0]
   423  			tc := &tcs[0]
   424  			refResource, err := getReferencedResource(resourceRefValRaw, tc, ns, kubeClient)
   425  			return tc, refResource, err
   426  		} else {
   427  			// "multi-kind" resource ref requires "kind"
   428  			return nil, nil, fmt.Errorf("'kind' is missing in the multi-type resource reference")
   429  		}
   430  	}
   431  }
   432  
   433  func getReferencedResource(resourceRefValRaw map[string]interface{}, tc *corekccv1alpha1.TypeConfig,
   434  	ns string, kubeClient client.Client) (*k8s.Resource, error) {
   435  	resourceRef := &v1alpha1.ResourceReference{}
   436  	if err := util.Marshal(resourceRefValRaw, resourceRef); err != nil {
   437  		return nil, fmt.Errorf("error marshalling raw resource reference object to resource reference struct: %w", err)
   438  	}
   439  	refResource, err := k8s.GetReferencedResourceIfReady(resourceRef, tc.GVK, ns, kubeClient)
   440  	if err != nil {
   441  		return nil, err
   442  	}
   443  	return refResource, nil
   444  }
   445  
   446  // TODO(kcc-eng): consolidate this method with krmtotf.resolveTargetFieldValue when resourceID support is added
   447  func resolveTargetFieldValue(refResource *k8s.Resource, typeConfig *corekccv1alpha1.TypeConfig) (string, error) {
   448  	if typeConfig.TargetField == "name" {
   449  		val, ok, err := unstructured.NestedString(refResource.Spec, k8s.ResourceIDFieldName)
   450  		if err != nil {
   451  			return "", err
   452  		}
   453  		if !ok {
   454  			return "", fmt.Errorf("couldn't resolve the resource Id")
   455  		}
   456  		return val, nil
   457  	}
   458  	if val, exist, _ := unstructured.NestedString(refResource.Status, strings.Split(typeConfig.TargetField, ".")...); exist {
   459  		return val, nil
   460  	}
   461  	if val, exist, _ := unstructured.NestedString(refResource.Spec, strings.Split(typeConfig.TargetField, ".")...); exist {
   462  		return val, nil
   463  	}
   464  	return "", fmt.Errorf("couldn't resolve the value for target field %v from the referenced resource %v", typeConfig.TargetField, refResource.GetNamespacedName())
   465  }
   466  
   467  // ResolveSpecAndStatus returns the resolved spec and status in different formats
   468  // gated by the 'state-into-spec' annotation.
   469  //
   470  // If the annotation takes the 'merge' value, the function returns the spec as a mix of k8s user managed fields and defaulted state from APIs
   471  // and returns the status with the legacy format containing observed state for output-only fields only.
   472  //
   473  // If the annotation takes the 'absent' value, the function will delegate to resolveDesiredStateInSpecAndObservedStateInStatus() to resolve
   474  // the spec and the status.
   475  func ResolveSpecAndStatus(state *unstructured.Unstructured, resource *dcl.Resource,
   476  	smLoader dclmetadata.ServiceMetadataLoader) (spec map[string]interface{}, status map[string]interface{}, err error) {
   477  	val, found := k8s.GetAnnotation(k8s.StateIntoSpecAnnotation, resource)
   478  	if !found || val == k8s.StateMergeIntoSpec {
   479  		spec, status, err = resolveMixedSpecAndLegacyStatus(state, resource, smLoader)
   480  	} else {
   481  		spec, status, err = resolveDesiredStateInSpecAndObservedStateInStatus(state, resource, smLoader)
   482  	}
   483  	if err != nil {
   484  		return nil, nil, err
   485  	}
   486  
   487  	// marshal via JSON in order to ensure consistency with dcl.Resource
   488  	normalizedSpec := make(map[string]interface{})
   489  	normalizedStatus := make(map[string]interface{})
   490  	if err := util.Marshal(&spec, &normalizedSpec); err != nil {
   491  		return nil, nil, fmt.Errorf("error normalizing the spec: %w", err)
   492  	}
   493  	if err := util.Marshal(&status, &normalizedStatus); err != nil {
   494  		return nil, nil, fmt.Errorf("error normalizing the status: %w", err)
   495  	}
   496  	return normalizedSpec, normalizedStatus, nil
   497  }
   498  
   499  // resolveMixedSpecAndLegacyStatus returns spec as a mix of k8s user managed fields and defaulted state from APIs
   500  // and returns status with the legacy format containing observed state for output-only fields only.
   501  func resolveMixedSpecAndLegacyStatus(state *unstructured.Unstructured, resource *dcl.Resource,
   502  	smLoader dclmetadata.ServiceMetadataLoader) (spec map[string]interface{}, status map[string]interface{}, err error) {
   503  	status, found, err := unstructured.NestedMap(state.Object, "status")
   504  	if err != nil {
   505  		return nil, nil, fmt.Errorf("error getting status from the state: %w", err)
   506  	}
   507  	if !found {
   508  		status = make(map[string]interface{})
   509  	}
   510  	conditions, found, err := unstructured.NestedFieldCopy(resource.Status, "conditions")
   511  	if err != nil {
   512  		return nil, nil, fmt.Errorf("error resolving conditions from resource status: %w", err)
   513  	}
   514  	if found {
   515  		status["conditions"] = conditions
   516  	}
   517  	// preserve the observedGeneration value
   518  	g, found, err := unstructured.NestedFieldCopy(resource.Status, "observedGeneration")
   519  	if err != nil {
   520  		return nil, nil, fmt.Errorf("error resolving observedGeneration from resource status: %w", err)
   521  	}
   522  	if found {
   523  		status["observedGeneration"] = g
   524  	}
   525  
   526  	stateSpec, found, err := unstructured.NestedMap(state.Object, "spec")
   527  	if err != nil {
   528  		return nil, nil, fmt.Errorf("error getting spec from the state: %w", err)
   529  	}
   530  	if !found {
   531  		stateSpec = make(map[string]interface{})
   532  	}
   533  	mergedSpec, err := mergeSpecWithLiteState(stateSpec, resource.Spec, []string{}, resource.Schema, resource.ManagedFields, smLoader)
   534  	if err != nil {
   535  		return nil, nil, fmt.Errorf("error merging spec from the live state and the raw spec: %w", err)
   536  	}
   537  
   538  	if err := populateResourceIDFieldInSpec(state, resource, mergedSpec); err != nil {
   539  		return nil, nil, fmt.Errorf("error populating 'resourceID' field in spec: %w", err)
   540  	}
   541  
   542  	// return nil rather than empty maps to simplify the resource representation in etcd
   543  	if len(mergedSpec) == 0 {
   544  		mergedSpec = nil
   545  	}
   546  	if len(status) == 0 {
   547  		status = nil
   548  	}
   549  	return mergedSpec, status, nil
   550  }
   551  
   552  func populateResourceIDFieldInSpec(state *unstructured.Unstructured, resource *dcl.Resource, spec map[string]interface{}) error {
   553  	// preserve the resourceID if specified in spec
   554  	if val, ok := resource.Spec[k8s.ResourceIDFieldName]; ok {
   555  		spec[k8s.ResourceIDFieldName] = val
   556  		return nil
   557  	}
   558  	// If it's the case that the resource with server-generated id is initially created,
   559  	// read the resource ID from state and store it
   560  	val, found, err := unstructured.NestedString(state.Object, "spec", k8s.ResourceIDFieldName)
   561  	if err != nil {
   562  		return err
   563  	}
   564  	if found {
   565  		spec[k8s.ResourceIDFieldName] = val
   566  	}
   567  	return nil
   568  }
   569  
   570  // resolveSpecAndObservedStateInStatus resolves spec as desired state and persists observed state in status.
   571  // TODO(b/193928224): persist the full observed state including both configurable fields and output-only fields in status.
   572  func resolveDesiredStateInSpecAndObservedStateInStatus(state *unstructured.Unstructured, resource *dcl.Resource,
   573  	smLoader dclmetadata.ServiceMetadataLoader) (
   574  	spec map[string]interface{}, status map[string]interface{}, err error) {
   575  	if resource.Spec != nil {
   576  		spec = deepcopy.MapStringInterface(resource.Spec)
   577  	}
   578  	if err := populateResourceIDFieldInSpec(state, resource, spec); err != nil {
   579  		return nil, nil, fmt.Errorf("error populating 'resourceID' field in spec: %w", err)
   580  	}
   581  	_, status, err = resolveMixedSpecAndLegacyStatus(state, resource, smLoader)
   582  	if err != nil {
   583  		return nil, nil, err
   584  	}
   585  	return spec, status, nil
   586  }
   587  
   588  func mergeSpecWithLiteState(state map[string]interface{}, spec map[string]interface{}, path []string,
   589  	schema *openapi.Schema, managedFields *fieldpath.Set, smLoader dclmetadata.ServiceMetadataLoader) (map[string]interface{}, error) {
   590  	res := make(map[string]interface{})
   591  	for f, s := range schema.Properties {
   592  		if dclextension.IsReferenceField(s) {
   593  			refField, val, err := mergeReferenceField(state, spec, append(path, f), s, smLoader)
   594  			if err != nil {
   595  				return nil, err
   596  			}
   597  			if val != nil {
   598  				res[refField] = val
   599  			}
   600  			continue
   601  		}
   602  
   603  		stateVal := state[f]
   604  		specVal := spec[f]
   605  		if stateVal == nil && specVal == nil {
   606  			continue
   607  		}
   608  		// for non-returnable values, use the last captured or user specified value
   609  		if stateVal == nil {
   610  			res[f] = deepcopy.DeepCopy(specVal)
   611  			continue
   612  		}
   613  		if specVal == nil {
   614  			res[f] = deepcopy.DeepCopy(stateVal)
   615  			continue
   616  		}
   617  
   618  		switch s.Type {
   619  		case "object":
   620  			if s.AdditionalProperties != nil {
   621  				if typeutil.IsPrimitiveType(s.AdditionalProperties.Type) {
   622  					val, err := mergePrimitiveMap(state, spec, append(path, f), managedFields)
   623  					if err != nil {
   624  						return nil, err
   625  					}
   626  					dcl.AddToMap(f, val, res)
   627  					continue
   628  				}
   629  				if s.AdditionalProperties.Type == "object" {
   630  					val, err := mergeObjectMap(state, spec, append(path, f), s.AdditionalProperties, managedFields, smLoader)
   631  					if err != nil {
   632  						return nil, err
   633  					}
   634  					dcl.AddToMap(f, val, res)
   635  					continue
   636  				}
   637  				return nil, fmt.Errorf("unsupported AdditionalProperties.Type for field '%v': %v", f, s.AdditionalProperties.Type)
   638  			}
   639  
   640  			val, err := mergeNestedObject(state, spec, append(path, f), s, managedFields, smLoader)
   641  			if err != nil {
   642  				return nil, err
   643  			}
   644  			if val != nil {
   645  				res[f] = val
   646  			}
   647  		case "array":
   648  			if typeutil.IsPrimitiveType(s.Items.Type) {
   649  				listVal, ok := stateVal.([]interface{})
   650  				if !ok {
   651  					return nil, fmt.Errorf("expected the value for field '%v' to be []interface{} but was actually %T", f, stateVal)
   652  				}
   653  				if len(listVal) != 0 {
   654  					res[f] = deepcopy.DeepCopy(listVal)
   655  				}
   656  				continue
   657  			}
   658  			if s.Items.Type == "object" {
   659  				retObjList, err := mergeObjectArray(stateVal, specVal, append(path, f), s, smLoader)
   660  				if err != nil {
   661  					return nil, err
   662  				}
   663  				if len(retObjList) == 0 {
   664  					continue
   665  				}
   666  				res[f] = retObjList
   667  				continue
   668  			}
   669  			return nil, fmt.Errorf("unsupported Items.Type for the array field '%v': %v", f, s.Items.Type)
   670  		case "string":
   671  			if k8s.IsK8sManaged(f, spec, managedFields) {
   672  				res[f] = specVal
   673  			} else {
   674  				isSensitiveField, err := dclextension.IsSensitiveField(s)
   675  				if err != nil {
   676  					return nil, err
   677  				}
   678  				if isSensitiveField {
   679  					// assert the stateVal is in the right format by marshalling into SensitiveField struct
   680  					sensitiveVal := corekccv1alpha1.SensitiveField{}
   681  					if err := util.Marshal(stateVal, &sensitiveVal); err != nil {
   682  						return nil, err
   683  					}
   684  					res[f] = stateVal
   685  				} else {
   686  					res[f] = stateVal
   687  				}
   688  			}
   689  		case "boolean", "number", "integer":
   690  			if k8s.IsK8sManaged(f, spec, managedFields) {
   691  				res[f] = specVal
   692  			} else {
   693  				res[f] = stateVal
   694  			}
   695  		default:
   696  			return nil, fmt.Errorf("unknown schema type %v", schema.Type)
   697  		}
   698  	}
   699  	return res, nil
   700  }
   701  
   702  func mergeReferenceField(state map[string]interface{}, spec map[string]interface{}, path []string,
   703  	s *openapi.Schema, smLoader dclmetadata.ServiceMetadataLoader) (string, interface{}, error) {
   704  	if dcl.IsMultiTypeParentReferenceField(path) {
   705  		return mergeMultiTypeParentReferenceField(state, spec, s, smLoader)
   706  	}
   707  	return mergeResourceReference(state, spec, path, s)
   708  }
   709  
   710  func mergeMultiTypeParentReferenceField(state map[string]interface{}, spec map[string]interface{},
   711  	s *openapi.Schema, smLoader dclmetadata.ServiceMetadataLoader) (string, interface{}, error) {
   712  	// See if the user already specified a value for one of the hierarchical
   713  	// references supported by the resource.
   714  	specVal, tc, err := dcl.GetHierarchicalRefFromConfigForMultiParentResource(spec, s, smLoader)
   715  	if err != nil {
   716  		return "", nil, fmt.Errorf("error getting hierarchical reference from spec for multi-parent resource: %w", err)
   717  	}
   718  	if specVal != nil {
   719  		return tc.Key, specVal, nil
   720  	}
   721  
   722  	// See if one of the hierarchical references was set in the state.
   723  	stateVal, tc, err := dcl.GetHierarchicalRefFromConfigForMultiParentResource(state, s, smLoader)
   724  	if err != nil {
   725  		return "", nil, fmt.Errorf("error getting hierarchical reference from state for multi-parent resource: %w", err)
   726  	}
   727  	if stateVal != nil {
   728  		return tc.Key, stateVal, nil
   729  	}
   730  
   731  	return "", nil, nil
   732  }
   733  
   734  func mergeResourceReference(state map[string]interface{}, spec map[string]interface{}, path []string, s *openapi.Schema) (string, interface{}, error) {
   735  	refField, err := dclextension.GetReferenceFieldName(path, s)
   736  	if err != nil {
   737  		return "", nil, err
   738  	}
   739  	if specVal, ok := spec[refField]; ok {
   740  		// The user already specified a value for the KCC reference field in
   741  		// the previous spec. Preserve it.
   742  		return refField, specVal, nil
   743  	} else if stateVal, ok := state[refField]; ok && stateVal != nil {
   744  		return refField, stateVal, nil
   745  	}
   746  	return "", nil, nil
   747  }
   748  
   749  func mergeObjectArray(stateVal, specVal interface{}, path []string, s *openapi.Schema, smLoader dclmetadata.ServiceMetadataLoader) ([]interface{}, error) {
   750  	field := pathslice.Base(path)
   751  	// DCL will return items in the original order.
   752  	retObjList := make([]interface{}, 0)
   753  	specList, ok := specVal.([]interface{})
   754  	if !ok {
   755  		return nil, fmt.Errorf("expected the spec value for field '%v' to be []interface{} but was actually %T", field, specVal)
   756  	}
   757  	stateList, ok := stateVal.([]interface{})
   758  	if !ok {
   759  		return nil, fmt.Errorf("expected the state value for field '%v' to be []interface{} but was actually %T", field, stateList)
   760  	}
   761  	if len(specList) > len(stateList) {
   762  		return nil, fmt.Errorf("there are fewer items for field '%v' returned in state than configured in spec; state: %v, spec: %v", field, stateList, specList)
   763  	}
   764  	for idx, elem := range stateList {
   765  		stateObjMap, ok := elem.(map[string]interface{})
   766  		if !ok {
   767  			return nil, fmt.Errorf("expected the item value from state for field '%v' to be map[string]interface{} but was actually %T", field, elem)
   768  		}
   769  		var specObjMap map[string]interface{}
   770  		if idx < len(specList) {
   771  			specObjMap, ok = specList[idx].(map[string]interface{})
   772  			if !ok {
   773  				return nil, fmt.Errorf("expected the item value from spec for field '%v' to be map[string]interface{} but was actually %T", field, elem)
   774  			}
   775  		}
   776  		val, err := mergeSpecWithLiteState(stateObjMap, specObjMap, path, s.Items, nil, smLoader)
   777  		if err != nil {
   778  			return nil, err
   779  		}
   780  		if val != nil {
   781  			retObjList = append(retObjList, val)
   782  		}
   783  	}
   784  	return retObjList, nil
   785  }
   786  
   787  func mergeObjectMap(state map[string]interface{}, spec map[string]interface{}, path []string, s *openapi.Schema, managedFields *fieldpath.Set, smLoader dclmetadata.ServiceMetadataLoader) (map[string]interface{}, error) {
   788  	field := pathslice.Base(path)
   789  	var nestedManagedFields *fieldpath.Set
   790  	if managedFields != nil {
   791  		pe := fieldpath.PathElement{FieldName: &field}
   792  		var found bool
   793  		nestedManagedFields, found = managedFields.Children.Get(pe)
   794  		if !found {
   795  			nestedManagedFields = fieldpath.NewSet()
   796  		}
   797  	}
   798  	retObjectMap := make(map[string]interface{})
   799  	specObjectMap, ok := spec[field].(map[string]interface{})
   800  	if !ok {
   801  		return nil, fmt.Errorf("expected the spec value for field '%v' to be map[string]interface{} but was actually %T", field, spec[field])
   802  	}
   803  	stateObjectMap, ok := state[field].(map[string]interface{})
   804  	if !ok {
   805  		return nil, fmt.Errorf("expected the state value for field '%v' to be map[string]interface{} but was actually %T", field, state[field])
   806  	}
   807  	if len(specObjectMap) != len(stateObjectMap) {
   808  		return nil, fmt.Errorf("the number of items for field '%v' returned in state is not the same as configured in spec; state: %v, spec: %v", field, stateObjectMap, specObjectMap)
   809  	}
   810  
   811  	for k, _ := range stateObjectMap {
   812  		if _, ok := specObjectMap[k]; !ok {
   813  			return nil, fmt.Errorf("key '%v' is not configured in spec for field '%v'", k, field)
   814  		}
   815  		mergedVal, err := mergeNestedObject(stateObjectMap, specObjectMap, append(path, k), s, nestedManagedFields, smLoader)
   816  		if err != nil {
   817  			return nil, err
   818  		}
   819  		retObjectMap[k] = mergedVal
   820  	}
   821  	return retObjectMap, nil
   822  }
   823  
   824  func mergePrimitiveMap(state map[string]interface{}, spec map[string]interface{}, path []string, managedFields *fieldpath.Set) (interface{}, error) {
   825  	field := pathslice.Base(path)
   826  	if k8s.IsK8sManaged(field, spec, managedFields) {
   827  		return deepcopy.DeepCopy(spec[field]), nil
   828  	}
   829  	if state[field] != nil {
   830  		valMap, ok := state[field].(map[string]interface{})
   831  		if !ok {
   832  			return nil, fmt.Errorf("expected the value for field '%v' to be map[string]interface{} but was actually %T", field, state[field])
   833  		}
   834  		if len(valMap) != 0 {
   835  			return deepcopy.DeepCopy(valMap), nil
   836  		}
   837  	}
   838  	return nil, nil
   839  }
   840  
   841  func mergeNestedObject(state map[string]interface{}, spec map[string]interface{}, path []string, s *openapi.Schema,
   842  	managedFields *fieldpath.Set, smLoader dclmetadata.ServiceMetadataLoader) (map[string]interface{}, error) {
   843  	field := pathslice.Base(path)
   844  	var nestedManagedFields *fieldpath.Set
   845  	if managedFields != nil {
   846  		pe := fieldpath.PathElement{FieldName: &field}
   847  		var found bool
   848  		nestedManagedFields, found = managedFields.Children.Get(pe)
   849  		if !found {
   850  			nestedManagedFields = fieldpath.NewSet()
   851  		}
   852  	}
   853  	stateConfigMap, ok := state[field].(map[string]interface{})
   854  	if !ok {
   855  		return nil, fmt.Errorf("expected the state value for field '%v' to be map[string]interface{} but was actually %T", field, state[field])
   856  	}
   857  	specConfigMap, ok := spec[field].(map[string]interface{})
   858  	if !ok {
   859  		return nil, fmt.Errorf("expected the spec value for field '%v' to be map[string]interface{} but was actually %T", field, spec[field])
   860  	}
   861  	val, err := mergeSpecWithLiteState(stateConfigMap, specConfigMap, path, s, nestedManagedFields, smLoader)
   862  	if err != nil {
   863  		return nil, err
   864  	}
   865  	return val, nil
   866  }
   867  

View as plain text