...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/scripts/generate-crds/main.go

Documentation: github.com/GoogleCloudPlatform/k8s-config-connector/scripts/generate-crds

     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  // This file generates CRDs for the type providers defined in the input type providers directory.
    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  	// create the output dir if it doesn't exist
    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  	// generate TF2CRD-based resources
    76  	crds = append(crds, generateTFBasedCRDs()...)
    77  	// generate DCL2CRD-based resources
    78  	crds = append(crds, generateDCLBasedCRDs()...)
    79  	// generate CRDs for the strongly-typed Go structs in pkg/apis
    80  	// (e.g. IAMPolicy, IAMPolicyMember, IAMAuditConfig, ServiceMapping)
    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  				// We merge the two TF resources: compute_instance, and compute_instance_from_template
   107  				// into one KCC resource, ComputeInstance, and convert its metadata field from a map
   108  				// to a structured list.
   109  				outputCRDs = append(outputCRDs, generateComputeInstanceCRD(sm, rcs))
   110  				continue
   111  			case "ComputeInstanceTemplate":
   112  				// We convert ComputeInstanceTemplate's metadata field from a map to a structured list.
   113  				outputCRDs = append(outputCRDs, generateComputeInstanceTemplateCRD(sm, rcs))
   114  				continue
   115  			}
   116  
   117  			// Another scenario of having multiple resource config mapping to the same kind is to consolidate
   118  			// locational resources into a single CRD, e.g. compute_address and compute_global_address will be
   119  			// merged to a single kind ComputeAddress
   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  // addOneOfRulesForMultiTypeResourceReferences returns a copy of the given CRD
   219  // but with a modified JSON schema which makes it so that keys for resource
   220  // references are mutually exclusive if a resource reference can have one of
   221  // multiple keys (i.e. if the resource reference has multiple TypeConfigs)
   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  		// Enforces that one and only one key can be specified for the resource reference field
   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  				// Keep the list of keys in sorted order to keep the output deterministic
   253  				// (i.e. always the same output for the same input)
   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  // addLocationField removes existing locational fields (region, zone) and replaces them with a 'location' field.
   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  	// It is assumed that locational fields (region, zone) would always be
   300  	// at the base level.
   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  	// In order to make setting custom metadata more in-line with environment-variable setting in
   535  	// k8s pods, as well as leaving it extensible to add "valueFrom" in the future, we convert
   536  	// the TF schema's map into a structured object array.
   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