1
2
3
4
5
6
7
8
9
10
11
12
13
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
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
136 if dclLabelsFieldFound && k == dclLabelsField {
137 continue
138 }
139
140
141 if dcl.IsContainerField([]string{k}) && !resource.SupportsHierarchicalReferences {
142 continue
143 }
144
145
146 if dcl.IsMultiTypeParentReferenceField([]string{k}) {
147
148
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
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
214
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
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
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
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
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
500
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
510
511
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
554
555 if !k8s.GVKListContains(a.allSupportedGVKs, tc.GVK) {
556 switch tc.GVK.Kind {
557 default:
558 return "", UnsupportedReferencedResource
559
560
561
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