1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package main
18
19 import (
20 "flag"
21 "fmt"
22 "io/ioutil"
23 "log"
24 "os"
25 "path"
26 "path/filepath"
27 "reflect"
28 "strings"
29
30 corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
31 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/crd/crddecoration"
32 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/crd/crdgeneration"
33 dclmetadata "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/metadata"
34 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/schema/dclschemaloader"
35 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/gcp"
36 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/gvks/supportedgvks"
37 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
38 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/servicemapping/servicemappingloader"
39 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/text"
40 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/repo"
41 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/slice"
42
43 "github.com/ghodss/yaml"
44 apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
45 crdgen "sigs.k8s.io/controller-tools/pkg/crd"
46 "sigs.k8s.io/controller-tools/pkg/genall"
47 )
48
49 const (
50 dirMode = 0700
51 fileMode = 0600
52 )
53
54 var (
55 outputDir = ""
56 )
57
58 func main() {
59 flag.StringVar(&outputDir, "output-dir", "", "Directory where CRD files are to be written to")
60 flag.Parse()
61 if outputDir == "" {
62 fmt.Printf("error: invalid value for output directory: '%s'\n", "empty string")
63 flag.PrintDefaults()
64 os.Exit(1)
65 }
66
67 if _, err := os.Stat(outputDir); os.IsNotExist(err) {
68 if err := os.Mkdir(outputDir, dirMode); err != nil {
69 fmt.Printf("error creating output directory '%v': %v\n", outputDir, err)
70 os.Exit(1)
71 }
72 }
73
74 crds := make([]*apiextensions.CustomResourceDefinition, 0)
75
76 crds = append(crds, generateTFBasedCRDs()...)
77
78 crds = append(crds, generateDCLBasedCRDs()...)
79
80
81 crds = append(crds, generateCRDsForTypesFiles()...)
82
83 for _, crd := range crds {
84 if err := crddecoration.DecorateCRD(crd); err != nil {
85 log.Fatalf("error decorating CRD %v: %v", crd.GetName(), err)
86 }
87 if err := outputCRDToFile(crd); err != nil {
88 log.Fatalf("error outputting CRD %v to file: %v", crd.GetName(), err)
89 }
90 }
91 outputPath, _ := filepath.Abs(outputDir)
92 fmt.Printf("CRD manifests generated under '%v'\n", outputPath)
93 }
94
95 func generateTFBasedCRDs() []*apiextensions.CustomResourceDefinition {
96 outputCRDs := make([]*apiextensions.CustomResourceDefinition, 0)
97 serviceMappings, err := servicemappingloader.GetServiceMappings()
98 if err != nil {
99 log.Fatalf("could not load service mappings: %v", err)
100 }
101 for _, sm := range serviceMappings {
102 kindResourceConfigsMap := generateKindResourceConfigsMap(&sm)
103 for kind, rcs := range kindResourceConfigsMap {
104 switch kind {
105 case "ComputeInstance":
106
107
108
109 outputCRDs = append(outputCRDs, generateComputeInstanceCRD(sm, rcs))
110 continue
111 case "ComputeInstanceTemplate":
112
113 outputCRDs = append(outputCRDs, generateComputeInstanceTemplateCRD(sm, rcs))
114 continue
115 }
116
117
118
119
120 isLocationalResource, err := isLocationalResource(rcs)
121 if err != nil {
122 log.Fatal(err)
123 }
124 crds := make([]*apiextensions.CustomResourceDefinition, 0)
125 for _, rc := range rcs {
126 crd, err := crdgeneration.GenerateTF2CRD(&sm, rc)
127 if err != nil {
128 log.Fatalf("error generating CRD for %v: %v", rc.Name, err)
129 }
130 crds = append(crds, crd)
131 }
132 crd, err := mergeCRDs(crds)
133 if err != nil {
134 log.Fatalf("error merging CRDs for kind %v: %v", kind, err)
135 }
136 if isLocationalResource {
137 addLocationField(crd, rcs)
138 }
139 crd = addOneOfRulesForMultiTypeResourceReferences(crd, rcs)
140 outputCRDs = append(outputCRDs, crd)
141 }
142 }
143 return outputCRDs
144 }
145
146 func generateDCLBasedCRDs() []*apiextensions.CustomResourceDefinition {
147 outputCRDs := make([]*apiextensions.CustomResourceDefinition, 0)
148 serviceMetadataLoader := dclmetadata.New()
149 schemaLoader, err := dclschemaloader.New()
150 if err != nil {
151 log.Fatalf("could not get a new DCL schema loader: %v", err)
152 }
153 smLoader, err := servicemappingloader.New()
154 if err != nil {
155 log.Fatalf("could not create service mapping loader: %v", err)
156 }
157 generator := crdgeneration.New(serviceMetadataLoader, schemaLoader, supportedgvks.All(smLoader, serviceMetadataLoader))
158 gvks := supportedgvks.BasedOnDCL(serviceMetadataLoader)
159 for _, gvk := range gvks {
160 s, err := dclschemaloader.GetDCLSchemaForGVK(gvk, serviceMetadataLoader, schemaLoader)
161 if err != nil {
162 log.Fatalf("error getting the DCL schema for GVK %v: %v", gvk, err)
163 }
164 crd, err := generator.GenerateCRDFromOpenAPISchema(s, gvk)
165 if err != nil {
166 log.Fatalf("error generating CRD for %v: %v", gvk, err)
167 }
168 outputCRDs = append(outputCRDs, crd)
169 }
170 return outputCRDs
171 }
172
173 func generateCRDsForTypesFiles() []*apiextensions.CustomResourceDefinition {
174 crds := make([]*apiextensions.CustomResourceDefinition, 0)
175 tempDir, err := ioutil.TempDir("", "crds_for_types")
176 if err != nil {
177 log.Fatalf("error creating temporary directory: %v", err)
178 }
179 defer removeDir(tempDir)
180 gens := genall.Generators{}
181 crdGen := genall.Generator(crdgen.Generator{})
182 gens = append(gens, &crdGen)
183 rootPath := repo.GetRootOrLogFatal()
184 apisPath := path.Join(rootPath, "pkg", "apis", "iam", "v1beta1")
185 roots, err := gens.ForRoots(apisPath)
186 if err != nil {
187 log.Fatalf("error producing a Runtime to run the generators: %v", err)
188 }
189 roots.OutputRules = genall.OutputRules{Default: genall.OutputToDirectory(tempDir)}
190 if failed := roots.Run(); failed {
191 log.Fatalf("failed generating CRDs from Go structs")
192 }
193 files, err := ioutil.ReadDir(tempDir)
194 if err != nil {
195 log.Fatalf("error reading temporary directory: %v", err)
196 }
197 for _, f := range files {
198 p := path.Join(tempDir, f.Name())
199 b, err := ioutil.ReadFile(p)
200 if err != nil {
201 log.Fatalf("error reading file %v: %v", p, err)
202 }
203 crd := &apiextensions.CustomResourceDefinition{}
204 if err = yaml.Unmarshal(b, crd); err != nil {
205 log.Fatalf("error marshalling file %v into CRD: %v", p, err)
206 }
207 crds = append(crds, crd)
208 }
209 return crds
210 }
211
212 func removeDir(dir string) {
213 if err := os.RemoveAll(dir); err != nil {
214 log.Fatalf("error removing directory '%v': %v", dir, err)
215 }
216 }
217
218
219
220
221
222 func addOneOfRulesForMultiTypeResourceReferences(crd *apiextensions.CustomResourceDefinition, rcs []*corekccv1alpha1.ResourceConfig) *apiextensions.CustomResourceDefinition {
223 outCRD := crd.DeepCopy()
224 jsonSchema := k8s.GetOpenAPIV3SchemaFromCRD(outCRD)
225 tfFieldsToReferenceKeys := getTFFieldsThatMapToMultipleReferenceKeys(rcs)
226 for tfField, keys := range tfFieldsToReferenceKeys {
227 field := append([]string{"spec"}, text.SnakeCaseStrsToLowerCamelCaseStrs(strings.Split(tfField, "."))...)
228
229 oneOfRule := make([]apiextensions.JSONSchemaProps, 0)
230 for _, key := range keys {
231 oneOfRule = append(oneOfRule,
232 apiextensions.JSONSchemaProps{
233 Required: []string{key},
234 },
235 )
236 }
237 jsonSchema = setOneOfRuleForField(jsonSchema, field, oneOfRule)
238 }
239 outCRD.Spec.Versions[0].Schema.OpenAPIV3Schema = jsonSchema
240 return outCRD
241 }
242
243 func getTFFieldsThatMapToMultipleReferenceKeys(rcs []*corekccv1alpha1.ResourceConfig) map[string][]string {
244 tfFieldsToRefKeys := make(map[string][]string)
245 for _, rc := range rcs {
246 for _, refConfig := range rc.ResourceReferences {
247 tfField := refConfig.TFField
248 if _, ok := tfFieldsToRefKeys[tfField]; !ok {
249 tfFieldsToRefKeys[tfField] = make([]string, 0)
250 }
251 for _, t := range refConfig.Types {
252
253
254 tfFieldsToRefKeys[tfField] = slice.IncludeString(tfFieldsToRefKeys[tfField], t.Key)
255 }
256 }
257 }
258 for tfField, keys := range tfFieldsToRefKeys {
259 if len(keys) <= 1 {
260 delete(tfFieldsToRefKeys, tfField)
261 }
262 }
263 return tfFieldsToRefKeys
264 }
265
266 func setOneOfRuleForField(s *apiextensions.JSONSchemaProps, field []string, oneOfRule []apiextensions.JSONSchemaProps) *apiextensions.JSONSchemaProps {
267 outSchema := s.DeepCopy()
268 subSchema := outSchema.Properties[field[0]]
269 if len(field) > 1 {
270 switch subSchema.Type {
271 case "array":
272 subSchema.Items.Schema = setOneOfRuleForField(subSchema.Items.Schema, field[1:], oneOfRule)
273 case "object":
274 subSchema = *setOneOfRuleForField(&subSchema, field[1:], oneOfRule)
275 default:
276 panic(fmt.Errorf("error parsing field %v: cannot iterate into type that is not object or array of objects", field))
277 }
278 } else {
279 switch subSchema.Type {
280 case "array":
281 subSchema.Items.Schema.OneOf = oneOfRule
282 default:
283 subSchema.OneOf = oneOfRule
284 }
285 }
286 outSchema.Properties[field[0]] = subSchema
287 return outSchema
288 }
289
290
291 func addLocationField(crd *apiextensions.CustomResourceDefinition, rcs []*corekccv1alpha1.ResourceConfig) {
292 schema := k8s.GetOpenAPIV3SchemaFromCRD(crd)
293 spec := schema.Properties["spec"]
294
295 locationalFields := map[string]bool{
296 "region": true,
297 "zone": true,
298 }
299
300
301 for field := range locationalFields {
302 delete(spec.Properties, field)
303 }
304 requiredFields := make([]string, 0)
305 for _, field := range spec.Required {
306 if _, ok := locationalFields[field]; !ok {
307 requiredFields = append(requiredFields, field)
308 }
309 }
310 spec.Required = requiredFields
311
312 locations := make(map[string]bool)
313 for _, rc := range rcs {
314 locations[rc.Locationality] = true
315 }
316 spec.Properties["location"] = apiextensions.JSONSchemaProps{
317 Type: "string",
318 Description: generateLocationFieldDescription(locations, crd.Spec.Names.Kind),
319 }
320 spec.Required = slice.IncludeString(spec.Required, "location")
321
322 schema.Properties["spec"] = spec
323 schema.Required = slice.IncludeString(schema.Required, "spec")
324 }
325
326 func generateLocationFieldDescription(locations map[string]bool, resource string) string {
327 description1 := fmt.Sprintf("Location represents the geographical location of the %v.", resource)
328 description2 := ""
329 if locations[gcp.Regional] {
330 description2 = "Specify a region name"
331 }
332 if locations[gcp.Zonal] {
333 if description2 == "" {
334 description2 = "Specify a zone name"
335 } else {
336 description2 = description2 + " or a zone name"
337 }
338 }
339 if locations[gcp.Global] {
340 if description2 == "" {
341 description2 = `Specify "global" for global resources`
342 } else {
343 description2 = description2 + ` or "global" for global resources`
344 }
345 }
346 if locations[gcp.Regional] || locations[gcp.Zonal] {
347 return fmt.Sprintf("%v %v. %v", description1, description2, "Reference: GCP definition of regions/zones (https://cloud.google.com/compute/docs/regions-zones/)")
348 } else {
349 return fmt.Sprintf("%v %v.", description1, description2)
350 }
351 }
352
353 func isLocationalResource(rcs []*corekccv1alpha1.ResourceConfig) (bool, error) {
354 m := map[string]bool{
355 gcp.Regional: false,
356 gcp.Global: false,
357 gcp.Zonal: false,
358 }
359 if len(rcs) > 1 {
360 for _, rc := range rcs {
361 if _, ok := m[rc.Locationality]; !ok {
362 return false, fmt.Errorf("unsupported locationality %v for kind %v is set", rc.Locationality, rc.Kind)
363 }
364 if m[rc.Locationality] {
365 return false, fmt.Errorf("there is more than one resource config defined for the same locationality %v for kind %v", rc.Locationality, rc.Kind)
366 }
367 }
368 return true, nil
369 }
370 if rcs[0].Locationality == "" {
371 return false, nil
372 }
373 if _, ok := m[rcs[0].Locationality]; !ok {
374 return false, fmt.Errorf("unsupported locationality %v for kind %v is set", rcs[0].Locationality, rcs[0].Kind)
375 }
376 return true, nil
377 }
378
379 func generateKindResourceConfigsMap(sm *corekccv1alpha1.ServiceMapping) map[string][]*corekccv1alpha1.ResourceConfig {
380 res := make(map[string][]*corekccv1alpha1.ResourceConfig)
381 for i, rc := range sm.Spec.Resources {
382 if _, ok := res[rc.Kind]; !ok {
383 res[rc.Kind] = make([]*corekccv1alpha1.ResourceConfig, 0)
384 }
385 res[rc.Kind] = append(res[rc.Kind], &sm.Spec.Resources[i])
386 }
387 return res
388 }
389
390 func mergeCRDs(crds []*apiextensions.CustomResourceDefinition) (*apiextensions.CustomResourceDefinition, error) {
391 if len(crds) == 0 {
392 return nil, fmt.Errorf("there is no CRD to merge")
393 }
394 if len(crds) == 1 {
395 return crds[0], nil
396 }
397
398 var mergedCrd *apiextensions.CustomResourceDefinition
399 for _, crd := range crds {
400 if mergedCrd == nil {
401 mergedCrd = crd.DeepCopy()
402 } else {
403 if !reflect.DeepEqual(mergedCrd.Name, crd.Name) {
404 return nil, fmt.Errorf("couldn't merge crds with different names: %v, %v", mergedCrd.Name, crd.Name)
405 }
406 if err := mergeJSONSchemaProps(k8s.GetOpenAPIV3SchemaFromCRD(mergedCrd), k8s.GetOpenAPIV3SchemaFromCRD(crd)); err != nil {
407 return nil, fmt.Errorf("couldn't merge crds for %v: %v", crd.Name, err)
408 }
409 }
410 }
411 return mergedCrd, nil
412 }
413
414 func mergeJSONSchemaProps(s1 *apiextensions.JSONSchemaProps, s2 *apiextensions.JSONSchemaProps) error {
415 if s1.Type != s2.Type {
416 return fmt.Errorf("types for the field value are different: one is %v and the other is %v", s1.Type, s2.Type)
417 }
418
419 if s1.Type == "array" {
420 return mergeJSONSchemaPropsForArrayType(s1, s2)
421 }
422
423 return mergeJSONSchemaPropsForNonArrayType(s1, s2)
424 }
425
426 func mergeJSONSchemaPropsForArrayType(s1 *apiextensions.JSONSchemaProps, s2 *apiextensions.JSONSchemaProps) error {
427 return mergeJSONSchemaPropsForNonArrayType(s1.Items.Schema, s2.Items.Schema)
428 }
429
430 func mergeJSONSchemaPropsForNonArrayType(s1 *apiextensions.JSONSchemaProps, s2 *apiextensions.JSONSchemaProps) error {
431 commonRequired := intersection(s1.Required, s2.Required)
432 s1.Required = commonRequired
433
434 for k, v2 := range s2.Properties {
435 if v1, ok := s1.Properties[k]; !ok {
436 s1.Properties[k] = v2
437 } else {
438 if err := mergeJSONSchemaProps(&v1, &v2); err != nil {
439 return fmt.Errorf("error merging JSON schema field %v: %v", k, err)
440 }
441 s1.Properties[k] = v1
442 }
443 }
444 return nil
445 }
446
447 func intersection(a, b []string) []string {
448 m := make(map[string]bool)
449 for _, item := range a {
450 m[item] = true
451 }
452 c := make([]string, 0)
453 for _, item := range b {
454 if _, ok := m[item]; ok {
455 c = slice.IncludeString(c, item)
456 }
457 }
458 return c
459 }
460
461 func generateComputeInstanceCRD(sm corekccv1alpha1.ServiceMapping, rcs []*corekccv1alpha1.ResourceConfig) *apiextensions.CustomResourceDefinition {
462 crds := make([]*apiextensions.CustomResourceDefinition, 0)
463 for _, rc := range rcs {
464 crd, err := crdgeneration.GenerateTF2CRD(&sm, rc)
465 if err != nil {
466 log.Fatalf("error generating CRD for %v: %v", rc.Name, err)
467 }
468 crds = append(crds, crd)
469 }
470 crd, err := mergeComputeInstanceCRDs(crds)
471 if err != nil {
472 log.Fatalf("error merging CRDs for kind ComputeInstance: %v", err)
473 }
474 setCustomMetadataSchemaforComputeInstanceAndTemplate(crd)
475 return crd
476 }
477
478 func mergeComputeInstanceCRDs(crds []*apiextensions.CustomResourceDefinition) (*apiextensions.CustomResourceDefinition, error) {
479 if len(crds) != 2 {
480 return nil, fmt.Errorf("there should be 2 ComputeInstance CRDs to merge")
481 }
482
483 var mergedCrd *apiextensions.CustomResourceDefinition
484 for _, crd := range crds {
485 if mergedCrd == nil {
486 mergedCrd = crd.DeepCopy()
487 } else {
488 if mergedCrd.Name != crd.Name {
489 return nil, fmt.Errorf("couldn't merge crds with different names: %v, %v", mergedCrd.Name, crd.Name)
490 }
491 mergeComputeInstanceJSONSchemaProps(k8s.GetOpenAPIV3SchemaFromCRD(mergedCrd), k8s.GetOpenAPIV3SchemaFromCRD(crd))
492 }
493 }
494 return mergedCrd, nil
495 }
496
497 func mergeComputeInstanceJSONSchemaProps(s1 *apiextensions.JSONSchemaProps, s2 *apiextensions.JSONSchemaProps) {
498 if !reflect.DeepEqual(s1.Required, s2.Required) {
499 s1.AnyOf = []apiextensions.JSONSchemaProps{
500 {
501 Required: s1.Required,
502 },
503 {
504 Required: s2.Required,
505 },
506 }
507 s1.Required = nil
508 }
509
510 for k, v2 := range s2.Properties {
511 if v1, ok := s1.Properties[k]; !ok {
512 s1.Properties[k] = v2
513 } else {
514 mergeComputeInstanceJSONSchemaProps(&v1, &v2)
515 s1.Properties[k] = v1
516 }
517 }
518 }
519
520 func generateComputeInstanceTemplateCRD(sm corekccv1alpha1.ServiceMapping, rcs []*corekccv1alpha1.ResourceConfig) *apiextensions.CustomResourceDefinition {
521 if len(rcs) != 1 {
522 log.Fatalf("expected only one resource config for compute instance template, got %v", len(rcs))
523 }
524 rc := rcs[0]
525 crd, err := crdgeneration.GenerateTF2CRD(&sm, rc)
526 if err != nil {
527 log.Fatalf("error generating CRD for %v: %v", rc.Name, err)
528 }
529 setCustomMetadataSchemaforComputeInstanceAndTemplate(crd)
530 return crd
531 }
532
533 func setCustomMetadataSchemaforComputeInstanceAndTemplate(crd *apiextensions.CustomResourceDefinition) {
534
535
536
537 metadataSchema := apiextensions.JSONSchemaProps{
538 Type: "array",
539 Items: &apiextensions.JSONSchemaPropsOrArray{
540 Schema: &apiextensions.JSONSchemaProps{
541 Type: "object",
542 Properties: map[string]apiextensions.JSONSchemaProps{
543 "key": {Type: "string"},
544 "value": {Type: "string"},
545 },
546 Required: []string{"key", "value"},
547 },
548 },
549 }
550 schema := k8s.GetOpenAPIV3SchemaFromCRD(crd)
551 specSchema := schema.Properties["spec"]
552 specSchema.Properties["metadata"] = metadataSchema
553 schema.Properties["spec"] = specSchema
554 }
555
556 func outputCRDToFile(crd *apiextensions.CustomResourceDefinition) error {
557 crdBytes, err := yaml.Marshal(crd)
558 if err != nil {
559 return err
560 }
561 outputFilename, err := crdgeneration.FileNameForCRD(crd)
562 if err != nil {
563 return err
564 }
565 outputFilepath := outputDir + "/" + outputFilename
566 if err := ioutil.WriteFile(outputFilepath, crdBytes, fileMode); err != nil {
567 return err
568 }
569 return nil
570 }
571
View as plain text