diff --git a/go.mod b/go.mod index 281e9221..73f08bdf 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( k8s.io/apimachinery v0.24.0 k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 sigs.k8s.io/controller-runtime v0.12.1 + sigs.k8s.io/yaml v1.3.0 ) require ( @@ -127,5 +128,4 @@ require ( k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/pkg/config/provider.go b/pkg/config/provider.go index dfba217c..2f76f8a4 100644 --- a/pkg/config/provider.go +++ b/pkg/config/provider.go @@ -1,3 +1,7 @@ +/* +Copyright 2022 Upbound Inc. +*/ + package config import ( @@ -96,6 +100,10 @@ type Provider struct { // resource name. Resources map[string]*Resource + // ProviderMetadataPath is the scraped provider metadata file path + // from Terraform registry + ProviderMetadataPath string + // resourceConfigurators is a map holding resource configurators where key // is Terraform resource name. resourceConfigurators map[string]ResourceConfiguratorChain @@ -146,10 +154,10 @@ func WithDefaultResourceFn(f DefaultResourceFn) ProviderOption { } } -// NewProviderWithSchema builds and returns a new Provider from provider +// NewProvider builds and returns a new Provider from provider // tfjson schema, that is generated using Terraform CLI with: // `terraform providers schema --json` -func NewProviderWithSchema(schema []byte, prefix string, modulePath string, opts ...ProviderOption) *Provider { +func NewProvider(schema []byte, prefix string, modulePath string, metadataPath string, opts ...ProviderOption) *Provider { ps := tfjson.ProviderSchemas{} if err := ps.UnmarshalJSON(schema); err != nil { panic(err) @@ -162,13 +170,8 @@ func NewProviderWithSchema(schema []byte, prefix string, modulePath string, opts rs = v.ResourceSchemas break } - return NewProvider(conversiontfjson.GetV2ResourceMap(rs), prefix, modulePath, opts...) -} -// NewProvider builds and returns a new Provider. -// Deprecated: This function will be removed soon, please use -// NewProviderWithSchema instead. -func NewProvider(resourceMap map[string]*schema.Resource, prefix string, modulePath string, opts ...ProviderOption) *Provider { + resourceMap := conversiontfjson.GetV2ResourceMap(rs) p := &Provider{ ModulePath: modulePath, TerraformResourcePrefix: fmt.Sprintf("%s_", prefix), @@ -181,6 +184,7 @@ func NewProvider(resourceMap map[string]*schema.Resource, prefix string, moduleP ".+", }, Resources: map[string]*Resource{}, + ProviderMetadataPath: metadataPath, resourceConfigurators: map[string]ResourceConfiguratorChain{}, } @@ -204,7 +208,6 @@ func NewProvider(resourceMap map[string]*schema.Resource, prefix string, moduleP p.Resources[name] = p.DefaultResourceFn(name, terraformResource) } - return p } diff --git a/pkg/pipeline/crd.go b/pkg/pipeline/crd.go index 432e1ea8..e6b780e6 100644 --- a/pkg/pipeline/crd.go +++ b/pkg/pipeline/crd.go @@ -42,6 +42,7 @@ type CRDGenerator struct { Group string ProviderShortName string LicenseHeaderPath string + Generated *tjtypes.Generated pkg *types.Package } @@ -65,6 +66,8 @@ func (cg *CRDGenerator) Generate(cfg *config.Resource) (string, error) { if err != nil { return "", errors.Wrapf(err, "cannot build types for %s", cfg.Kind) } + cg.Generated = &gen + // TODO(muvaf): TypePrinter uses the given scope to see if the type exists // before printing. We should ideally load the package in file system but // loading the local package will result in error if there is diff --git a/pkg/pipeline/example.go b/pkg/pipeline/example.go new file mode 100644 index 00000000..99f10681 --- /dev/null +++ b/pkg/pipeline/example.go @@ -0,0 +1,301 @@ +/* +Copyright 2022 Upbound Inc. +*/ + +package pipeline + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/crossplane/crossplane-runtime/pkg/fieldpath" + "github.com/pkg/errors" + "sigs.k8s.io/yaml" + + "github.com/upbound/upjet/pkg/config" + "github.com/upbound/upjet/pkg/resource/json" + tjtypes "github.com/upbound/upjet/pkg/types" +) + +var ( + reRef = regexp.MustCompile(`\${(.+)}`) + reFile = regexp.MustCompile(`file\("(.+)"\)`) +) + +type pavedWithManifest struct { + manifestPath string + paved *fieldpath.Paved + refsResolved bool +} + +// ResourceExample represents the scraped example HCL configuration +// for a Terraform resource +type ResourceExample struct { + Manifest string `yaml:"manifest"` + References map[string]string `yaml:"references,omitempty"` +} + +// Resource represents the scraped metadata for a Terraform resource +type Resource struct { + SubCategory string `yaml:"subCategory"` + Description string `yaml:"description,omitempty"` + Name string `yaml:"name"` + TitleName string `yaml:"titleName"` + Examples []ResourceExample `yaml:"examples,omitempty"` + ArgumentDocs map[string]string `yaml:"argumentDocs"` + ImportStatements []string `yaml:"importStatements"` +} + +// ProviderMetadata metadata for a Terraform native provider +type ProviderMetadata struct { + Name string `yaml:"name"` + Resources map[string]*Resource `yaml:"resources"` +} + +// NewProviderMetadataFromFile loads metadata from the specified YAML-formatted file +func NewProviderMetadataFromFile(path string) (*ProviderMetadata, error) { + buff, err := ioutil.ReadFile(filepath.Clean(path)) + if err != nil { + return nil, errors.Wrapf(err, "failed to read metadata file %q", path) + } + + metadata := &ProviderMetadata{} + return metadata, errors.Wrap(yaml.Unmarshal(buff, metadata), "failed to unmarshal provider metadata") +} + +// ExampleGenerator represents a pipeline for generating example manifests. +// Generates example manifests for Terraform resources under examples-generated. +type ExampleGenerator struct { + rootDir string + resourceMeta map[string]*Resource + resources map[string]*pavedWithManifest +} + +// NewExampleGenerator returns a configured ExampleGenerator +func NewExampleGenerator(rootDir string, resourceMeta map[string]*Resource) *ExampleGenerator { + return &ExampleGenerator{ + rootDir: rootDir, + resourceMeta: resourceMeta, + resources: make(map[string]*pavedWithManifest), + } +} + +// StoreExamples stores the generated example manifests under examples-generated in +// their respective API groups. +func (eg *ExampleGenerator) StoreExamples() error { + for n, pm := range eg.resources { + if err := eg.resolveReferencesOfPaved(pm); err != nil { + return errors.Wrapf(err, "cannot resolve references for resource: %s", n) + } + u := pm.paved.UnstructuredContent() + delete(u["spec"].(map[string]interface{})["forProvider"].(map[string]interface{}), "depends_on") + buff, err := yaml.Marshal(u) + if err != nil { + return errors.Wrapf(err, "cannot marshal example manifest for resource: %s", n) + } + manifestDir := filepath.Dir(pm.manifestPath) + if err := os.MkdirAll(manifestDir, 0750); err != nil { + return errors.Wrapf(err, "cannot mkdir %s", manifestDir) + } + // no sensitive info in the example manifest + if err := ioutil.WriteFile(pm.manifestPath, buff, 0644); err != nil { // nolint:gosec + return errors.Wrapf(err, "cannot write example manifest file %s for resource %s", pm.manifestPath, n) + } + } + return nil +} + +func (eg *ExampleGenerator) resolveReferencesOfPaved(pm *pavedWithManifest) error { + if pm.refsResolved { + return nil + } + pm.refsResolved = true + return errors.Wrap(eg.resolveReferences(pm.paved.UnstructuredContent()), "failed to resolve references of paved") +} + +func (eg *ExampleGenerator) resolveReferences(params map[string]interface{}) error { // nolint:gocyclo + for k, v := range params { + switch t := v.(type) { + case map[string]interface{}: + if err := eg.resolveReferences(t); err != nil { + return err + } + + case []interface{}: + for _, e := range t { + eM, ok := e.(map[string]interface{}) + if !ok { + continue + } + if err := eg.resolveReferences(eM); err != nil { + return err + } + } + + case string: + g := reRef.FindStringSubmatch(t) + if len(g) != 2 { + continue + } + path := strings.Split(g[1], ".") + // expected reference format is .. + if len(path) < 3 { + continue + } + pm := eg.resources[path[0]] + if pm == nil || pm.paved == nil { + continue + } + if err := eg.resolveReferencesOfPaved(pm); err != nil { + return errors.Wrapf(err, "cannot recursively resolve references for %q", path[0]) + } + pathStr := strings.Join(append([]string{"spec", "forProvider"}, path[2:]...), ".") + s, err := pm.paved.GetString(pathStr) + if fieldpath.IsNotFound(err) { + continue + } + if err != nil { + return errors.Wrapf(err, "cannot get string value from paved: %s", pathStr) + } + params[k] = s + } + } + return nil +} + +// Generate generates an example manifest for the specified Terraform resource. +func (eg *ExampleGenerator) Generate(group, version string, r *config.Resource, fieldTransformations map[string]tjtypes.Transformation) error { + rm := eg.resourceMeta[r.Name] + if rm == nil || len(rm.Examples) == 0 { + return nil + } + var exampleParams map[string]interface{} + if err := json.TFParser.Unmarshal([]byte(rm.Examples[0].Manifest), &exampleParams); err != nil { + return errors.Wrapf(err, "cannot unmarshal example manifest for resource: %s", r.Name) + } + transformFields(exampleParams, r.ExternalName.OmittedFields, fieldTransformations, "") + + example := map[string]interface{}{ + "apiVersion": fmt.Sprintf("%s/%s", group, version), + "kind": r.Kind, + "metadata": map[string]interface{}{ + "name": "example", + }, + "spec": map[string]interface{}{ + "forProvider": exampleParams, + "providerConfigRef": map[string]interface{}{ + "name": "example", + }, + }, + } + manifestDir := filepath.Join(eg.rootDir, "examples-generated", strings.ToLower(strings.Split(group, ".")[0])) + eg.resources[r.Name] = &pavedWithManifest{ + manifestPath: filepath.Join(manifestDir, fmt.Sprintf("%s.yaml", strings.ToLower(r.Kind))), + paved: fieldpath.Pave(example), + } + return nil +} + +func getHierarchicalName(prefix, name string) string { + if prefix == "" { + return name + } + return fmt.Sprintf("%s.%s", prefix, name) +} + +func transformFields(params map[string]interface{}, omittedFields []string, t map[string]tjtypes.Transformation, namePrefix string) { // nolint:gocyclo + for _, hn := range omittedFields { + for n := range params { + if hn == getHierarchicalName(namePrefix, n) { + delete(params, n) + break + } + } + } + + for n, v := range params { + switch pT := v.(type) { + case map[string]interface{}: + transformFields(pT, omittedFields, t, getHierarchicalName(namePrefix, n)) + + case []interface{}: + for _, e := range pT { + eM, ok := e.(map[string]interface{}) + if !ok { + continue + } + transformFields(eM, omittedFields, t, getHierarchicalName(namePrefix, n)) + } + } + } + + for hn, transform := range t { + for n, v := range params { + if hn == getHierarchicalName(namePrefix, n) { + delete(params, n) + if transform.IsRef { + if !transform.IsSensitive { + params[transform.TransformedName] = getRefField(v, + map[string]interface{}{ + "name": "example", + }) + } else { + secretName, secretKey := getSecretRef(v) + params[transform.TransformedName] = getRefField(v, + map[string]interface{}{ + "name": secretName, + "namespace": "crossplane-system", + "key": secretKey, + }) + } + } else { + params[transform.TransformedName] = v + } + break + } + } + } +} + +func getRefField(v interface{}, ref map[string]interface{}) interface{} { + switch v.(type) { + case []interface{}: + return []interface{}{ + ref, + } + + default: + return ref + } +} + +func getSecretRef(v interface{}) (string, string) { + secretName := "example-secret" + secretKey := "example-key" + s, ok := v.(string) + if !ok { + return secretName, secretKey + } + g := reRef.FindStringSubmatch(s) + if len(g) != 2 { + return secretName, secretKey + } + f := reFile.FindStringSubmatch(g[1]) + switch { + case len(f) == 2: // then a file reference + _, file := filepath.Split(f[1]) + secretKey = fmt.Sprintf("attribute.%s", file) + default: + parts := strings.Split(g[1], ".") + if len(parts) < 3 { + return secretName, secretKey + } + secretName = fmt.Sprintf("example-%s", strings.Join(strings.Split(parts[0], "_")[1:], "-")) + secretKey = fmt.Sprintf("attribute.%s", strings.Join(parts[2:], ".")) + } + return secretName, secretKey +} diff --git a/pkg/pipeline/run.go b/pkg/pipeline/run.go index 735d9993..112dcc11 100644 --- a/pkg/pipeline/run.go +++ b/pkg/pipeline/run.go @@ -57,6 +57,15 @@ func Run(pc *config.Provider, rootDir string) { // nolint:gocyclo resourcesGroups[group][resource.Version][name] = resource } + metaResources := make(map[string]*Resource) + if pc.ProviderMetadataPath != "" { + providerMetadata, err := NewProviderMetadataFromFile(filepath.Join(rootDir, pc.ProviderMetadataPath)) + if err != nil { + panic(errors.Wrap(err, "cannot read Terraform provider metadata")) + } + metaResources = providerMetadata.Resources + } + // Add ProviderConfig API package to the list of API version packages. apiVersionPkgList := make([]string, 0) for _, p := range pc.BasePackages.APIVersion { @@ -68,6 +77,7 @@ func Run(pc *config.Provider, rootDir string) { // nolint:gocyclo controllerPkgList = append(controllerPkgList, filepath.Join(pc.ModulePath, p)) } count := 0 + exampleGen := NewExampleGenerator(rootDir, metaResources) for group, versions := range resourcesGroups { for version, resources := range versions { var tfResources []*terraformedInput @@ -90,6 +100,9 @@ func Run(pc *config.Provider, rootDir string) { // nolint:gocyclo panic(errors.Wrapf(err, "cannot generate controller for resource %s", name)) } controllerPkgList = append(controllerPkgList, ctrlPkgPath) + if err := exampleGen.Generate(group, version, resources[name], crdGen.Generated.FieldTransformations); err != nil { + panic(errors.Wrapf(err, "cannot generate example manifest for resource %s", name)) + } count++ } @@ -104,6 +117,10 @@ func Run(pc *config.Provider, rootDir string) { // nolint:gocyclo } } + if err := exampleGen.StoreExamples(); err != nil { + panic(errors.Wrapf(err, "cannot store examples")) + } + if err := NewRegisterGenerator(rootDir, pc.ModulePath).Generate(apiVersionPkgList); err != nil { panic(errors.Wrap(err, "cannot generate register file")) } diff --git a/pkg/types/builder.go b/pkg/types/builder.go index a7a3e5d9..6c62a7f8 100644 --- a/pkg/types/builder.go +++ b/pkg/types/builder.go @@ -26,6 +26,15 @@ const ( emptyStruct = "struct{}" ) +// Transformation represents a transformation applied to a +// Terraform resource attribute. It's used to record any +// transformations applied by the CRD generation pipeline. +type Transformation struct { + TransformedName string + IsRef bool + IsSensitive bool +} + // Generated is a struct that holds generated types type Generated struct { Types []*types.Named @@ -33,6 +42,8 @@ type Generated struct { ForProviderType *types.Named AtProviderType *types.Named + + FieldTransformations map[string]Transformation } // Builder is used to generate Go type equivalence of given Terraform schema. @@ -53,16 +64,18 @@ func NewBuilder(pkg *types.Package) *Builder { // Build returns parameters and observation types built out of Terraform schema. func (g *Builder) Build(cfg *config.Resource) (Generated, error) { - fp, ap, err := g.buildResource(cfg.TerraformResource, cfg, nil, nil, false, cfg.Kind) + fieldTransformations := make(map[string]Transformation) + fp, ap, err := g.buildResource(cfg.TerraformResource, cfg, nil, nil, false, fieldTransformations, cfg.Kind) return Generated{ - Types: g.genTypes, - Comments: g.comments, - ForProviderType: fp, - AtProviderType: ap, + Types: g.genTypes, + Comments: g.comments, + ForProviderType: fp, + AtProviderType: ap, + FieldTransformations: fieldTransformations, }, errors.Wrapf(err, "cannot build the Types") } -func (g *Builder) buildResource(res *schema.Resource, cfg *config.Resource, tfPath []string, xpPath []string, asBlocksMode bool, names ...string) (*types.Named, *types.Named, error) { //nolint:gocyclo +func (g *Builder) buildResource(res *schema.Resource, cfg *config.Resource, tfPath []string, xpPath []string, asBlocksMode bool, t map[string]Transformation, names ...string) (*types.Named, *types.Named, error) { //nolint:gocyclo // NOTE(muvaf): There can be fields in the same CRD with same name but in // different types. Since we generate the type using the field name, there // can be collisions. In order to be able to generate unique names consistently, @@ -83,29 +96,34 @@ func (g *Builder) buildResource(res *schema.Resource, cfg *config.Resource, tfPa } var f *Field + tr := Transformation{} switch { case res.Schema[snakeFieldName].Sensitive: var drop bool - f, drop, err = NewSensitiveField(g, cfg, r, res.Schema[snakeFieldName], snakeFieldName, tfPath, xpPath, names, asBlocksMode) + f, drop, err = NewSensitiveField(g, cfg, r, res.Schema[snakeFieldName], snakeFieldName, tfPath, xpPath, names, asBlocksMode, t) if err != nil { return nil, nil, err } if drop { continue } + tr.IsSensitive = true + tr.IsRef = true case reference != nil: - f, err = NewReferenceField(g, cfg, r, res.Schema[snakeFieldName], reference, snakeFieldName, tfPath, xpPath, names, asBlocksMode) + f, err = NewReferenceField(g, cfg, r, res.Schema[snakeFieldName], reference, snakeFieldName, tfPath, xpPath, names, asBlocksMode, t) if err != nil { return nil, nil, err } + tr.IsRef = true default: - f, err = NewField(g, cfg, r, res.Schema[snakeFieldName], snakeFieldName, tfPath, xpPath, names, asBlocksMode) + f, err = NewField(g, cfg, r, res.Schema[snakeFieldName], snakeFieldName, tfPath, xpPath, names, asBlocksMode, t) if err != nil { return nil, nil, err } } - f.AddToResource(g, r, typeNames) + tr.TransformedName = f.TransformedName + t[fieldPath(f.TerraformPaths)] = tr } paramType, obsType := g.AddToBuilder(typeNames, r) @@ -130,7 +148,7 @@ func (g *Builder) AddToBuilder(typeNames *TypeNames, r *resource) (*types.Named, return paramType, obsType } -func (g *Builder) buildSchema(f *Field, cfg *config.Resource, names []string, r *resource) (types.Type, error) { // nolint:gocyclo +func (g *Builder) buildSchema(f *Field, cfg *config.Resource, t map[string]Transformation, names []string, r *resource) (types.Type, error) { // nolint:gocyclo switch f.Schema.Type { case schema.TypeBool: return types.NewPointer(types.Universe.Lookup("bool").Type()), nil @@ -160,7 +178,7 @@ func (g *Builder) buildSchema(f *Field, cfg *config.Resource, names []string, r return nil, errors.Errorf("element type of %s is basic but not one of known basic types", fieldPath(names)) } case *schema.Schema: - newf, err := NewField(g, cfg, r, et, f.Name.Snake, f.TerraformPaths, f.CRDPaths, names, false) + newf, err := NewField(g, cfg, r, et, f.Name.Snake, f.TerraformPaths, f.CRDPaths, names, false, t) if err != nil { return nil, err } @@ -172,7 +190,7 @@ func (g *Builder) buildSchema(f *Field, cfg *config.Resource, names []string, r if f.Schema.ConfigMode == schema.SchemaConfigModeAttr { asBlocksMode = true } - paramType, obsType, err := g.buildResource(et, cfg, f.TerraformPaths, f.CRDPaths, asBlocksMode, names...) + paramType, obsType, err := g.buildResource(et, cfg, f.TerraformPaths, f.CRDPaths, asBlocksMode, t, names...) if err != nil { return nil, errors.Wrapf(err, "cannot infer type from resource schema of element type of %s", fieldPath(names)) } @@ -276,8 +294,8 @@ func (r *resource) addObservationField(f *Field, field *types.Var) { r.obsTags = append(r.obsTags, fmt.Sprintf(`json:"%s" tf:"%s"`, f.JSONTag, f.TFTag)) } -func (r *resource) addReferenceFields(g *Builder, paramName *types.TypeName, field *types.Var, ref config.Reference) { - refFields, refTags := g.generateReferenceFields(paramName, field, ref) +func (r *resource) addReferenceFields(g *Builder, paramName *types.TypeName, field *types.Var, f *Field) { + refFields, refTags := g.generateReferenceFields(paramName, field, f) r.paramTags = append(r.paramTags, refTags...) r.paramFields = append(r.paramFields, refFields...) } diff --git a/pkg/types/field.go b/pkg/types/field.go index 70db51a4..857e51f5 100644 --- a/pkg/types/field.go +++ b/pkg/types/field.go @@ -25,10 +25,11 @@ type Field struct { FieldType types.Type AsBlocksMode bool Reference *config.Reference + TransformedName string } // NewField returns a constructed Field object. -func NewField(g *Builder, cfg *config.Resource, r *resource, sch *schema.Schema, snakeFieldName string, tfPath, xpPath, names []string, asBlocksMode bool) (*Field, error) { +func NewField(g *Builder, cfg *config.Resource, r *resource, sch *schema.Schema, snakeFieldName string, tfPath, xpPath, names []string, asBlocksMode bool, t map[string]Transformation) (*Field, error) { f := &Field{ Schema: sch, Name: name.NewFromSnake(snakeFieldName), @@ -43,6 +44,7 @@ func NewField(g *Builder, cfg *config.Resource, r *resource, sch *schema.Schema, f.Comment = comment f.TFTag = fmt.Sprintf("%s,omitempty", f.Name.Snake) f.JSONTag = fmt.Sprintf("%s,omitempty", f.Name.LowerCamelComputed) + f.TransformedName = f.Name.LowerCamelComputed // Terraform paths, e.g. { "lifecycle_rule", "*", "transition", "*", "days" } for https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket#lifecycle_rule f.TerraformPaths = append(tfPath, f.Name.Snake) // nolint:gocritic @@ -62,7 +64,7 @@ func NewField(g *Builder, cfg *config.Resource, r *resource, sch *schema.Schema, } } - fieldType, err := g.buildSchema(f, cfg, names, r) + fieldType, err := g.buildSchema(f, cfg, t, names, r) if err != nil { return nil, errors.Wrapf(err, "cannot infer type from schema of field %s", f.Name.Snake) } @@ -72,8 +74,8 @@ func NewField(g *Builder, cfg *config.Resource, r *resource, sch *schema.Schema, } // NewSensitiveField returns a constructed sensitive Field object. -func NewSensitiveField(g *Builder, cfg *config.Resource, r *resource, sch *schema.Schema, snakeFieldName string, tfPath, xpPath, names []string, asBlocksMode bool) (*Field, bool, error) { //nolint:gocyclo - f, err := NewField(g, cfg, r, sch, snakeFieldName, tfPath, xpPath, names, asBlocksMode) +func NewSensitiveField(g *Builder, cfg *config.Resource, r *resource, sch *schema.Schema, snakeFieldName string, tfPath, xpPath, names []string, asBlocksMode bool, t map[string]Transformation) (*Field, bool, error) { //nolint:gocyclo + f, err := NewField(g, cfg, r, sch, snakeFieldName, tfPath, xpPath, names, asBlocksMode, t) if err != nil { return nil, false, err } @@ -105,7 +107,8 @@ func NewSensitiveField(g *Builder, cfg *config.Resource, r *resource, sch *schem case "map[string]string", "map[string]*string": f.FieldType = types.NewMap(types.Universe.Lookup("string").Type(), typeSecretKeySelector) } - f.JSONTag = name.NewFromCamel(f.FieldNameCamel).LowerCamelComputed + f.TransformedName = name.NewFromCamel(f.FieldNameCamel).LowerCamelComputed + f.JSONTag = f.TransformedName if f.Schema.Optional { f.FieldType = types.NewPointer(f.FieldType) f.JSONTag += ",omitempty" @@ -114,8 +117,8 @@ func NewSensitiveField(g *Builder, cfg *config.Resource, r *resource, sch *schem } // NewReferenceField returns a constructed reference Field object. -func NewReferenceField(g *Builder, cfg *config.Resource, r *resource, sch *schema.Schema, ref *config.Reference, snakeFieldName string, tfPath, xpPath, names []string, asBlocksMode bool) (*Field, error) { - f, err := NewField(g, cfg, r, sch, snakeFieldName, tfPath, xpPath, names, asBlocksMode) +func NewReferenceField(g *Builder, cfg *config.Resource, r *resource, sch *schema.Schema, ref *config.Reference, snakeFieldName string, tfPath, xpPath, names []string, asBlocksMode bool, t map[string]Transformation) (*Field, error) { + f, err := NewField(g, cfg, r, sch, snakeFieldName, tfPath, xpPath, names, asBlocksMode, t) if err != nil { return nil, err } @@ -148,7 +151,7 @@ func (f *Field) AddToResource(g *Builder, r *resource, typeNames *TypeNames) { } if f.Reference != nil { - r.addReferenceFields(g, typeNames.ParameterTypeName, field, *f.Reference) + r.addReferenceFields(g, typeNames.ParameterTypeName, field, f) } g.comments.AddFieldComment(typeNames.ParameterTypeName, f.FieldNameCamel, f.Comment.Build()) diff --git a/pkg/types/reference.go b/pkg/types/reference.go index 53409782..e857c7a5 100644 --- a/pkg/types/reference.go +++ b/pkg/types/reference.go @@ -9,7 +9,6 @@ import ( "github.com/pkg/errors" "golang.org/x/tools/go/packages" - "github.com/upbound/upjet/pkg/config" "github.com/upbound/upjet/pkg/types/comments" "github.com/upbound/upjet/pkg/types/name" ) @@ -27,10 +26,10 @@ var typeSelectorField types.Type var typeSecretKeySelector types.Type var commentOptional *comments.Comment -func (g *Builder) generateReferenceFields(t *types.TypeName, f *types.Var, r config.Reference) (fields []*types.Var, tags []string) { +func (g *Builder) generateReferenceFields(t *types.TypeName, f *types.Var, field *Field) (fields []*types.Var, tags []string) { _, isSlice := f.Type().(*types.Slice) - rfn := r.RefFieldName + rfn := field.Reference.RefFieldName if rfn == "" { rfn = f.Name() + "Ref" if isSlice { @@ -38,7 +37,7 @@ func (g *Builder) generateReferenceFields(t *types.TypeName, f *types.Var, r con } } - sfn := r.SelectorFieldName + sfn := field.Reference.SelectorFieldName if sfn == "" { sfn = f.Name() + "Selector" } @@ -58,6 +57,7 @@ func (g *Builder) generateReferenceFields(t *types.TypeName, f *types.Var, r con g.comments.AddFieldComment(t, rfn, commentOptional.Build()) g.comments.AddFieldComment(t, sfn, commentOptional.Build()) + field.TransformedName = rn.LowerCamelComputed return []*types.Var{ref, sel}, []string{refTag, selTag} } diff --git a/pkg/types/reference_test.go b/pkg/types/reference_test.go index 215c3239..91e4e650 100644 --- a/pkg/types/reference_test.go +++ b/pkg/types/reference_test.go @@ -124,7 +124,7 @@ func TestBuilder_generateReferenceFields(t *testing.T) { g := &Builder{ comments: twtypes.Comments{}, } - gotFields, gotTags := g.generateReferenceFields(tc.args.t, tc.args.f, tc.args.r) + gotFields, gotTags := g.generateReferenceFields(tc.args.t, tc.args.f, &Field{Reference: &tc.args.r}) if diff := cmp.Diff(tc.want.outFields, gotFields, cmp.Comparer(func(a, b *types.Var) bool { return a.String() == b.String() })); diff != "" {