1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package crdgeneration
16
17 import (
18 "fmt"
19 "strings"
20
21 corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
22 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/crd/crdgeneration/crdboilerplate"
23 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
24 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/krmtotf"
25 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/text"
26 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/slice"
27
28 "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
29 "github.com/hashicorp/terraform-provider-google-beta/google-beta"
30 apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
31 )
32
33 const (
34 TF2CRDLabel = "cnrm.cloud.google.com/tf2crd"
35 )
36
37 func GenerateTF2CRD(sm *corekccv1alpha1.ServiceMapping, resourceConfig *corekccv1alpha1.ResourceConfig) (*apiextensions.CustomResourceDefinition, error) {
38 resource := resourceConfig.Name
39 p := google.Provider()
40 r, ok := p.ResourcesMap[resource]
41 if !ok {
42 return nil, fmt.Errorf("unknown resource %v", resource)
43 }
44 s := r.Schema
45 specFields := make(map[string]*schema.Schema)
46 statusFields := make(map[string]*schema.Schema)
47 for k, v := range s {
48 if isConfigurableField(v) {
49 specFields[k] = v
50 } else {
51 statusFields[k] = v
52 }
53 }
54 openAPIV3Schema := crdboilerplate.GetOpenAPIV3SchemaSkeleton()
55 specJSONSchema := tfObjectSchemaToJSONSchema(specFields)
56 statusJSONSchema := tfObjectSchemaToJSONSchema(statusFields)
57 removeIgnoredFields(resourceConfig, specJSONSchema, statusJSONSchema)
58 removeOverwrittenFields(resourceConfig, specJSONSchema)
59 markRequiredLocationalFieldsRequired(resourceConfig, specJSONSchema)
60 addResourceIDFieldIfSupported(resourceConfig, specJSONSchema)
61 handleHierarchicalReferences(resourceConfig, specJSONSchema)
62
63 if len(specJSONSchema.Properties) > 0 {
64 openAPIV3Schema.Properties["spec"] = *specJSONSchema
65 if len(specJSONSchema.Required) > 0 {
66 openAPIV3Schema.Required = slice.IncludeString(openAPIV3Schema.Required, "spec")
67 }
68 }
69
70 statusJSONSchema, err := k8s.RenameStatusFieldsWithReservedNamesIfResourceNotExcluded(resource, statusJSONSchema)
71 if err != nil {
72 return nil, fmt.Errorf("error renaming status fields with reserved names for %#v: %v", statusJSONSchema, err)
73 }
74 for k, v := range statusJSONSchema.Properties {
75 openAPIV3Schema.Properties["status"].Properties[k] = v
76 }
77
78 group := strings.ToLower(sm.Spec.Name) + "." + ApiDomain
79
80 kind := text.SnakeCaseToUpperCamelCase(resource)
81 if resourceConfig != nil && resourceConfig.Kind != "" {
82 kind = resourceConfig.Kind
83 }
84 crd := GetCustomResourceDefinition(kind, group, sm.GetVersionFor(resourceConfig), openAPIV3Schema, TF2CRDLabel)
85 if resourceConfig.AutoGenerated {
86
87 crd.ObjectMeta.Labels[k8s.KCCStabilityLabel] = k8s.StabilityLevelAlpha
88 } else {
89 crd.ObjectMeta.Labels[k8s.KCCStabilityLabel] = k8s.StabilityLevelStable
90 }
91 return crd, nil
92 }
93
94 func tfObjectSchemaToJSONSchema(s map[string]*schema.Schema) *apiextensions.JSONSchemaProps {
95 jsonSchema := apiextensions.JSONSchemaProps{
96 Type: "object",
97 Properties: make(map[string]apiextensions.JSONSchemaProps),
98 }
99 for k, v := range s {
100 key := text.SnakeCaseToLowerCamelCase(k)
101 if v.Required {
102 jsonSchema.Required = slice.IncludeString(jsonSchema.Required, key)
103 }
104 js := *tfSchemaToJSONSchema(v)
105 description := js.Description
106 if description != "" {
107 description = ensureEndsInPeriod(description)
108 }
109 if v.ForceNew {
110 description = strings.TrimSpace("Immutable. " + description)
111 }
112 if v.Deprecated != "" {
113 deprecationMsg := ensureEndsInPeriod(fmt.Sprintf("DEPRECATED. %v", v.Deprecated))
114 description = strings.TrimSpace(fmt.Sprintf("%v %v", deprecationMsg, description))
115 }
116
117 for _, word := range []string{"terraform", "Terraform"} {
118 if !strings.Contains(description, word) {
119 continue
120 }
121 if v.Deprecated != "" {
122 panic(fmt.Errorf("about to strip field description since it contains "+
123 "the word '%v', but we likely must avoid stripping the "+
124 "description entirely since it contains a deprecation message "+
125 "that likely should stay included. Suggest changing field's "+
126 "description and/or deprecation message to drop the word '%v'. "+
127 "Description:\n%v",
128 word, word, description))
129 }
130 description = ""
131 }
132 js.Description = description
133 jsonSchema.Properties[key] = js
134 }
135 return &jsonSchema
136 }
137
138 func ensureEndsInPeriod(str string) string {
139 if !strings.HasSuffix(str, ".") {
140 return str + "."
141 }
142 return str
143 }
144
145 func tfSchemaToJSONSchema(tfSchema *schema.Schema) *apiextensions.JSONSchemaProps {
146 jsonSchema := apiextensions.JSONSchemaProps{}
147 switch tfSchema.Type {
148 case schema.TypeBool:
149 jsonSchema.Type = "boolean"
150 case schema.TypeFloat:
151 jsonSchema.Type = "number"
152 case schema.TypeInt:
153 jsonSchema.Type = "integer"
154 case schema.TypeSet:
155
156 fallthrough
157 case schema.TypeList:
158 jsonSchema.Type = "array"
159 switch v := tfSchema.Elem.(type) {
160 case *schema.Resource:
161
162
163 if tfSchema.MaxItems == 1 {
164 jsonSchema = *tfObjectSchemaToJSONSchema(v.Schema)
165 break
166 }
167 jsonSchema.Items = &apiextensions.JSONSchemaPropsOrArray{
168 Schema: tfObjectSchemaToJSONSchema(v.Schema),
169 }
170 case *schema.Schema:
171
172 jsonSchema.Items = &apiextensions.JSONSchemaPropsOrArray{
173 Schema: tfSchemaToJSONSchema(v),
174 }
175 default:
176 panic("could not parse elem attribute of TF list/set schema")
177 }
178 case schema.TypeMap:
179
180
181 jsonSchema.Type = "object"
182 if mapSchema, ok := tfSchema.Elem.(*schema.Schema); ok {
183 jsonSchema.AdditionalProperties = &apiextensions.JSONSchemaPropsOrBool{
184 Schema: tfSchemaToJSONSchema(mapSchema),
185 }
186 }
187 case schema.TypeString:
188 if tfSchema.Sensitive && isConfigurableField(tfSchema) {
189 jsonSchema = crdboilerplate.GetSensitiveFieldSchemaBoilerplate()
190 } else {
191 jsonSchema.Type = "string"
192 }
193 case schema.TypeInvalid:
194 panic(fmt.Errorf("schema type is invalid"))
195 default:
196 panic(fmt.Errorf("unknown schema type %v", tfSchema.Type))
197 }
198 jsonSchema.Description = tfSchema.Description
199 return &jsonSchema
200 }
201
202 func removeOverwrittenFields(rc *corekccv1alpha1.ResourceConfig, s *apiextensions.JSONSchemaProps) {
203 if rc.MetadataMapping.Name != "" {
204 removeField(rc.MetadataMapping.Name, s)
205 }
206 if rc.MetadataMapping.Labels != "" {
207 removeField(rc.MetadataMapping.Labels, s)
208 }
209 for _, refConfig := range rc.ResourceReferences {
210 handleResourceReference(refConfig, s)
211 }
212 for _, d := range rc.Directives {
213 removeField(d, s)
214 }
215 if !krmtotf.SupportsHierarchicalReferences(rc) {
216
217
218 for _, c := range rc.Containers {
219 removeField(c.TFField, s)
220 }
221 }
222 }
223
224 func removeIgnoredFields(rc *corekccv1alpha1.ResourceConfig, specJSONSchema, statusJSONSchema *apiextensions.JSONSchemaProps) {
225 for _, f := range rc.IgnoredFields {
226 removedInSpec := removeFieldIfExist(f, specJSONSchema)
227 removedInStatus := removeFieldIfExist(f, statusJSONSchema)
228 if removedInSpec && removedInStatus {
229 panic(fmt.Errorf("found ignored field %s in both spec and status JSON schema for resource %s", f, rc.Name))
230 }
231 if !removedInSpec && !removedInStatus {
232 panic(fmt.Errorf("cannot find ignored field %s in either spec or status JSON schema for resource %s", f, rc.Name))
233 }
234 }
235 }
236
237
238
239
240 func removeFieldIfExist(f string, s *apiextensions.JSONSchemaProps) bool {
241 if !fieldExists(f, s) {
242 return false
243 }
244 removeField(f, s)
245 return true
246 }
247
248 func markRequiredLocationalFieldsRequired(rc *corekccv1alpha1.ResourceConfig, s *apiextensions.JSONSchemaProps) {
249 if rc.IDTemplate == "" {
250 return
251 }
252
253 locationalFields := []string{"region", "zone", "location"}
254 for _, field := range locationalFields {
255
256
257 if _, ok := s.Properties[field]; !ok {
258 continue
259 }
260 if !strings.Contains(rc.IDTemplate, fmt.Sprintf("{{%v}}", field)) {
261 continue
262 }
263 s.Required = slice.IncludeString(s.Required, field)
264 }
265 }
266
267 func handleResourceReference(refConfig corekccv1alpha1.ReferenceConfig, s *apiextensions.JSONSchemaProps) {
268 *s = populateReference(strings.Split(refConfig.TFField, "."), refConfig, s)
269 }
270
271 func populateReference(path []string, refConfig corekccv1alpha1.ReferenceConfig, s *apiextensions.JSONSchemaProps) apiextensions.JSONSchemaProps {
272 field := text.SnakeCaseToLowerCamelCase(path[0])
273 if len(path) > 1 {
274 subSchema := s.Properties[field]
275 switch subSchema.Type {
276 case "array":
277 itemSchema := populateReference(path[1:], refConfig, subSchema.Items.Schema)
278 subSchema.Items.Schema = &itemSchema
279 return *s
280 case "object":
281 objSchema := populateReference(path[1:], refConfig, &subSchema)
282 s.Properties[field] = objSchema
283 return *s
284 default:
285 panic(fmt.Errorf("error parsing reference %v: cannot iterate into type that is not object or array of objects", path))
286 }
287 }
288
289
290 isList := s.Properties[field].Type == "array"
291 var refSchema *apiextensions.JSONSchemaProps
292 key := field
293 if len(refConfig.Types) == 0 {
294 if refConfig.Key != "" {
295 key = refConfig.Key
296 delete(s.Properties, field)
297 if slice.StringSliceContains(s.Required, field) {
298 s.Required = slice.RemoveStringFromStringSlice(s.Required, field)
299 s.Required = slice.IncludeString(s.Required, key)
300 }
301 }
302 refSchema = GetResourceReferenceSchemaFromTypeConfig(refConfig.TypeConfig)
303 } else {
304 refSchema = &apiextensions.JSONSchemaProps{
305 Type: "object",
306 Properties: map[string]apiextensions.JSONSchemaProps{},
307 }
308 for _, v := range refConfig.Types {
309 if v.JSONSchemaType == "" {
310 refSchema.Properties[v.Key] = *GetResourceReferenceSchemaFromTypeConfig(v)
311 } else {
312 refSchema.Properties[v.Key] = apiextensions.JSONSchemaProps{
313 Type: v.JSONSchemaType,
314 }
315 }
316 }
317 }
318
319 refSchema.Description = refConfig.Description
320
321 if isList {
322 s.Properties[key] = apiextensions.JSONSchemaProps{
323 Type: "array",
324 Items: &apiextensions.JSONSchemaPropsOrArray{
325 Schema: refSchema,
326 },
327 }
328 } else {
329 s.Properties[key] = *refSchema
330 }
331
332 return *s
333 }
334
335 func getDescriptionForExternalRef(typeConfig corekccv1alpha1.TypeConfig) string {
336 targetField := typeConfig.TargetField
337 if targetField == "" {
338 targetField = "name"
339 }
340 targetField = text.SnakeCaseToLowerCamelCase(targetField)
341 article := text.IndefiniteArticleFor(typeConfig.GVK.Kind)
342 if typeConfig.ValueTemplate != "" {
343 return fmt.Sprintf(
344 "Allowed value: string of the format `%v`, where {{value}} is the `%v` field of %v `%v` resource.",
345 typeConfig.ValueTemplate, targetField, article, typeConfig.GVK.Kind,
346 )
347 }
348 return fmt.Sprintf("Allowed value: The `%v` field of %v `%v` resource.", targetField, article, typeConfig.GVK.Kind)
349 }
350
351 func GetResourceReferenceSchemaFromTypeConfig(typeConfig corekccv1alpha1.TypeConfig) *apiextensions.JSONSchemaProps {
352 description := getDescriptionForExternalRef(typeConfig)
353 return crdboilerplate.GetResourceReferenceSchemaBoilerplate(description)
354 }
355
356 func fieldExists(f string, s *apiextensions.JSONSchemaProps) bool {
357 path := strings.Split(f, ".")
358 return nestedFieldExists(path, s)
359 }
360
361 func nestedFieldExists(path []string, s *apiextensions.JSONSchemaProps) bool {
362 if len(path) == 0 {
363 panic("unexpected empty field path")
364 }
365
366 field := text.SnakeCaseToLowerCamelCase(path[0])
367 subSchema, exists := s.Properties[field]
368 if len(path) == 1 {
369 return exists
370 }
371
372 switch subSchema.Type {
373 case "array":
374 return nestedFieldExists(path[1:], subSchema.Items.Schema)
375 case "object":
376 return nestedFieldExists(path[1:], &subSchema)
377 default:
378 return false
379 }
380 }
381
382 func removeField(tfField string, s *apiextensions.JSONSchemaProps) {
383 *s = removeNestedField(strings.Split(tfField, "."), *s)
384 }
385
386 func removeNestedField(path []string, s apiextensions.JSONSchemaProps) apiextensions.JSONSchemaProps {
387 field := text.SnakeCaseToLowerCamelCase(path[0])
388 if len(path) > 1 {
389 subSchema := s.Properties[field]
390 switch subSchema.Type {
391 case "array":
392 itemSchema := removeNestedField(path[1:], *subSchema.Items.Schema)
393 subSchema.Items.Schema = &itemSchema
394 case "object":
395 subSchema = removeNestedField(path[1:], subSchema)
396 default:
397 panic(fmt.Errorf("error parsing field %v: cannot iterate into type that is not object or array of objects", path))
398 }
399 s.Properties[field] = subSchema
400 return s
401 }
402 delete(s.Properties, field)
403 s.Required = slice.RemoveStringFromStringSlice(s.Required, field)
404 return s
405 }
406
407 func isConfigurableField(tfSchema *schema.Schema) bool {
408 return tfSchema.Required || tfSchema.Optional
409 }
410
411 func addResourceIDFieldIfSupported(rc *corekccv1alpha1.ResourceConfig, spec *apiextensions.JSONSchemaProps) {
412 if !krmtotf.SupportsResourceIDField(rc) {
413 return
414 }
415
416 spec.Properties[k8s.ResourceIDFieldName] = apiextensions.JSONSchemaProps{
417 Type: "string",
418 Description: generateResourceIDFieldDescription(rc),
419 }
420 }
421
422 func generateResourceIDFieldDescription(rc *corekccv1alpha1.ResourceConfig) string {
423 targetFieldCamelCase := text.SnakeCaseToLowerCamelCase(rc.ResourceID.TargetField)
424 isServerGeneratedResourceID := krmtotf.IsResourceIDFieldServerGenerated(rc)
425 return GenerateResourceIDFieldDescription(targetFieldCamelCase, isServerGeneratedResourceID)
426 }
427
428 func handleHierarchicalReferences(rc *corekccv1alpha1.ResourceConfig, spec *apiextensions.JSONSchemaProps) {
429 if len(rc.Containers) > 0 {
430
431
432
433 *spec = *MarkHierarchicalReferencesOptionalButMutuallyExclusive(spec, rc.HierarchicalReferences)
434 } else {
435 *spec = *MarkHierarchicalReferencesRequiredButMutuallyExclusive(spec, rc.HierarchicalReferences)
436 }
437 }
438
View as plain text