...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/crd/crdgeneration/dcl2crdgeneration.go

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

     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 crdgeneration
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"log"
    21  	"strings"
    22  
    23  	corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
    24  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/crd/crdgeneration/crdboilerplate"
    25  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl"
    26  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/extension"
    27  	dclextension "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/extension"
    28  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/metadata"
    29  	dclmetatda "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/metadata"
    30  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/schema/dclschemaloader"
    31  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    32  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/text"
    33  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/pathslice"
    34  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/slice"
    35  
    36  	"github.com/nasa9084/go-openapi"
    37  	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    38  	"k8s.io/apimachinery/pkg/runtime/schema"
    39  )
    40  
    41  const (
    42  	Dcl2CRDLabel = "cnrm.cloud.google.com/dcl2crd"
    43  )
    44  
    45  var (
    46  	UnsupportedReferencedResource = fmt.Errorf("referenced resource is unsupported by KCC")
    47  )
    48  
    49  type DCL2CRDGenerator struct {
    50  	metadataLoader   dclmetatda.ServiceMetadataLoader
    51  	schemaLoader     dclschemaloader.DCLSchemaLoader
    52  	allSupportedGVKs []schema.GroupVersionKind
    53  }
    54  
    55  func New(metadataLoader dclmetatda.ServiceMetadataLoader, schemaLoader dclschemaloader.DCLSchemaLoader, allSupportedGVKs []schema.GroupVersionKind) *DCL2CRDGenerator {
    56  	return &DCL2CRDGenerator{
    57  		metadataLoader:   metadataLoader,
    58  		schemaLoader:     schemaLoader,
    59  		allSupportedGVKs: allSupportedGVKs,
    60  	}
    61  }
    62  
    63  // GenerateCRDFromOpenAPISchema returns a CustomResourceDefinition given the DCL OpenAPI schema
    64  func (a *DCL2CRDGenerator) GenerateCRDFromOpenAPISchema(schema *openapi.Schema, gvk schema.GroupVersionKind) (*apiextensions.CustomResourceDefinition, error) {
    65  	r, found := a.metadataLoader.GetResourceWithGVK(gvk)
    66  	if !found {
    67  		return nil, fmt.Errorf("ServiceMetadata for resource with GVK %v is not found", gvk)
    68  	}
    69  	openAPIV3Schema, err := a.generateOpenAPIV3Schema(schema, r)
    70  	if err != nil {
    71  		return nil, fmt.Errorf("error generating CRD schema for %v: %v", gvk.Kind, err)
    72  	}
    73  	crd := GetCustomResourceDefinition(gvk.Kind, gvk.Group, gvk.Version, openAPIV3Schema, Dcl2CRDLabel)
    74  	if r.DCLVersion == "alpha" {
    75  		crd.ObjectMeta.Labels[k8s.KCCStabilityLabel] = k8s.StabilityLevelAlpha
    76  	} else {
    77  		crd.ObjectMeta.Labels[k8s.KCCStabilityLabel] = k8s.StabilityLevelStable
    78  	}
    79  	return crd, nil
    80  }
    81  
    82  func (a *DCL2CRDGenerator) generateOpenAPIV3Schema(schema *openapi.Schema, resource metadata.Resource) (*apiextensions.JSONSchemaProps, error) {
    83  	var err error
    84  	crdSchema := crdboilerplate.GetOpenAPIV3SchemaSkeleton()
    85  	specJSONSchema, err := a.generateSpecJSONSchema(schema, resource)
    86  	if err != nil {
    87  		return nil, fmt.Errorf("error generating spec schema %w", err)
    88  	}
    89  	statusJSONSchema, err := generateStatusJSONSchema(schema)
    90  	if err != nil {
    91  		return nil, fmt.Errorf("error generating status schema %w", err)
    92  	}
    93  	if len(specJSONSchema.Properties) > 0 {
    94  		crdSchema.Properties["spec"] = *specJSONSchema
    95  		if len(specJSONSchema.Required) > 0 {
    96  			crdSchema.Required = slice.IncludeString(crdSchema.Required, "spec")
    97  		}
    98  	}
    99  	if statusJSONSchema != nil {
   100  		statusJSONSchema, err = k8s.RenameStatusFieldsWithReservedNames(statusJSONSchema)
   101  		if err != nil {
   102  			return nil, fmt.Errorf("error renaming status fields with reserved names: %v", err)
   103  		}
   104  		for k, v := range statusJSONSchema.Properties {
   105  			crdSchema.Properties["status"].Properties[k] = v
   106  		}
   107  	}
   108  	return crdSchema, nil
   109  }
   110  
   111  func (a *DCL2CRDGenerator) generateSpecJSONSchema(schema *openapi.Schema, resource metadata.Resource) (*apiextensions.JSONSchemaProps, error) {
   112  	var err error
   113  	if schema.Type != "object" {
   114  		return nil, fmt.Errorf("expect the entry level DCL OpenAPI schema to be object type, but got %v", schema.Type)
   115  	}
   116  	jsonSchema := &apiextensions.JSONSchemaProps{
   117  		Type:       "object",
   118  		Properties: make(map[string]apiextensions.JSONSchemaProps),
   119  	}
   120  	required := make([]string, 0)
   121  	dclLabelsField, _, dclLabelsFieldFound, err := extension.GetLabelsFieldSchema(schema)
   122  	if err != nil {
   123  		return nil, fmt.Errorf("error extracting DCL labels field schema: %v", err)
   124  	}
   125  	for k, v := range schema.Properties {
   126  		if !v.ReadOnly {
   127  			if k == "name" {
   128  				s, err := handleNameField(v)
   129  				if err != nil {
   130  					return nil, fmt.Errorf("error handling 'name' field: %w", err)
   131  				}
   132  				jsonSchema.Properties["resourceID"] = *s
   133  				continue
   134  			}
   135  			// TODO(b/164208968): handle edge cases where dclLabelsField field is not at the top level
   136  			if dclLabelsFieldFound && k == dclLabelsField {
   137  				continue
   138  			}
   139  			// TODO(b/186159460): Delete this if-block once all resources
   140  			// support hierarchical references.
   141  			if dcl.IsContainerField([]string{k}) && !resource.SupportsHierarchicalReferences {
   142  				continue
   143  			}
   144  			// Multi-type parent reference fields (i.e. "parent" fields) need
   145  			// to be split up into separate resource reference fields
   146  			if dcl.IsMultiTypeParentReferenceField([]string{k}) {
   147  				// TODO(b/186159460): Delete this if-block once all resources
   148  				// support hierarchical references.
   149  				if !resource.SupportsHierarchicalReferences {
   150  					return nil, fmt.Errorf("resource supports 'parent' field but doesn't support hierarchical references")
   151  				}
   152  				refs, err := a.multiTypeParentFieldToHierarchicalRefs(v)
   153  				if err != nil {
   154  					return nil, err
   155  				}
   156  				for fieldName, s := range refs {
   157  					s, err := prependImmutableToDescriptionIfImmutable(s, v)
   158  					if err != nil {
   159  						return nil, fmt.Errorf("error prepending Immutable to description of hierarchical reference field %v if field is immutable: %v", fieldName, err)
   160  					}
   161  					jsonSchema.Properties[fieldName] = *s
   162  				}
   163  				continue
   164  			}
   165  			fieldName, s, err := a.dclSchemaToSpecJSONSchema([]string{k}, v, false)
   166  			if err != nil {
   167  				return nil, err
   168  			}
   169  			if isRequiredField(schema, k) {
   170  				required = slice.IncludeString(required, fieldName)
   171  			}
   172  			jsonSchema.Properties[fieldName] = *s
   173  		}
   174  	}
   175  	if len(required) != 0 {
   176  		jsonSchema.Required = required
   177  	}
   178  	jsonSchema, err = a.addSchemaRulesForHierarchicalReferences(jsonSchema, schema, resource)
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  	return jsonSchema, nil
   183  }
   184  
   185  func generateStatusJSONSchema(schema *openapi.Schema) (*apiextensions.JSONSchemaProps, error) {
   186  	if schema.Type != "object" {
   187  		return nil, fmt.Errorf("expect the entry level DCL OpenAPI schema to be object type, but got %v", schema.Type)
   188  	}
   189  	return getStatusSchema(schema), nil
   190  }
   191  
   192  func getStatusSchema(schema *openapi.Schema) *apiextensions.JSONSchemaProps {
   193  	// we treat read-only fields as status fields
   194  	if schema.ReadOnly {
   195  		return dclSchemaToStatusJSONSchema(schema)
   196  	}
   197  	if schema.Type == "object" {
   198  		jsonSchema := &apiextensions.JSONSchemaProps{
   199  			Type:       "object",
   200  			Properties: make(map[string]apiextensions.JSONSchemaProps),
   201  		}
   202  		for k, v := range schema.Properties {
   203  			s := getStatusSchema(v)
   204  			if s != nil {
   205  				jsonSchema.Properties[k] = *s
   206  			}
   207  		}
   208  		if len(jsonSchema.Properties) == 0 {
   209  			return nil
   210  		}
   211  		return jsonSchema
   212  	}
   213  	// for now, don't split nested read-only fields in an array of objects into status
   214  	// those fields only make sense along with the whole object.
   215  	return nil
   216  }
   217  
   218  func dclSchemaToStatusJSONSchema(schema *openapi.Schema) *apiextensions.JSONSchemaProps {
   219  	jsonSchema := apiextensions.JSONSchemaProps{}
   220  	jsonSchema.Type = schema.Type
   221  	jsonSchema.Description = schema.Description
   222  	jsonSchema.Format = schema.Format
   223  
   224  	switch schema.Type {
   225  	case "object":
   226  		// The field additionalProperties is mutually exclusive with properties in CustomResourceDefinition
   227  		if schema.AdditionalProperties != nil {
   228  			jsonSchema.AdditionalProperties = &apiextensions.JSONSchemaPropsOrBool{
   229  				Schema: dclSchemaToStatusJSONSchema(schema.AdditionalProperties),
   230  			}
   231  			break
   232  		}
   233  		jsonSchema.Properties = make(map[string]apiextensions.JSONSchemaProps)
   234  		for k, v := range schema.Properties {
   235  			s := dclSchemaToStatusJSONSchema(v)
   236  			jsonSchema.Properties[k] = *s
   237  		}
   238  	case "array":
   239  		itemSchema := dclSchemaToStatusJSONSchema(schema.Items)
   240  		jsonSchema.Items = &apiextensions.JSONSchemaPropsOrArray{
   241  			Schema: itemSchema,
   242  		}
   243  	case "boolean", "number", "string", "integer":
   244  		jsonSchema.Type = schema.Type
   245  	default:
   246  		log.Fatalf("unknown schema type %v", schema.Type)
   247  	}
   248  	return &jsonSchema
   249  }
   250  
   251  func (a *DCL2CRDGenerator) dclSchemaToSpecJSONSchema(path []string, schema *openapi.Schema, isCollectionItemSchema bool) (string, *apiextensions.JSONSchemaProps, error) {
   252  	field := pathslice.Base(path)
   253  	if !schema.ReadOnly && extension.IsReferenceField(schema) {
   254  		refFieldName, err := extension.GetReferenceFieldName(path, schema)
   255  		if err != nil {
   256  			return "", nil, fmt.Errorf("error resolving the name for reference field %s: %w", field, err)
   257  		}
   258  		refSchema, err := a.handleReferenceField(path, schema)
   259  		if err != nil {
   260  			return "", nil, fmt.Errorf("error resolving the reference schema for field %v: %w", field, err)
   261  		}
   262  		refSchema, err = prependImmutableToDescriptionIfImmutable(refSchema, schema)
   263  		if err != nil {
   264  			return "", nil, fmt.Errorf("error prepending Immutable to description of field %v if field is immutable: %v", field, err)
   265  		}
   266  		return refFieldName, refSchema, nil
   267  	}
   268  	isSensitive, err := extension.IsSensitiveField(schema)
   269  	if err != nil {
   270  		return "", nil, fmt.Errorf("error checking sensitivity for field %v: %w", field, err)
   271  	}
   272  	if !schema.ReadOnly && isSensitive {
   273  		s := crdboilerplate.GetSensitiveFieldSchemaBoilerplate()
   274  		s.Description = schema.Description
   275  		jsonSchema, err := prependImmutableToDescriptionIfImmutable(&s, schema)
   276  		if err != nil {
   277  			return "", nil, fmt.Errorf("error prepending Immutable to description of field %v if field is immutable: %v", field, err)
   278  		}
   279  		return field, jsonSchema, nil
   280  	}
   281  	jsonSchema := &apiextensions.JSONSchemaProps{}
   282  	jsonSchema.Type = schema.Type
   283  	jsonSchema.Description = schema.Description
   284  	jsonSchema.Format = schema.Format
   285  
   286  	jsonSchema, err = prependImmutableToDescriptionIfImmutable(jsonSchema, schema)
   287  	if err != nil {
   288  		return "", nil, fmt.Errorf("error prepending Immutable to description of field %v if field is immutable: %v", field, err)
   289  	}
   290  
   291  	fieldName := field
   292  	switch schema.Type {
   293  	case "object":
   294  		// The field additionalProperties is mutually exclusive with properties in CustomResourceDefinition
   295  		if schema.AdditionalProperties != nil {
   296  			_, s, err := a.dclSchemaToSpecJSONSchema(path, schema.AdditionalProperties, true)
   297  			if err != nil {
   298  				return "", nil, err
   299  			}
   300  			jsonSchema.AdditionalProperties = &apiextensions.JSONSchemaPropsOrBool{
   301  				Schema: s,
   302  			}
   303  			break
   304  		}
   305  		jsonSchema.Properties = make(map[string]apiextensions.JSONSchemaProps)
   306  		required := make([]string, 0)
   307  		for k, v := range schema.Properties {
   308  			if !v.ReadOnly || isCollectionItemSchema {
   309  				fieldName, s, err := a.dclSchemaToSpecJSONSchema(append(path, k), v, isCollectionItemSchema)
   310  				if err != nil {
   311  					return "", nil, err
   312  				}
   313  				jsonSchema.Properties[fieldName] = *s
   314  				if isRequiredField(schema, k) {
   315  					required = slice.IncludeString(required, fieldName)
   316  				}
   317  			}
   318  		}
   319  		if len(required) != 0 {
   320  			jsonSchema.Required = required
   321  		}
   322  	case "array":
   323  		f, itemSchema, err := a.dclSchemaToSpecJSONSchema(path, schema.Items, true)
   324  		if err != nil {
   325  			return "", nil, err
   326  		}
   327  		fieldName = f
   328  		jsonSchema.Items = &apiextensions.JSONSchemaPropsOrArray{
   329  			Schema: itemSchema,
   330  		}
   331  	case "boolean", "number", "string", "integer":
   332  		jsonSchema.Type = schema.Type
   333  	default:
   334  		log.Fatalf("unknown schema type %v for field %v", schema.Type, field)
   335  	}
   336  	return fieldName, jsonSchema, nil
   337  }
   338  
   339  func (a *DCL2CRDGenerator) multiTypeParentFieldToHierarchicalRefs(schema *openapi.Schema) (map[string]*apiextensions.JSONSchemaProps, error) {
   340  	tcs, err := dcl.GetReferenceTypeConfigs(schema, a.metadataLoader)
   341  	if err != nil {
   342  		return nil, fmt.Errorf("error getting reference type configs for DCL field 'parent': %w", err)
   343  	}
   344  
   345  	keys := make([]string, 0)
   346  	for _, tc := range tcs {
   347  		keys = append(keys, tc.Key)
   348  	}
   349  
   350  	hierarchicalRefs := make(map[string]*apiextensions.JSONSchemaProps)
   351  	for _, tc := range tcs {
   352  		s, err := a.resolveResourceReferenceJSONSchemaPerType(&tc, "")
   353  		if err != nil {
   354  			return nil, err
   355  		}
   356  		s.Description = fmt.Sprintf("The %v that this resource belongs to. Only one of [%v] may be specified.", tc.GVK.Kind, strings.Join(keys, ", "))
   357  		hierarchicalRefs[tc.Key] = s
   358  	}
   359  	return hierarchicalRefs, nil
   360  }
   361  
   362  func (a *DCL2CRDGenerator) handleReferenceField(path []string, schema *openapi.Schema) (*apiextensions.JSONSchemaProps, error) {
   363  	if dcl.IsSingleTypeParentReferenceField(path) {
   364  		return a.handleSingleTypeParentReferenceField(path, schema)
   365  	}
   366  	if schema.Type == "array" {
   367  		return a.handleListOfReferencesField(schema)
   368  	}
   369  	return a.resolveResourceReferenceJSONSchema(schema)
   370  }
   371  
   372  func (a *DCL2CRDGenerator) handleSingleTypeParentReferenceField(path []string, schema *openapi.Schema) (*apiextensions.JSONSchemaProps, error) {
   373  	field := pathslice.Base(path)
   374  	refSchema, err := a.resolveResourceReferenceJSONSchema(schema)
   375  	if err != nil {
   376  		return nil, err
   377  	}
   378  	refSchema.Description = fmt.Sprintf("The %v that this resource belongs to.", strings.Title(field))
   379  	return refSchema, nil
   380  }
   381  
   382  func (a *DCL2CRDGenerator) handleListOfReferencesField(schema *openapi.Schema) (*apiextensions.JSONSchemaProps, error) {
   383  	refSchema, err := a.resolveResourceReferenceJSONSchema(schema.Items)
   384  	if err != nil {
   385  		return nil, err
   386  	}
   387  	res := &apiextensions.JSONSchemaProps{
   388  		Type: "array",
   389  		Items: &apiextensions.JSONSchemaPropsOrArray{
   390  			Schema: refSchema,
   391  		},
   392  	}
   393  	return res, nil
   394  }
   395  
   396  func handleNameField(schema *openapi.Schema) (*apiextensions.JSONSchemaProps, error) {
   397  	isServerGenerated, err := extension.IsResourceIDFieldServerGenerated(schema)
   398  	if err != nil {
   399  		return nil, err
   400  	}
   401  
   402  	var description string
   403  	if isServerGenerated {
   404  		description = GenerateResourceIDFieldDescription("name", true)
   405  	} else {
   406  		description = GenerateResourceIDFieldDescription("name", false)
   407  	}
   408  
   409  	return &apiextensions.JSONSchemaProps{
   410  		Type:        schema.Type,
   411  		Description: description,
   412  	}, nil
   413  }
   414  
   415  func (a *DCL2CRDGenerator) resolveResourceReferenceJSONSchema(schema *openapi.Schema) (*apiextensions.JSONSchemaProps, error) {
   416  	tcs, err := dcl.GetReferenceTypeConfigs(schema, a.metadataLoader)
   417  	if err != nil {
   418  		return nil, err
   419  	}
   420  
   421  	// We name reference field as {fieldName}Ref.
   422  	if len(tcs) == 1 {
   423  		refSchema, err := a.resolveResourceReferenceJSONSchemaPerType(&tcs[0], schema.Description)
   424  		return refSchema, err
   425  	} else {
   426  		refSchema, err := a.resolveResourceReferenceJSONSchemaMultiTypes(tcs, schema.Description)
   427  		return refSchema, err
   428  	}
   429  }
   430  
   431  func (a *DCL2CRDGenerator) resolveResourceReferenceJSONSchemaPerType(tc *corekccv1alpha1.TypeConfig, description string) (*apiextensions.JSONSchemaProps, error) {
   432  	supported, err := a.validateReferencedResourceKind(tc)
   433  	if err != nil {
   434  		return nil, err
   435  	}
   436  	externalRefDescription, err := a.getDescriptionForExternalRef(tc, description)
   437  	if err != nil {
   438  		return nil, err
   439  	}
   440  	refSchema := crdboilerplate.GetResourceReferenceSchemaBoilerplate(externalRefDescription)
   441  	if !supported {
   442  		MarkReferencedKindsNotSupported(refSchema, []string{tc.GVK.Kind})
   443  	}
   444  	return refSchema, nil
   445  }
   446  
   447  func (a *DCL2CRDGenerator) resolveResourceReferenceJSONSchemaMultiTypes(tcs []corekccv1alpha1.TypeConfig, description string) (*apiextensions.JSONSchemaProps, error) {
   448  	supportedKinds := make([]string, 0)
   449  	unsupportedKinds := make([]string, 0)
   450  	for _, tc := range tcs {
   451  		supported, err := a.validateReferencedResourceKind(&tc)
   452  		if err != nil {
   453  			return nil, err
   454  		}
   455  		if !supported {
   456  			unsupportedKinds = append(unsupportedKinds, tc.GVK.Kind)
   457  		} else {
   458  			supportedKinds = append(supportedKinds, tc.GVK.Kind)
   459  		}
   460  	}
   461  	externalRefDescription, err := a.getDescriptionForMultiKindExternalRef(tcs, description)
   462  	if err != nil {
   463  		return nil, err
   464  	}
   465  	refSchema := crdboilerplate.GetMultiKindResourceReferenceSchemaBoilerplate(externalRefDescription, supportedKinds)
   466  	if len(unsupportedKinds) > 0 {
   467  		MarkReferencedKindsNotSupported(refSchema, unsupportedKinds)
   468  	}
   469  	return refSchema, nil
   470  }
   471  
   472  func (a *DCL2CRDGenerator) validateReferencedResourceKind(tc *corekccv1alpha1.TypeConfig) (supported bool, err error) {
   473  	if !k8s.GVKListContains(a.allSupportedGVKs, tc.GVK) {
   474  		return false, nil
   475  	}
   476  	// On runtime, we need to load the DCL schema for the referenced resource type to resolve the standardized resource name
   477  	if tc.TargetField == "name" {
   478  		_, err := dclschemaloader.GetDCLSchemaForGVK(tc.GVK, a.metadataLoader, a.schemaLoader)
   479  		if err != nil {
   480  			return false, fmt.Errorf("error getting the DCL schema for %v: %w; if it's a supported tf-based resource type, "+
   481  				"ensure that it has been declared in pkg/dcl/metadata/metadata.go with releasable flag as false, "+
   482  				"and that its service is imported in pkg/dcl/schema/dclschemaloader/dclschemaloader.go, "+
   483  				"since we need to load its OpenAPI schema for 'x-dcl-id' template", tc.GVK, err)
   484  		}
   485  	}
   486  	return true, nil
   487  }
   488  
   489  func isRequiredField(schema *openapi.Schema, field string) bool {
   490  	for _, item := range schema.Required {
   491  		if field == item {
   492  			return true
   493  		}
   494  	}
   495  	return false
   496  }
   497  
   498  func (a *DCL2CRDGenerator) addSchemaRulesForHierarchicalReferences(jsonSchema *apiextensions.JSONSchemaProps, schema *openapi.Schema, resource metadata.Resource) (*apiextensions.JSONSchemaProps, error) {
   499  	// TODO(b/186159460): Delete this if-block once all resources support
   500  	// hierarchical references.
   501  	if !resource.SupportsHierarchicalReferences {
   502  		return jsonSchema, nil
   503  	}
   504  	hierarchicalRefs, err := dcl.GetHierarchicalReferenceConfigFromDCLSchema(schema, a.metadataLoader)
   505  	if err != nil {
   506  		return nil, fmt.Errorf("error getting hierarchical reference config for resource: %w", err)
   507  	}
   508  	if resource.SupportsContainerAnnotations {
   509  		// If resource supports resource-level container annotations, mark
   510  		// hierarchical references optional since users can use the annotations
   511  		// to configure the references.
   512  		return MarkHierarchicalReferencesOptionalButMutuallyExclusive(jsonSchema, hierarchicalRefs), nil
   513  	}
   514  	return MarkHierarchicalReferencesRequiredButMutuallyExclusive(jsonSchema, hierarchicalRefs), nil
   515  }
   516  
   517  func (a *DCL2CRDGenerator) getDescriptionForExternalRef(tc *corekccv1alpha1.TypeConfig, baseDescription string) (string, error) {
   518  	exampleAllowedValue, err := a.getExampleAllowedValueForExternalRef(tc)
   519  	if err != nil {
   520  		if errors.Is(err, UnsupportedReferencedResource) {
   521  			return baseDescription, nil
   522  		}
   523  		return "", err
   524  	}
   525  	return text.AppendStrAsNewParagraph(
   526  		baseDescription,
   527  		fmt.Sprintf("Allowed value: %v", exampleAllowedValue),
   528  	), nil
   529  }
   530  
   531  func (a *DCL2CRDGenerator) getDescriptionForMultiKindExternalRef(tcs []corekccv1alpha1.TypeConfig, baseDescription string) (string, error) {
   532  	exampleAllowedValues := make([]string, 0)
   533  	for _, tc := range tcs {
   534  		v, err := a.getExampleAllowedValueForExternalRef(&tc)
   535  		if err != nil {
   536  			if errors.Is(err, UnsupportedReferencedResource) {
   537  				continue
   538  			}
   539  			return "", err
   540  		}
   541  		exampleAllowedValues = append(exampleAllowedValues, v)
   542  	}
   543  	if len(exampleAllowedValues) == 0 {
   544  		return baseDescription, nil
   545  	}
   546  	return text.AppendStrAsNewParagraph(
   547  		baseDescription,
   548  		fmt.Sprintf("Allowed values:\n* %v", strings.Join(exampleAllowedValues, "\n* ")),
   549  	), nil
   550  }
   551  
   552  func (a *DCL2CRDGenerator) getExampleAllowedValueForExternalRef(tc *corekccv1alpha1.TypeConfig) (string, error) {
   553  	// Cannot programmatically determine values allowed by `external` if the
   554  	// referenced resource is not yet supported by KCC.
   555  	if !k8s.GVKListContains(a.allSupportedGVKs, tc.GVK) {
   556  		switch tc.GVK.Kind {
   557  		default:
   558  			return "", UnsupportedReferencedResource
   559  		// For some resources, `external` allows the same value regardless of
   560  		// the referencing resource, allowing us to just hardcode an allowed
   561  		// value.
   562  		case "Organization":
   563  			return "The Google Cloud resource name of a Google Cloud Organization (format: `organizations/{{name}}`).", nil
   564  		case "BillingAccount":
   565  			return "The Google Cloud resource name of a Google Cloud Billing Account (format: `billingAccounts/{{name}}`).", nil
   566  		}
   567  	}
   568  
   569  	article := text.IndefiniteArticleFor(tc.GVK.Kind)
   570  	switch tc.TargetField {
   571  	case "":
   572  		return "", fmt.Errorf("reference field unexpectedly does not have a target field specified")
   573  	case "name":
   574  		s, err := dclschemaloader.GetDCLSchemaForGVK(tc.GVK, a.metadataLoader, a.schemaLoader)
   575  		if err != nil {
   576  			return "", fmt.Errorf("error getting DCL schema for GVK %v: %w", tc.GVK, err)
   577  		}
   578  		template, err := dclextension.GetNameValueTemplate(s)
   579  		if err != nil {
   580  			return "", fmt.Errorf("error getting name value template for GVK %v: %w", tc.GVK, err)
   581  		}
   582  		return fmt.Sprintf("The Google Cloud resource name of %v `%v` resource (format: `%v`).", article, tc.GVK.Kind, template), nil
   583  	default:
   584  		return fmt.Sprintf("The `%v` field of %v `%v` resource.", tc.TargetField, article, tc.GVK.Kind), nil
   585  	}
   586  }
   587  
   588  func prependImmutableToDescriptionIfImmutable(jsonSchema *apiextensions.JSONSchemaProps, schema *openapi.Schema) (*apiextensions.JSONSchemaProps, error) {
   589  	jsonSchemaCopy := jsonSchema.DeepCopy()
   590  	ok, err := dclextension.IsImmutableField(schema)
   591  	if err != nil {
   592  		return nil, fmt.Errorf("error determining if field is immutable: %v", err)
   593  	}
   594  	if ok {
   595  		jsonSchemaCopy.Description = strings.TrimSpace("Immutable. " + jsonSchemaCopy.Description)
   596  	}
   597  	return jsonSchemaCopy, nil
   598  }
   599  

View as plain text