...

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

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

     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 conversion
    16  
    17  import (
    18  	"fmt"
    19  	"strings"
    20  
    21  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl"
    22  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/extension"
    23  	dclmetadata "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/metadata"
    24  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/schema/dclschemaloader"
    25  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    26  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/label"
    27  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/pathslice"
    28  
    29  	dclunstruct "github.com/GoogleCloudPlatform/declarative-resource-client-library/unstructured"
    30  	"github.com/nasa9084/go-openapi"
    31  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    32  )
    33  
    34  // A Converter knows how to convert between KRM and DCL format.
    35  type Converter struct {
    36  	SchemaLoader   dclschemaloader.DCLSchemaLoader
    37  	MetadataLoader dclmetadata.ServiceMetadataLoader
    38  }
    39  
    40  // New returns a Converter.
    41  func New(schemaLoader dclschemaloader.DCLSchemaLoader, metadataLoader dclmetadata.ServiceMetadataLoader) *Converter {
    42  	c := &Converter{}
    43  	c.SchemaLoader = schemaLoader
    44  	c.MetadataLoader = metadataLoader
    45  	return c
    46  }
    47  
    48  // KRMObjectToDCLObject converts the given KCC Lite KRM resource to a DCL resource.
    49  func (c *Converter) KRMObjectToDCLObject(obj *unstructured.Unstructured) (*dclunstruct.Resource, error) {
    50  	gvk := obj.GroupVersionKind()
    51  	stv, err := dclmetadata.ToServiceTypeVersion(gvk, c.MetadataLoader)
    52  	if err != nil {
    53  		return nil, fmt.Errorf("error resolving the DCL ServiceTypeVersion from GroupVersionKind %v: %w", gvk, err)
    54  	}
    55  	resourceMetadata, found := c.MetadataLoader.GetResourceWithGVK(gvk)
    56  	if !found {
    57  		return nil, fmt.Errorf("ServiceMetadata for resource with GroupVersionKind %v not found", gvk)
    58  	}
    59  	// load DCL schema per DCL ServiceTypeVersion
    60  	dclSchema, err := c.SchemaLoader.GetDCLSchema(stv)
    61  	if err != nil {
    62  		return nil, fmt.Errorf("error getting the DCL schema for ServiceTypeVersion %v: %w", stv, err)
    63  	}
    64  	if dclSchema.Type != "object" {
    65  		return nil, fmt.Errorf("expect the entry level DCL OpenAPI schema to be 'object' type, but got %v", dclSchema.Type)
    66  	}
    67  
    68  	r := &dclunstruct.Resource{
    69  		STV: stv,
    70  	}
    71  	// Only convert spec fields for now since output-only fields are not enforceable for DCL.
    72  	spec := obj.UnstructuredContent()["spec"]
    73  	if spec == nil {
    74  		spec = make(map[string]interface{})
    75  	}
    76  	dclObj, err := convertToDCL(spec, []string{}, dclSchema, c.MetadataLoader, false)
    77  	if err != nil {
    78  		return nil, fmt.Errorf("error converting the spec of resource %v/%v to DCL object: %w", obj.GetNamespace(), obj.GetName(), err)
    79  	}
    80  	dclObjMap, ok := dclObj.(map[string]interface{})
    81  	if !ok {
    82  		return nil, fmt.Errorf("expected the converted DCL object to be map[string]interface{} but was actually %T", dclObj)
    83  	}
    84  	r.Object = dclObjMap
    85  	// special handling on name, container and labels fields
    86  	if err := convertToDCLNameField(obj, r, dclSchema); err != nil {
    87  		return nil, fmt.Errorf("error resolving the value of 'name' field for resource %v/%v: %w", obj.GetNamespace(), obj.GetName(), err)
    88  	}
    89  	// TODO(b/186159460): Delete this if-block once all resources support
    90  	// hierarchical references.
    91  	if !resourceMetadata.SupportsHierarchicalReferences {
    92  		if err := convertToDCLContainerField(obj, r, dclSchema); err != nil {
    93  			return nil, fmt.Errorf("error resolving the value of the container field for resource %v/%v: %w", obj.GetNamespace(), obj.GetName(), err)
    94  		}
    95  	}
    96  	if err := convertToDCLLabelsField(obj, r, dclSchema); err != nil {
    97  		return nil, fmt.Errorf("error converting Kubernetes labels into DCL labels for resource %v/%v: %w", obj.GetNamespace(), obj.GetName(), err)
    98  	}
    99  	return r, nil
   100  }
   101  
   102  // DCLObjectToKRMObject converts the given DCL resource to a KCC Lite KRM resource.
   103  func (c *Converter) DCLObjectToKRMObject(resource *dclunstruct.Resource) (*unstructured.Unstructured, error) {
   104  	obj := &unstructured.Unstructured{
   105  		Object: make(map[string]interface{}),
   106  	}
   107  	gvk, err := dclmetadata.ToGroupVersionKind(resource.STV, c.MetadataLoader)
   108  	if err != nil {
   109  		return nil, fmt.Errorf("error resolving GroupVersionKind from the DCL ServiceTypeVersion %v: %w", resource.STV, err)
   110  	}
   111  	obj.SetGroupVersionKind(gvk)
   112  	resourceMetadata, found := c.MetadataLoader.GetResourceWithGVK(gvk)
   113  	if !found {
   114  		return nil, fmt.Errorf("ServiceMetadata for resource with GroupVersionKind %v not found", gvk)
   115  	}
   116  	dclSchema, err := c.SchemaLoader.GetDCLSchema(resource.STV)
   117  	if err != nil {
   118  		return nil, fmt.Errorf("error getting the DCL schema for ServiceTypeVersion %v", resource.STV)
   119  	}
   120  	if dclSchema.Type != "object" {
   121  		return nil, fmt.Errorf("expect the entry level DCL OpenAPI schema to be object type, but got %v", dclSchema.Type)
   122  	}
   123  
   124  	// ensure DCL-returned state contains no nil values
   125  	dcl.TrimNilFields(resource.Object)
   126  	// convert dcl state to spec and status
   127  	spec, err := convertToKRMSpec(resource.Object, []string{}, dclSchema, c.MetadataLoader, resourceMetadata, false)
   128  	if err != nil {
   129  		return nil, fmt.Errorf("error extracting the spec from the DCL resource %v/%v: %w", obj.GetNamespace(), obj.GetName(), err)
   130  	}
   131  	if spec != nil {
   132  		specMap, ok := spec.(map[string]interface{})
   133  		if !ok {
   134  			return nil, fmt.Errorf("expected the converted spec to be map[string]interface{} but was actually %T", spec)
   135  		}
   136  		if len(specMap) > 0 {
   137  			obj.Object["spec"] = specMap
   138  		}
   139  	}
   140  	status, err := convertToKRMStatus(resource.Object, dclSchema)
   141  	if err != nil {
   142  		return nil, fmt.Errorf("error extracting the status from the DCL resource %v/%v: %w", obj.GetNamespace(), obj.GetName(), err)
   143  	}
   144  	if len(status) > 0 {
   145  		obj.Object["status"] = status
   146  	}
   147  	// special handling on name, container and labels fields
   148  	if err := liftDCLLabelsField(obj, dclSchema); err != nil {
   149  		return nil, fmt.Errorf("error lifting 'labels' field to metadata.labels: %w", err)
   150  	}
   151  	// TODO(b/186159460): Delete this if-block once all resources support
   152  	// hierarchical references.
   153  	if !resourceMetadata.SupportsHierarchicalReferences {
   154  		if err := liftDCLContainerField(obj, dclSchema); err != nil {
   155  			return nil, fmt.Errorf("error lifting contianer field to annotation: %w", err)
   156  		}
   157  	}
   158  	if err := convertToKRMResourceIDField(obj, dclSchema); err != nil {
   159  		return nil, fmt.Errorf("error converting 'name' field to 'resourceID': %w", err)
   160  	}
   161  	return obj, nil
   162  }
   163  
   164  func convertToKRMSpec(val interface{}, path []string, schema *openapi.Schema, smLoader dclmetadata.ServiceMetadataLoader,
   165  	resourceMetadata dclmetadata.Resource, isCollectionItemSchema bool) (interface{}, error) {
   166  	if val == nil {
   167  		return nil, nil
   168  	}
   169  	switch schema.Type {
   170  	case "object":
   171  		obj, ok := val.(map[string]interface{})
   172  		if !ok {
   173  			return nil, fmt.Errorf("expected the value to be map[string]interface{}, but was actually %T", val)
   174  		}
   175  		res := make(map[string]interface{})
   176  		// The field additionalProperties is mutually exclusive with properties in CustomResourceDefinition.
   177  		if schema.AdditionalProperties != nil {
   178  			for k, v := range obj {
   179  				convertedVal, err := convertToKRMSpec(v, append(path, k), schema.AdditionalProperties, smLoader, resourceMetadata, true)
   180  				if err != nil {
   181  					return nil, err
   182  				}
   183  				dcl.AddToMap(k, convertedVal, res)
   184  			}
   185  			return res, nil
   186  		}
   187  		for field, fieldSchema := range schema.Properties {
   188  			if fieldSchema.ReadOnly && !isCollectionItemSchema {
   189  				continue
   190  			}
   191  			val, ok := obj[field]
   192  			if !ok || val == nil {
   193  				continue
   194  			}
   195  			isSensitive, err := extension.IsSensitiveField(fieldSchema)
   196  			if err != nil {
   197  				return nil, err
   198  			}
   199  			if !fieldSchema.ReadOnly && isSensitive {
   200  				convertedVal, err := convertSensitiveFieldToKRM(val)
   201  				if err != nil {
   202  					return nil, fmt.Errorf("error resolving the value for sensitive field %v: %w", field, err)
   203  				}
   204  				dcl.AddToMap(field, convertedVal, res)
   205  				continue
   206  			}
   207  			// For resources that don't support hierarchical references,
   208  			// convert the container field to a primitive field in the spec.
   209  			// This field will later be converted to an annotation by
   210  			// liftDCLContainerField().
   211  			// TODO(b/186159460): Delete this if-block once all resources support
   212  			// hierarchical references.
   213  			if !fieldSchema.ReadOnly && dcl.IsContainerField(append(path, field)) && !resourceMetadata.SupportsHierarchicalReferences {
   214  				convertedVal, err := convertToKRMSpec(obj[field], append(path, field), fieldSchema, smLoader, resourceMetadata, isCollectionItemSchema)
   215  				if err != nil {
   216  					return nil, fmt.Errorf("error resolving the value for DCL field %v: %w", field, err)
   217  				}
   218  				dcl.AddToMap(field, convertedVal, res)
   219  				continue
   220  			}
   221  			if !fieldSchema.ReadOnly && extension.IsReferenceField(fieldSchema) {
   222  				refField, convertedVal, err := convertReferenceFieldToKRM(append(path, field), obj[field], fieldSchema, smLoader)
   223  				if err != nil {
   224  					return nil, fmt.Errorf("error converting the value for reference field %v: %w", field, err)
   225  				}
   226  				dcl.AddToMap(refField, convertedVal, res)
   227  				continue
   228  			}
   229  			convertedVal, err := convertToKRMSpec(obj[field], append(path, field), fieldSchema, smLoader, resourceMetadata, isCollectionItemSchema)
   230  			if err != nil {
   231  				return nil, fmt.Errorf("error resolving the value for DCL field %v: %w", field, err)
   232  			}
   233  			dcl.AddToMap(field, convertedVal, res)
   234  		}
   235  		return res, nil
   236  	case "array":
   237  		items, ok := val.([]interface{})
   238  		if !ok {
   239  			return nil, fmt.Errorf("expected the value to be array but was actually %T", val)
   240  		}
   241  		if len(items) == 0 {
   242  			return nil, nil
   243  		}
   244  		res := make([]interface{}, 0)
   245  		for _, item := range items {
   246  			processedItem, err := convertToKRMSpec(item, path, schema.Items, smLoader, resourceMetadata, true)
   247  			if err != nil {
   248  				return nil, fmt.Errorf("error converting list item: %v", err)
   249  			}
   250  			res = append(res, processedItem)
   251  		}
   252  		return res, nil
   253  	case "string", "boolean", "number", "integer":
   254  		return val, nil
   255  	default:
   256  		return nil, fmt.Errorf("unknown schema type %v", schema.Type)
   257  	}
   258  }
   259  
   260  func getStatusFieldsWithValuePopulated(path string, val interface{}, schema *openapi.Schema, paths []string) ([]string, error) {
   261  	if val == nil {
   262  		return paths, nil
   263  	}
   264  	if schema.ReadOnly {
   265  		paths = append(paths, path)
   266  		return paths, nil
   267  	}
   268  	if schema.Type == "object" {
   269  		obj, ok := val.(map[string]interface{})
   270  		if !ok {
   271  			return nil, fmt.Errorf("expected the value to be map[string]interface{} but was actually %T", val)
   272  		}
   273  		if len(obj) == 0 {
   274  			return paths, nil
   275  		}
   276  		if schema.AdditionalProperties != nil {
   277  			return getStatusFieldsWithValuePopulated(path, obj, schema.AdditionalProperties, paths)
   278  		}
   279  		var err error
   280  		for k, v := range obj {
   281  			fieldSchema, ok := schema.Properties[k]
   282  			if !ok {
   283  				continue
   284  			}
   285  			qualifiedName := k
   286  			if path != "" {
   287  				qualifiedName = path + "." + k
   288  			}
   289  			paths, err = getStatusFieldsWithValuePopulated(qualifiedName, v, fieldSchema, paths)
   290  			if err != nil {
   291  				return nil, fmt.Errorf("error getting status fields with value from %v: %w", qualifiedName, err)
   292  			}
   293  		}
   294  		return paths, nil
   295  	}
   296  	return paths, nil
   297  }
   298  
   299  func convertToKRMStatus(obj map[string]interface{}, schema *openapi.Schema) (map[string]interface{}, error) {
   300  	paths := make([]string, 0)
   301  	paths, err := getStatusFieldsWithValuePopulated("", obj, schema, paths)
   302  	if err != nil {
   303  		return nil, fmt.Errorf("error getting status fields from DCL object: %w", err)
   304  	}
   305  	status := make(map[string]interface{})
   306  	for _, path := range paths {
   307  		val, found, err := unstructured.NestedFieldCopy(obj, strings.Split(path, ".")...)
   308  		if err != nil {
   309  			return nil, fmt.Errorf("error copying the value for status field %v: %w", path, err)
   310  		}
   311  		if !found {
   312  			return nil, fmt.Errorf("couldn't find the value for status field %v", path)
   313  		}
   314  		splitPath := strings.Split(path, ".")
   315  		splitPath = renameStatusFieldIfCollidesWithReservedName(splitPath)
   316  		if err := unstructured.SetNestedField(status, val, splitPath...); err != nil {
   317  			return nil, fmt.Errorf("error setting the value for status field %v: %w", path, err)
   318  		}
   319  	}
   320  	return status, nil
   321  }
   322  
   323  func convertToDCL(val interface{}, path []string, schema *openapi.Schema,
   324  	smLoader dclmetadata.ServiceMetadataLoader, isCollectionItemSchema bool) (interface{}, error) {
   325  	if val == nil {
   326  		return nil, nil
   327  	}
   328  	switch schema.Type {
   329  	case "object":
   330  		obj, ok := val.(map[string]interface{})
   331  		if !ok {
   332  			return nil, fmt.Errorf("expected the value to be map[string]interface{} but was actually %T", val)
   333  		}
   334  		// The field additionalProperties is mutually exclusive with properties in CustomResourceDefinition.
   335  		if schema.AdditionalProperties != nil {
   336  			res := make(map[string]interface{})
   337  			for k, v := range obj {
   338  				dclVal, err := convertToDCL(v, append(path, k), schema.AdditionalProperties, smLoader, true)
   339  				if err != nil {
   340  					return nil, fmt.Errorf("error converting item with AdditionalProperties schema: %w", err)
   341  				}
   342  				dcl.AddToMap(k, dclVal, res)
   343  			}
   344  			return res, nil
   345  		}
   346  		res := make(map[string]interface{})
   347  		for field, fieldSchema := range schema.Properties {
   348  			// Avoid dropping read-only fields from objects in collections
   349  			// (maps, arrays) to avoid the possibility of ending up with empty
   350  			// objects, as this would result in diffs given that the live value
   351  			// of the object is non-empty. Note: we only need to do this for
   352  			// objects in collections as that is the only case where read-only
   353  			// fields can end up in the spec instead of status.
   354  			if fieldSchema.ReadOnly && !isCollectionItemSchema {
   355  				continue
   356  			}
   357  
   358  			// It is expected that convertToDCL() will be first called with an
   359  			// entry-level DCL schema, which is always an object, and then be
   360  			// called in the recursion with non-entry-level schemas. So we only
   361  			// check sensitive fields here (instead of checking it in the string
   362  			// type) before calling convertToDCL() recursively to process field
   363  			// generically.
   364  			isSensitive, err := extension.IsSensitiveField(fieldSchema)
   365  			if err != nil {
   366  				return nil, err
   367  			}
   368  			if !fieldSchema.ReadOnly && isSensitive {
   369  				convertedVal, err := convertSensitiveFieldToDCL(append(path, field), obj)
   370  				if err != nil {
   371  					return nil, fmt.Errorf("error resolving the value for sensitive field %v: %w", field, err)
   372  				}
   373  				dcl.AddToMap(field, convertedVal, res)
   374  				continue
   375  			}
   376  			if !fieldSchema.ReadOnly && extension.IsReferenceField(fieldSchema) {
   377  				convertedVal, err := convertReferenceFieldToDCL(append(path, field), obj, fieldSchema, smLoader)
   378  				if err != nil {
   379  					return nil, fmt.Errorf("error resolving the value for reference field %v: %w", field, err)
   380  				}
   381  				dcl.AddToMap(field, convertedVal, res)
   382  				continue
   383  			}
   384  			convertedVal, err := convertToDCL(obj[field], append(path, field), fieldSchema, smLoader, isCollectionItemSchema)
   385  			if err != nil {
   386  				return nil, fmt.Errorf("error resolving the value for DCL field %v: %w", field, err)
   387  			}
   388  			dcl.AddToMap(field, convertedVal, res)
   389  		}
   390  		return res, nil
   391  	case "array":
   392  		items, ok := val.([]interface{})
   393  		if !ok {
   394  			return nil, fmt.Errorf("expected the value to be []interface{} but was actually %T", val)
   395  		}
   396  		res := make([]interface{}, 0)
   397  		for _, item := range items {
   398  			processedItem, err := convertToDCL(item, path, schema.Items, smLoader, true)
   399  			if err != nil {
   400  				return nil, fmt.Errorf("error converting the item: %v", err)
   401  			}
   402  			res = append(res, processedItem)
   403  		}
   404  		return res, nil
   405  	case "integer":
   406  		return dcl.CanonicalizeIntegerValue(val)
   407  	case "number":
   408  		return dcl.CanonicalizeNumberValue(val)
   409  	case "string", "boolean":
   410  		return val, nil
   411  	default:
   412  		return nil, fmt.Errorf("unknown schema type %v", schema.Type)
   413  	}
   414  }
   415  
   416  func convertSensitiveFieldToDCL(path []string, obj map[string]interface{}) (interface{}, error) {
   417  	field := pathslice.Base(path)
   418  	if obj[field] == nil {
   419  		return nil, nil
   420  	}
   421  	raw := obj[field]
   422  	secretRef, ok := raw.(map[string]interface{})
   423  	if !ok {
   424  		return nil, fmt.Errorf("expected the value to be map[string]interface{} for field %v but was actually %T", field, raw)
   425  	}
   426  	return secretRef["value"], nil
   427  }
   428  
   429  func convertSensitiveFieldToKRM(val interface{}) (interface{}, error) {
   430  	if val == nil {
   431  		return nil, nil
   432  	}
   433  	sensitiveFieldStruct := make(map[string]interface{})
   434  	sensitiveFieldStruct["value"] = val
   435  	return sensitiveFieldStruct, nil
   436  }
   437  
   438  func convertReferenceFieldToDCL(path []string, obj map[string]interface{}, schema *openapi.Schema,
   439  	smLoader dclmetadata.ServiceMetadataLoader) (interface{}, error) {
   440  	if dcl.IsMultiTypeParentReferenceField(path) {
   441  		return convertMultiTypeParentReferenceFieldToDCL(obj, schema, smLoader)
   442  	}
   443  	if schema.Type == "array" {
   444  		return convertListOfReferencesFieldToDCL(path, obj, schema)
   445  	}
   446  	return convertRegularReferenceFieldToDCL(path, obj, schema)
   447  }
   448  
   449  func convertMultiTypeParentReferenceFieldToDCL(obj map[string]interface{}, schema *openapi.Schema,
   450  	smLoader dclmetadata.ServiceMetadataLoader) (interface{}, error) {
   451  	rawVal, tc, err := dcl.GetHierarchicalRefFromConfigForMultiParentResource(obj, schema, smLoader)
   452  	if err != nil {
   453  		return nil, fmt.Errorf("error getting hierarchical reference from config for multi-parent resource: %w", err)
   454  	}
   455  	if rawVal == nil {
   456  		return nil, fmt.Errorf("no hierarchical reference found for multi-parent resource")
   457  	}
   458  	refField := tc.Key
   459  	refObj, ok := rawVal.(map[string]interface{})
   460  	if !ok {
   461  		return nil, fmt.Errorf("expected the value to be map[string]interface{} for reference field %v but was actually %T", refField, rawVal)
   462  	}
   463  
   464  	// Prefix the 'external' value with the parent prefix (e.g. "projects/")
   465  	// since that is how DCL distinguishes between different types of parents
   466  	// for multi-type parent reference fields.
   467  	rawExternalVal, ok := refObj["external"]
   468  	if !ok {
   469  		return nil, fmt.Errorf("'external' was unexpectedly not set for reference field %v", refField)
   470  	}
   471  	externalVal, ok := rawExternalVal.(string)
   472  	if !ok {
   473  		return nil, fmt.Errorf("expected the value of 'external' to be string for reference field %v but was actually %T", refField, rawExternalVal)
   474  	}
   475  	if externalVal == "" {
   476  		return externalVal, nil
   477  	}
   478  	parentPrefix := dcl.ParentPrefixForKind(tc.GVK.Kind)
   479  	if strings.HasPrefix(externalVal, parentPrefix) {
   480  		return externalVal, nil
   481  	}
   482  	return fmt.Sprintf("%v%v", parentPrefix, externalVal), nil
   483  }
   484  
   485  func convertListOfReferencesFieldToDCL(path []string, obj map[string]interface{}, schema *openapi.Schema) (interface{}, error) {
   486  	refField, err := extension.GetReferenceFieldName(path, schema)
   487  	if err != nil {
   488  		return nil, fmt.Errorf("error getting the reference field name %w", err)
   489  	}
   490  	if obj[refField] == nil {
   491  		return nil, nil
   492  	}
   493  	rawVal := obj[refField]
   494  	items, ok := rawVal.([]interface{})
   495  	if !ok {
   496  		return nil, fmt.Errorf("expected the value to be []interface{} for reference field %v but was actually %T", refField, rawVal)
   497  	}
   498  	res := make([]interface{}, 0)
   499  	for _, item := range items {
   500  		refObj, ok := item.(map[string]interface{})
   501  		if !ok {
   502  			return nil, fmt.Errorf("expected the value for the reference to be map[string]interface{}, but was actually %T", item)
   503  		}
   504  		refVal, err := resolveReferenceValue(refObj, schema.Items)
   505  		if err != nil {
   506  			return nil, err
   507  		}
   508  		res = append(res, refVal)
   509  	}
   510  	return res, nil
   511  }
   512  
   513  func convertRegularReferenceFieldToDCL(path []string, obj map[string]interface{}, schema *openapi.Schema) (interface{}, error) {
   514  	refField, err := extension.GetReferenceFieldName(path, schema)
   515  	if err != nil {
   516  		return nil, fmt.Errorf("error getting the reference field name %w", err)
   517  	}
   518  	if obj[refField] == nil {
   519  		return nil, nil
   520  	}
   521  	rawVal := obj[refField]
   522  	refObj, ok := rawVal.(map[string]interface{})
   523  	if !ok {
   524  		return nil, fmt.Errorf("expected the value to be map[string]interface{} for reference field %v but was actually %T", refField, rawVal)
   525  	}
   526  	return resolveReferenceValue(refObj, schema)
   527  }
   528  
   529  func convertReferenceFieldToKRM(path []string, val interface{}, schema *openapi.Schema, smLoader dclmetadata.ServiceMetadataLoader) (string, interface{}, error) {
   530  	if dcl.IsMultiTypeParentReferenceField(path) {
   531  		return convertMultiTypeParentReferenceFieldToKRM(path, val, schema, smLoader)
   532  	}
   533  	if schema.Type == "array" {
   534  		return convertListOfReferencesFieldToKRM(path, val, schema)
   535  	}
   536  	return convertRegularReferenceToKRM(path, val, schema)
   537  }
   538  
   539  func convertMultiTypeParentReferenceFieldToKRM(path []string, val interface{}, schema *openapi.Schema, smLoader dclmetadata.ServiceMetadataLoader) (string, interface{}, error) {
   540  	field := pathslice.Base(path)
   541  	tcs, err := dcl.GetReferenceTypeConfigs(schema, smLoader)
   542  	if err != nil {
   543  		return "", nil, fmt.Errorf("error getting reference type configs for DCL field '%v': %w", field, err)
   544  	}
   545  	v, ok := val.(string)
   546  	if !ok {
   547  		return "", nil, fmt.Errorf("expected the value to be string for DCL field '%v' but was actually %T", field, val)
   548  	}
   549  	if v == "" {
   550  		return "", nil, fmt.Errorf("value of DCL field '%v' is unexpectedly an empty string", field)
   551  	}
   552  	for _, tc := range tcs {
   553  		// Multi-type parent reference fields in DCL use parent prefixes (e.g.
   554  		// "projects/") to denote the type of resource being referenced.
   555  		if strings.HasPrefix(v, dcl.ParentPrefixForKind(tc.GVK.Kind)) {
   556  			convertedVal := map[string]interface{}{
   557  				"external": v,
   558  			}
   559  			return tc.Key, convertedVal, nil
   560  		}
   561  	}
   562  	return "", nil, fmt.Errorf("value for DCL field %v could not be recognized as a valid parent: %v", field, v)
   563  }
   564  
   565  func convertListOfReferencesFieldToKRM(path []string, val interface{}, schema *openapi.Schema) (string, interface{}, error) {
   566  	field := pathslice.Base(path)
   567  	refField, err := extension.GetReferenceFieldName(path, schema)
   568  	if err != nil {
   569  		return "", nil, fmt.Errorf("error getting the reference field name %w", err)
   570  	}
   571  	items, ok := val.([]interface{})
   572  	if !ok {
   573  		return "", nil, fmt.Errorf("expected the value to be []interface{} for reference field %v but was actually %T", refField, val)
   574  	}
   575  	res := make([]interface{}, 0)
   576  	for _, item := range items {
   577  		convertedVal, err := convertReferenceValueToKRM(item, schema.Items)
   578  		if err != nil {
   579  			return "", nil, err
   580  		}
   581  		res = append(res, convertedVal)
   582  	}
   583  	return field, res, nil
   584  }
   585  
   586  func convertRegularReferenceToKRM(path []string, val interface{}, schema *openapi.Schema) (string, interface{}, error) {
   587  	field := pathslice.Base(path)
   588  	refField, err := extension.GetReferenceFieldName(path, schema)
   589  	if err != nil {
   590  		return "", nil, fmt.Errorf("error getting the reference field name %w", err)
   591  	}
   592  	convertedVal, err := convertReferenceValueToKRM(val, schema)
   593  	if err != nil {
   594  		return "", nil, fmt.Errorf("error converting reference value %v to KRM format for field %v: %w", val, field, err)
   595  	}
   596  	return refField, convertedVal, nil
   597  }
   598  
   599  func convertReferenceValueToKRM(val interface{}, schema *openapi.Schema) (interface{}, error) {
   600  	refConfigs, err := getDCLReferenceExtension(schema)
   601  	if err != nil {
   602  		return nil, fmt.Errorf("error getting DCL reference extension for reference value %v:  %w", val, err)
   603  	}
   604  	if len(refConfigs) >= 1 {
   605  		res := make(map[string]interface{})
   606  		res["external"] = val
   607  		return res, nil
   608  	}
   609  	return nil, fmt.Errorf("getting empty resource types list for reference value")
   610  }
   611  
   612  func getDCLReferenceExtension(schema *openapi.Schema) ([]interface{}, error) {
   613  	raw, ok := schema.Extension["x-dcl-references"]
   614  	if !ok {
   615  		return nil, fmt.Errorf("'x-dcl-references' extension is not defined")
   616  	}
   617  	refConfigs, ok := raw.([]interface{})
   618  	if !ok {
   619  		return nil, fmt.Errorf("wrong type for 'x-dcl-references' extension: %T, expect to have []interface{}", raw)
   620  	}
   621  	return refConfigs, nil
   622  }
   623  
   624  func resolveReferenceValue(obj map[string]interface{}, schema *openapi.Schema) (interface{}, error) {
   625  	if obj == nil {
   626  		return nil, nil
   627  	}
   628  	refConfigs, err := getDCLReferenceExtension(schema)
   629  	if err != nil {
   630  		return nil, fmt.Errorf("error getting DCL reference extension: %w", err)
   631  	}
   632  	if len(refConfigs) >= 1 {
   633  		return obj["external"], nil
   634  	}
   635  	return nil, fmt.Errorf("couldn't resolve the reference value from %v", obj)
   636  }
   637  
   638  func convertToDCLContainerField(obj *unstructured.Unstructured, r *dclunstruct.Resource, schema *openapi.Schema) error {
   639  	container, found, err := getContainerFieldName(schema)
   640  	if err != nil {
   641  		return fmt.Errorf("error getting the contianer field name %w", err)
   642  	}
   643  	if !found {
   644  		return nil
   645  	}
   646  	annotations := obj.GetAnnotations()
   647  	key := fmt.Sprintf("%s/%s-id", k8s.CNRMGroup, container)
   648  	containerID := annotations[key]
   649  	if containerID == "" {
   650  		return fmt.Errorf("couldn't resolve the value for container field %s", container)
   651  	}
   652  	r.Object[container] = containerID
   653  	return nil
   654  }
   655  
   656  func liftDCLContainerField(obj *unstructured.Unstructured, schema *openapi.Schema) error {
   657  	container, found, err := getContainerFieldName(schema)
   658  	if err != nil {
   659  		return fmt.Errorf("error getting the contianer field name %w", err)
   660  	}
   661  	if !found {
   662  		return nil
   663  	}
   664  	val, ok, err := unstructured.NestedString(obj.Object, "spec", container)
   665  	if err != nil || !ok {
   666  		return fmt.Errorf("couldn't get the value for container field %s: %w", container, err)
   667  	}
   668  	annotations := obj.GetAnnotations()
   669  	if annotations == nil {
   670  		annotations = make(map[string]string)
   671  	}
   672  	key := fmt.Sprintf("%s/%s-id", k8s.CNRMGroup, container)
   673  	annotations[key] = val
   674  	obj.SetAnnotations(annotations)
   675  	unstructured.RemoveNestedField(obj.Object, "spec", container)
   676  	return nil
   677  }
   678  
   679  func getContainerFieldName(schema *openapi.Schema) (string, bool, error) {
   680  	raw, ok := schema.Extension["x-dcl-parent-container"]
   681  	if !ok {
   682  		return "", false, nil
   683  	}
   684  	// DCL currently doesn't support resources that could have multiple container kinds.
   685  	container, ok := raw.(string)
   686  	if !ok {
   687  		return "", false, fmt.Errorf("wrong type for 'x-dcl-parent-container' extension: %T, expect to have string type", raw)
   688  	}
   689  	return container, true, nil
   690  }
   691  
   692  func liftDCLLabelsField(obj *unstructured.Unstructured, dclSchema *openapi.Schema) error {
   693  	labelsField, _, found, err := extension.GetLabelsFieldSchema(dclSchema)
   694  	if err != nil {
   695  		return fmt.Errorf("error getting DCL labels field : '%v'", err)
   696  	}
   697  	if !found {
   698  		return nil
   699  	}
   700  	//TODO(b/164208968): handle edge cases where 'labels' field is not at the top level
   701  	valMap, found, err := unstructured.NestedMap(obj.Object, "spec", labelsField)
   702  	if err != nil {
   703  		return fmt.Errorf("error getting labels %w", err)
   704  	}
   705  	if !found {
   706  		return nil
   707  	}
   708  	labels := make(map[string]string)
   709  	for k, v := range valMap {
   710  		labels[k] = v.(string)
   711  	}
   712  	obj.SetLabels(labels)
   713  	unstructured.RemoveNestedField(obj.Object, "spec", labelsField)
   714  	return nil
   715  }
   716  
   717  // The DCL schema use 'name' field to define the resource ID segment of the resource,
   718  // this function will convert it to the unified 'resourceID' field across KCC resources
   719  func convertToKRMResourceIDField(obj *unstructured.Unstructured, schema *openapi.Schema) error {
   720  	// If there is no 'name' field defined in the DCL schema, skip it.
   721  	// One example is DNSRecordSet resource.
   722  	s, found := extension.GetNameFieldSchema(schema)
   723  	if !found {
   724  		return nil
   725  	}
   726  	// If the 'name' field is read-only, skip it.
   727  	// This could happen to resources that don't have the 'name' field as a part of their URLs;
   728  	// however, the REST API returns a output-only `name` field in the response.
   729  	if s.ReadOnly {
   730  		return nil
   731  	}
   732  	val, found, err := unstructured.NestedString(obj.Object, "spec", "name")
   733  	if err != nil {
   734  		return fmt.Errorf("error getting the value of 'name' field: %w", err)
   735  	}
   736  	if !found {
   737  		return fmt.Errorf("'name' field is not found")
   738  	}
   739  	if err := unstructured.SetNestedField(obj.Object, val, "spec", "resourceID"); err != nil {
   740  		return fmt.Errorf("error setting resourceID field: %w", err)
   741  	}
   742  	unstructured.RemoveNestedField(obj.Object, "spec", "name")
   743  	return nil
   744  }
   745  
   746  // convertToDCLNameField converts 'resourceID' field in KCC to 'name' field in DCL
   747  func convertToDCLNameField(obj *unstructured.Unstructured, r *dclunstruct.Resource, schema *openapi.Schema) error {
   748  	// If there is no 'name' field defined in the DCL schema, skip it.
   749  	// One example is DNSRecordSet resource.
   750  	s, found := extension.GetNameFieldSchema(schema)
   751  	if !found {
   752  		return nil
   753  	}
   754  	// If the 'name' field is read-only, skip it.
   755  	// This could happen to resources that don't have the 'name' field as a part of their URLs;
   756  	// however, the REST API returns a output-only `name` field in the response.
   757  	if s.ReadOnly {
   758  		return nil
   759  	}
   760  
   761  	isServerGeneratedID, err := extension.IsResourceIDFieldServerGenerated(s)
   762  	if err != nil {
   763  		return fmt.Errorf("error parsing 'name' field schema: %w", err)
   764  	}
   765  	// convert 'resourceID' field to DCL's 'name' field
   766  	val, found, err := unstructured.NestedString(obj.UnstructuredContent(), "spec", "resourceID")
   767  	if err != nil {
   768  		return fmt.Errorf("error getting the value of %s: %w", k8s.ResourceIDFieldPath, err)
   769  	}
   770  	if !found {
   771  		// If the resource has a server-generated id, unspecified 'resourceID' field means creating a brand new resource.
   772  		// Leave 'name' field in DCL unspecified also.
   773  		if isServerGeneratedID {
   774  			return nil
   775  		}
   776  
   777  		// If the resource allows user-specified name, use metadata.name as default if 'resourceID' field is not specified in spec
   778  		val = obj.GetName()
   779  	}
   780  	if val == "" {
   781  		return fmt.Errorf("the resolved value for 'name' field is invalid: '' (empty string)")
   782  	}
   783  	r.Object["name"] = val
   784  	return nil
   785  }
   786  
   787  func convertToDCLLabelsField(obj *unstructured.Unstructured, r *dclunstruct.Resource, schema *openapi.Schema) error {
   788  	labelsField, _, _, err := extension.GetLabelsFieldSchema(schema)
   789  	if err != nil {
   790  		return fmt.Errorf("error getting DCL labels field: '%v'", err)
   791  	}
   792  	//TODO(b/164208968): handle edge cases where 'labels' field is not at the top level
   793  	if _, ok := schema.Properties[labelsField]; !ok {
   794  		return nil
   795  	}
   796  	labels := label.NewGCPLabelsFromK8SLabels(obj.GetLabels(), label.GetDefaultLabels())
   797  	if len(labels) != 0 {
   798  		r.Object[labelsField] = labels
   799  	}
   800  	return nil
   801  }
   802  
   803  func renameStatusFieldIfCollidesWithReservedName(path []string) []string {
   804  	reservedNames := k8s.ReservedStatusFieldNames()
   805  	if _, ok := reservedNames[path[0]]; ok {
   806  		path[0] = k8s.RenameStatusFieldWithReservedName(path[0])
   807  	}
   808  	return path
   809  }
   810  

View as plain text