...

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

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

     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 dcl
    16  
    17  import (
    18  	"fmt"
    19  	"strings"
    20  
    21  	corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
    22  	dclmetadata "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/metadata"
    23  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/schema/dclschemaloader"
    24  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    25  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/text"
    26  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util"
    27  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/pathslice"
    28  
    29  	"github.com/nasa9084/go-openapi"
    30  	"k8s.io/apimachinery/pkg/runtime/schema"
    31  )
    32  
    33  var (
    34  	// The following variables represent the fields that are used to reference
    35  	// parent resources. DCL resources can support either one multi-type parent
    36  	// reference field or one of the single-type parent reference fields.
    37  	multiTypeParentReferenceField              = "parent"
    38  	singleTypeParentReferenceFieldProject      = "project"
    39  	singleTypeParentReferenceFieldFolder       = "folder"
    40  	singleTypeParentReferenceFieldOrganization = "organization"
    41  	singleTypeParentReferenceFields            = map[string]bool{
    42  		singleTypeParentReferenceFieldProject:      true,
    43  		singleTypeParentReferenceFieldFolder:       true,
    44  		singleTypeParentReferenceFieldOrganization: true,
    45  	}
    46  
    47  	// The following variables represent the different hierarchical reference
    48  	// configurations that can be had by DCL-based KCC resources.
    49  	hierarchicalReferenceProject = corekccv1alpha1.HierarchicalReference{
    50  		Key:  "projectRef",
    51  		Type: corekccv1alpha1.HierarchicalReferenceTypeProject,
    52  	}
    53  	hierarchicalReferenceFolder = corekccv1alpha1.HierarchicalReference{
    54  		Key:  "folderRef",
    55  		Type: corekccv1alpha1.HierarchicalReferenceTypeFolder,
    56  	}
    57  	hierarchicalReferenceOrganization = corekccv1alpha1.HierarchicalReference{
    58  		Key:  "organizationRef",
    59  		Type: corekccv1alpha1.HierarchicalReferenceTypeOrganization,
    60  	}
    61  	hierarchicalReferenceBillingAccount = corekccv1alpha1.HierarchicalReference{
    62  		Key:  "billingAccountRef",
    63  		Type: corekccv1alpha1.HierarchicalReferenceTypeBillingAccount,
    64  	}
    65  )
    66  
    67  /*
    68  *
    69  dclReferenceExtensionElem represents an element in a 'x-dcl-references' list.
    70  ```
    71  
    72  	x-dcl-references:
    73  	- field: name
    74  	  parent: true
    75  	  resource: SomeService/SomeResourceType
    76  
    77  ```
    78  *
    79  */
    80  type dclReferenceExtensionElem struct {
    81  	// Resource indicates the referenced resource type in the format: Service/ResourceKind, e.g. Compute/BackendBucket.
    82  	Resource string `json:"resource"`
    83  	// Field is the referenced resource's field from which to extract the desired value, e.g. "name", "selfLink"
    84  	Field string `json:"field"`
    85  	// Parent specifies whether the referenced resource is a parent. If the parent
    86  	// is successfully deleted, it's assumed that this resource may be deleted without any call to the
    87  	// underlying API.
    88  	Parent bool `json:"parent,omitempty"`
    89  }
    90  
    91  func GetReferenceTypeConfigs(schema *openapi.Schema, smLoader dclmetadata.ServiceMetadataLoader) ([]corekccv1alpha1.TypeConfig, error) {
    92  	refExtensionElems, err := getDCLReferenceExtensionElems(schema)
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  	res := make([]corekccv1alpha1.TypeConfig, 0)
    97  	for _, e := range refExtensionElems {
    98  		tc, err := toTypeConfig(e, smLoader)
    99  		if err != nil {
   100  			return nil, err
   101  		}
   102  		res = append(res, *tc)
   103  	}
   104  	return res, nil
   105  }
   106  
   107  // ToTypeConfig converts a 'x-dcl-references' element to a TypeConfig.
   108  func ToTypeConfig(rawElem map[interface{}]interface{}, smLoader dclmetadata.ServiceMetadataLoader) (*corekccv1alpha1.TypeConfig, error) {
   109  	e, err := toDCLReferenceExtensionElem(rawElem)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  	return toTypeConfig(e, smLoader)
   114  }
   115  
   116  func toTypeConfig(e dclReferenceExtensionElem, smLoader dclmetadata.ServiceMetadataLoader) (*corekccv1alpha1.TypeConfig, error) {
   117  	if err := validateDCLReferenceExtensionElem(e); err != nil {
   118  		return nil, err
   119  	}
   120  	tc := &corekccv1alpha1.TypeConfig{}
   121  	tc.TargetField = e.Field
   122  	tc.Parent = e.Parent
   123  	refGVK, err := getReferenceGVK(e.Resource, smLoader)
   124  	if err != nil {
   125  		return nil, fmt.Errorf("error resolving the GVK for referenced resource: %w", err)
   126  	}
   127  	tc.GVK = refGVK
   128  	tc.Key = text.LowercaseInitial(k8s.KindWithoutServicePrefix(tc.GVK)) + "Ref"
   129  	return tc, nil
   130  }
   131  
   132  func validateDCLReferenceExtensionElem(e dclReferenceExtensionElem) error {
   133  	if e.Resource == "" {
   134  		return fmt.Errorf("required 'resource' attribute is not specified in 'x-dcl-references' extension")
   135  	}
   136  	if e.Field == "" {
   137  		return fmt.Errorf("required 'field' attribute is not specified in 'x-dcl-references' extension")
   138  	}
   139  	return nil
   140  }
   141  
   142  func convertToStringInterfaceMap(in map[interface{}]interface{}) (map[string]interface{}, error) {
   143  	out := make(map[string]interface{})
   144  	for k, v := range in {
   145  		s, ok := k.(string)
   146  		if !ok {
   147  			return nil, fmt.Errorf("wrong type for the key: %T, expect to have string", k)
   148  		}
   149  		out[s] = v
   150  	}
   151  	return out, nil
   152  }
   153  
   154  func getReferenceGVK(resource string, smLoader dclmetadata.ServiceMetadataLoader) (schema.GroupVersionKind, error) {
   155  	// The 'resource' attribute of a 'x-dcl-references' element has the format
   156  	// "Service/DCLType" (e.g. Compute/BackendBucket)
   157  	components := strings.Split(resource, "/")
   158  	if len(components) != 2 {
   159  		return schema.GroupVersionKind{}, fmt.Errorf("invalid format for 'resource' attribute in 'x-dcl-references' extension: %v", resource)
   160  	}
   161  	service := components[0]
   162  	dclType := components[1]
   163  
   164  	sm, found := smLoader.GetServiceMetadata(service)
   165  	if !found {
   166  		return schema.GroupVersionKind{}, fmt.Errorf("ServiceMetadata for service %v is not found", service)
   167  	}
   168  	r, found := sm.GetResourceWithType(dclType)
   169  	if !found {
   170  		return schema.GroupVersionKind{}, fmt.Errorf("resource with DCL type %v not supported in service %v", dclType, service)
   171  	}
   172  	return dclmetadata.GVKForResource(sm, r), nil
   173  }
   174  
   175  func getDCLReferenceExtensionElems(schema *openapi.Schema) ([]dclReferenceExtensionElem, error) {
   176  	extension, ok := schema.Extension["x-dcl-references"]
   177  	if !ok {
   178  		return nil, fmt.Errorf("no 'x-dcl-references' extension found")
   179  	}
   180  	extensionAsList, ok := extension.([]interface{})
   181  	if !ok {
   182  		return nil, fmt.Errorf("wrong type for 'x-dcl-references' extension: %T, expect to have []interface{}", extension)
   183  	}
   184  	res := make([]dclReferenceExtensionElem, 0)
   185  	for _, elem := range extensionAsList {
   186  		elemAsMap, ok := elem.(map[interface{}]interface{})
   187  		if !ok {
   188  			return nil, fmt.Errorf("wrong type for element in 'x-dcl-references' extension %T, expect to have map[interface{}]interface{}", elem)
   189  		}
   190  		e, err := toDCLReferenceExtensionElem(elemAsMap)
   191  		if err != nil {
   192  			return nil, err
   193  		}
   194  		res = append(res, e)
   195  	}
   196  	return res, nil
   197  }
   198  
   199  func toDCLReferenceExtensionElem(rawElem map[interface{}]interface{}) (dclReferenceExtensionElem, error) {
   200  	m, err := convertToStringInterfaceMap(rawElem)
   201  	if err != nil {
   202  		return dclReferenceExtensionElem{}, fmt.Errorf("error converting 'x-dcl-references' element to map[string]interface{}: %v", err)
   203  	}
   204  	e := dclReferenceExtensionElem{}
   205  	if err := util.Marshal(m, &e); err != nil {
   206  		return dclReferenceExtensionElem{}, fmt.Errorf("error marshalling 'x-dcl-references' element to struct: %v", err)
   207  	}
   208  	return e, nil
   209  }
   210  
   211  func GetHierarchicalReferencesForGVK(gvk schema.GroupVersionKind, smLoader dclmetadata.ServiceMetadataLoader, schemaLoader dclschemaloader.DCLSchemaLoader) ([]corekccv1alpha1.HierarchicalReference, error) {
   212  	r, found := smLoader.GetResourceWithGVK(gvk)
   213  	if !found {
   214  		return nil, fmt.Errorf("ServiceMetadata for resource with GroupVersionKind %v not found", gvk)
   215  	}
   216  	// TODO(b/186159460): Delete this if-block once all resources support
   217  	// hierarchical references.
   218  	if !r.SupportsHierarchicalReferences {
   219  		return nil, nil
   220  	}
   221  	stv, err := dclmetadata.ToServiceTypeVersion(gvk, smLoader)
   222  	if err != nil {
   223  		return nil, fmt.Errorf("error getting DCL ServiceTypeVersion for GroupVersionKind %v: %w", gvk, err)
   224  	}
   225  	dclSchema, err := schemaLoader.GetDCLSchema(stv)
   226  	if err != nil {
   227  		return nil, fmt.Errorf("error getting the DCL Schema for GroupVersionKind %v: %w", gvk, err)
   228  	}
   229  	hierarchicalRefs, err := GetHierarchicalReferenceConfigFromDCLSchema(dclSchema, smLoader)
   230  	if err != nil {
   231  		return nil, fmt.Errorf("error resolving the hierarchical reference config from DCL schema for GroupVersionKind %v: %w", gvk, err)
   232  	}
   233  	return hierarchicalRefs, nil
   234  }
   235  
   236  func GetHierarchicalReferenceConfigFromDCLSchema(schema *openapi.Schema, smLoader dclmetadata.ServiceMetadataLoader) ([]corekccv1alpha1.HierarchicalReference, error) {
   237  	// Resource supports multiple parent types
   238  	if SupportsMultipleParentTypes(schema) {
   239  		res, err := GetHierarchicalReferenceConfigForMultiParentResource(schema, smLoader)
   240  		if err != nil {
   241  			return nil, fmt.Errorf("error getting hierarchical reference config for resource that supports multiple parent types: %w", err)
   242  		}
   243  		return res, nil
   244  	}
   245  
   246  	// Resource supports one parent type or none at all
   247  	field, err := getSingleTypeParentReferenceField(schema)
   248  	if err != nil {
   249  		return nil, fmt.Errorf("error getting single-type parent reference field for resource: %w", err)
   250  	}
   251  	if field == "" {
   252  		// Resource doesn't support any parent reference fields
   253  		return nil, nil
   254  	}
   255  	switch field {
   256  	case singleTypeParentReferenceFieldProject:
   257  		return []corekccv1alpha1.HierarchicalReference{hierarchicalReferenceProject}, nil
   258  	case singleTypeParentReferenceFieldFolder:
   259  		return []corekccv1alpha1.HierarchicalReference{hierarchicalReferenceFolder}, nil
   260  	case singleTypeParentReferenceFieldOrganization:
   261  		return []corekccv1alpha1.HierarchicalReference{hierarchicalReferenceOrganization}, nil
   262  	default:
   263  		panic(fmt.Errorf("unrecognized single-type parent reference field: %v", field))
   264  	}
   265  }
   266  
   267  func GetHierarchicalReferenceConfigForMultiParentResource(schema *openapi.Schema, smLoader dclmetadata.ServiceMetadataLoader) ([]corekccv1alpha1.HierarchicalReference, error) {
   268  	if !SupportsMultipleParentTypes(schema) {
   269  		return nil, fmt.Errorf("resource does not support multiple parent types")
   270  	}
   271  
   272  	parentFieldSchema := schema.Properties["parent"]
   273  	tcs, err := GetReferenceTypeConfigs(parentFieldSchema, smLoader)
   274  	if err != nil {
   275  		return nil, fmt.Errorf("error getting reference type configs for DCL field 'parent': %w", err)
   276  	}
   277  
   278  	res := make([]corekccv1alpha1.HierarchicalReference, 0)
   279  	for _, tc := range tcs {
   280  		switch tc.GVK.Kind {
   281  		case "Project":
   282  			res = append(res, hierarchicalReferenceProject)
   283  		case "Folder":
   284  			res = append(res, hierarchicalReferenceFolder)
   285  		case "Organization":
   286  			res = append(res, hierarchicalReferenceOrganization)
   287  		case "BillingAccount":
   288  			res = append(res, hierarchicalReferenceBillingAccount)
   289  		default:
   290  			panic(fmt.Errorf("'parent' field references an unsupported resource kind: %v", tc.GVK.Kind))
   291  		}
   292  	}
   293  	return res, nil
   294  }
   295  
   296  func getSingleTypeParentReferenceField(schema *openapi.Schema) (string, error) {
   297  	if SupportsMultipleParentTypes(schema) {
   298  		return "", fmt.Errorf("resource supports multiple parent types, not one")
   299  	}
   300  	var res string
   301  	for f := range singleTypeParentReferenceFields {
   302  		_, ok := schema.Properties[f]
   303  		if !ok {
   304  			continue
   305  		}
   306  		if res != "" {
   307  			return "", fmt.Errorf("resource unexpectedly has more than one single-type parent reference field")
   308  		}
   309  		res = f
   310  	}
   311  	return res, nil
   312  }
   313  
   314  func ParentReferenceFields() []string {
   315  	return []string{
   316  		singleTypeParentReferenceFieldProject,
   317  		singleTypeParentReferenceFieldFolder,
   318  		singleTypeParentReferenceFieldOrganization,
   319  		multiTypeParentReferenceField,
   320  	}
   321  }
   322  
   323  func IsParentReferenceField(path []string) bool {
   324  	return IsSingleTypeParentReferenceField(path) ||
   325  		IsMultiTypeParentReferenceField(path)
   326  }
   327  
   328  func IsSingleTypeParentReferenceField(path []string) bool {
   329  	if len(path) > 1 {
   330  		return false
   331  	}
   332  	field := pathslice.Base(path)
   333  	_, ok := singleTypeParentReferenceFields[field]
   334  	return ok
   335  }
   336  
   337  func IsMultiTypeParentReferenceField(path []string) bool {
   338  	if len(path) > 1 {
   339  		return false
   340  	}
   341  	field := pathslice.Base(path)
   342  	return field == multiTypeParentReferenceField
   343  }
   344  
   345  func SupportsMultipleParentTypes(schema *openapi.Schema) bool {
   346  	_, ok := schema.Properties[multiTypeParentReferenceField]
   347  	return ok
   348  }
   349  
   350  // GetHierarchicalRefFromConfigForMultiParentResource gets the value and
   351  // TypeConfig of the hierarchical reference in the KRM config, assuming that
   352  // the config is for a multi-parent resource (i.e. supports more than one
   353  // hierarchical reference). Returns nil if no hierarchical reference is found.
   354  // Returns an error if multiple are found.
   355  func GetHierarchicalRefFromConfigForMultiParentResource(config map[string]interface{}, schema *openapi.Schema,
   356  	smLoader dclmetadata.ServiceMetadataLoader) (interface{}, *corekccv1alpha1.TypeConfig, error) {
   357  	tcs, err := GetReferenceTypeConfigs(schema, smLoader)
   358  	if err != nil {
   359  		return nil, nil, fmt.Errorf("error getting reference type configs: %w", err)
   360  	}
   361  	var rawVal interface{}
   362  	var typeConfig corekccv1alpha1.TypeConfig
   363  	for _, tc := range tcs {
   364  		if v, ok := config[tc.Key]; ok && v != nil {
   365  			if rawVal != nil {
   366  				return nil, nil, fmt.Errorf("multiple hierarchical references found in config: %v and %v", typeConfig.Key, tc.Key)
   367  			}
   368  			rawVal = v
   369  			typeConfig = tc
   370  		}
   371  	}
   372  	if rawVal == nil {
   373  		return nil, nil, nil
   374  	}
   375  	return rawVal, &typeConfig, nil
   376  }
   377  
   378  // ParentPrefixForKind gets the parent prefix for the given kind (e.g.
   379  // "Project" => "projects/"). This is used for setting/parsing values of
   380  // multi-type parent reference fields in DCL which require a parent
   381  // prefix to denote the parent type.
   382  func ParentPrefixForKind(kind string) string {
   383  	switch kind {
   384  	case "Project", "Folder", "Organization", "BillingAccount":
   385  		return fmt.Sprintf("%vs/", text.LowercaseInitial(kind))
   386  	default:
   387  		panic(fmt.Errorf("tried to get parent prefix for kind %v which is not recognized as a hierarchical resource", kind))
   388  	}
   389  }
   390  

View as plain text