From 11eb059d3256e2a998847c3bc2611a993a05609a Mon Sep 17 00:00:00 2001 From: kklimonda-cl Date: Mon, 29 Jul 2024 09:27:14 +0200 Subject: [PATCH] feat: Added generation of location-related code across all terraform templates (#118) --- pkg/translate/terraform_provider/funcs.go | 334 +++++++++++++++++- pkg/translate/terraform_provider/helper.go | 1 + pkg/translate/terraform_provider/template.go | 144 ++------ .../terraform_provider_file.go | 81 +++-- .../terraform_provider_file_test.go | 4 +- 5 files changed, 411 insertions(+), 153 deletions(-) diff --git a/pkg/translate/terraform_provider/funcs.go b/pkg/translate/terraform_provider/funcs.go index c0be4a75..575ecb86 100644 --- a/pkg/translate/terraform_provider/funcs.go +++ b/pkg/translate/terraform_provider/funcs.go @@ -375,9 +375,310 @@ func RenderCopyFromPangoFunctions(pkgName string, terraformTypePrefix string, pr return processTemplate(copyFromPangoTmpl, "copy-from-pango", data, funcMap) } -func ResourceCreateFunction(structName string, serviceName string, paramSpec *properties.Normalization, terraformProvider *properties.TerraformProviderFile, resourceSDKName string) (string, error) { +const renderLocationTmpl = ` +{{- range .Locations }} +type {{ .StructName }} struct { + {{- range .Fields }} + {{ .Name }} {{ .Type }} {{ range .Tags }}{{ . }} {{ end }} + {{- end }} +} +{{- end }} +` + +func RenderLocationStructs(names *NameProvider, spec *properties.Normalization) (string, error) { + type fieldCtx struct { + Name string + Type string + Tags []string + } + + type locationCtx struct { + StructName string + Fields []fieldCtx + } + + type context struct { + Locations []locationCtx + } + + var locations []locationCtx + + // Create the top location structure that references other locations + topLocation := locationCtx{ + StructName: fmt.Sprintf("%sLocation", names.StructName), + } + + for name, data := range spec.Locations { + structName := fmt.Sprintf("%s%sLocation", names.StructName, pascalCase(name)) + tfTag := fmt.Sprintf("`tfsdk:\"%s\"`", name) + var structType string + if len(data.Vars) > 0 { + structType = fmt.Sprintf("*%s", structName) + } else { + structType = "types.Bool" + } + + topLocation.Fields = append(topLocation.Fields, fieldCtx{ + Name: pascalCase(name), + Type: structType, + Tags: []string{tfTag}, + }) + + if len(data.Vars) == 0 { + continue + } + + var fields []fieldCtx + for paramName, param := range data.Vars { + paramTag := fmt.Sprintf("`tfsdk:\"%s\"`", paramName) + fields = append(fields, fieldCtx{ + Name: param.Name.CamelCase, + Type: "types.String", + Tags: []string{paramTag}, + }) + } + + location := locationCtx{ + StructName: structName, + Fields: fields, + } + locations = append(locations, location) + } + + locations = append(locations, topLocation) + + data := context{ + Locations: locations, + } + return processTemplate(renderLocationTmpl, "render-location-structs", data, commonFuncMap) +} + +const locationSchemaGetterTmpl = ` +{{- define "renderLocationAttribute" }} +"{{ .Name }}": {{ .SchemaType }}{ + Description: "{{ .Description }}", + {{- if .Required }} + Required: true + {{- else }} + Optional: true, + {{- end }} + {{- if .Computed }} + Computed: true, + {{- end }} + {{- if .Default }} + Default: {{ .Default.Type }}({{ .Default.Value }}), + {{- end }} + {{- if .Attributes }} + Attributes: map[string]rsschema.Attribute{ + {{- range .Attributes }} + {{- template "renderLocationAttribute" . }} + {{- end }} + }, + {{- end }} + PlanModifiers: []planmodifier.{{ .ModifierType }}{ + {{ .ModifierType | LowerCase }}planmodifier.RequiresReplace(), + }, +}, +{{- end }} + +func {{ .StructName }}LocationsSchema() rsschema.Attribute { + {{- with .Schema }} + return rsschema.SingleNestedAttribute{ + Description: "{{ .Description }}", + Required: true, + Attributes: map[string]rsschema.Attribute{ +{{- range .Attributes }} +{{- template "renderLocationAttribute" . }} +{{- end }} + }, + } +} + {{- end }} +` + +func RenderLocationSchemaGetter(names *NameProvider, spec *properties.Normalization) (string, error) { + type defaultCtx struct { + Type string + Value string + } + + type attributeCtx struct { + Name string + SchemaType string + Description string + Required bool + Computed bool + Default *defaultCtx + ModifierType string + Attributes []attributeCtx + } + + var attributes []attributeCtx + for _, data := range spec.Locations { + var schemaType string + if len(data.Vars) == 0 { + schemaType = "rsschema.BoolAttribute" + } else { + schemaType = "rsschema.SingleNestedAttribute" + } + + var variableAttrs []attributeCtx + for _, variable := range data.Vars { + attribute := attributeCtx{ + Name: variable.Name.Underscore, + Description: variable.Description, + SchemaType: "rsschema.StringAttribute", + Required: false, + Computed: true, + Default: &defaultCtx{ + Type: "stringdefault.StaticString", + Value: fmt.Sprintf(`"%s"`, variable.Default), + }, + ModifierType: "String", + } + variableAttrs = append(variableAttrs, attribute) + } + + var modifierType string + if len(variableAttrs) > 0 { + modifierType = "Object" + } else { + modifierType = "Bool" + } + + attribute := attributeCtx{ + Name: data.Name.Underscore, + SchemaType: schemaType, + Description: data.Description, + Required: false, + Attributes: variableAttrs, + ModifierType: modifierType, + } + attributes = append(attributes, attribute) + } + + topAttribute := attributeCtx{ + Name: "location", + SchemaType: "rsschema.SingleNestedAttribute", + Description: "The location of this object.", + Required: true, + Attributes: attributes, + ModifierType: "Object", + } + + type context struct { + StructName string + Schema attributeCtx + } + + data := context{ + StructName: names.StructName, + Schema: topAttribute, + } + + return processTemplate(locationSchemaGetterTmpl, "render-location-schema-getter", data, commonFuncMap) +} + +type locationFieldCtx struct { + Name string + Type string +} + +type locationCtx struct { + Name string + TerraformStructName string + SdkStructName string + IsBool bool + Fields []locationFieldCtx +} + +func renderLocationsGetContext(names *NameProvider, spec *properties.Normalization) []locationCtx { + var locations []locationCtx + + for _, location := range spec.Locations { + var fields []locationFieldCtx + for _, variable := range location.Vars { + fields = append(fields, locationFieldCtx{ + Name: variable.Name.CamelCase, + Type: "String", + }) + } + locations = append(locations, locationCtx{ + Name: location.Name.CamelCase, + TerraformStructName: fmt.Sprintf("%s%sLocation", names.StructName, location.Name.CamelCase), + SdkStructName: fmt.Sprintf("%s.%sLocation", names.PackageName, location.Name.CamelCase), + IsBool: len(location.Vars) == 0, + Fields: fields, + }) + } + + type context struct { + Locations []locationCtx + } + + return locations +} + +const locationsPangoToState = ` +{{- range .Locations }} + {{- if .IsBool }} +if loc.Location.{{ .Name }} { + state.Location.{{ .Name }} = types.BoolValue(true) +} + {{- else }} +if loc.Location.{{ .Name }} != nil { + location := &{{ .TerraformStructName }}{ + {{ $locationName := .Name }} + {{- range .Fields }} + {{ .Name }}: types.{{ .Type }}Value(loc.Location.{{ $locationName }}.{{ .Name }}), + {{- end }} + } + state.Location.{{ .Name }} = location +} + {{- end }} +{{- end }} +` + +func RenderLocationsPangoToState(names *NameProvider, spec *properties.Normalization) (string, error) { + type context struct { + Locations []locationCtx + } + data := context{Locations: renderLocationsGetContext(names, spec)} + return processTemplate(locationsPangoToState, "render-locations-pango-to-state", data, commonFuncMap) +} + +const locationsStateToPango = ` +{{- range .Locations }} + {{- if .IsBool }} +if !state.Location.{{ .Name }}.IsNull() && state.Location.{{ .Name }}.ValueBool() { + loc.Location.{{ .Name }} = true +} + {{- else }} +if state.Location.{{ .Name }} != nil { + location := &{{ .SdkStructName }}{ + {{ $locationName := .Name }} + {{- range .Fields }} + {{ .Name }}: state.Location.{{ $locationName }}.{{ .Name }}.ValueString(), + {{- end }} + } + loc.Location.{{ .Name }} = location +} + {{- end }} +{{- end }} +` + +func RenderLocationsStateToPango(names *NameProvider, spec *properties.Normalization) (string, error) { + type context struct { + Locations []locationCtx + } + data := context{Locations: renderLocationsGetContext(names, spec)} + return processTemplate(locationsStateToPango, "render-locations-state-to-pango", data, commonFuncMap) +} + +func ResourceCreateFunction(names *NameProvider, serviceName string, paramSpec *properties.Normalization, terraformProvider *properties.TerraformProviderFile, resourceSDKName string) (string, error) { funcMap := template.FuncMap{ - "ConfigToEntry": ConfigEntry, + "ConfigToEntry": ConfigEntry, + "RenderLocationsStateToPango": func() (string, error) { return RenderLocationsStateToPango(names, paramSpec) }, "ResourceParamToSchema": func(paramName string, paramParameters properties.SpecParam) (string, error) { return ParamToSchemaResource(paramName, paramParameters, terraformProvider) }, @@ -388,7 +689,7 @@ func ResourceCreateFunction(structName string, serviceName string, paramSpec *pr } data := map[string]interface{}{ - "structName": structName, + "structName": names.ResourceStructName, "serviceName": naming.CamelCase("", serviceName, "", false), "paramSpec": paramSpec.Spec, "resourceSDKName": resourceSDKName, @@ -398,33 +699,42 @@ func ResourceCreateFunction(structName string, serviceName string, paramSpec *pr return processTemplate(resourceCreateFunction, "resource-create-function", data, funcMap) } -func ResourceReadFunction(structName string, serviceName string, paramSpec *properties.Normalization, resourceSDKName string) (string, error) { +func ResourceReadFunction(names *NameProvider, serviceName string, paramSpec *properties.Normalization, resourceSDKName string) (string, error) { if strings.Contains(serviceName, "group") { serviceName = "group" } data := map[string]interface{}{ - "structName": structName, - "serviceName": naming.CamelCase("", serviceName, "", false), - "resourceSDKName": resourceSDKName, - "locations": paramSpec.Locations, + "structName": names.StructName, + "resourceStructName": names.ResourceStructName, + "serviceName": naming.CamelCase("", serviceName, "", false), + "resourceSDKName": resourceSDKName, + "locations": paramSpec.Locations, } - return processTemplate(resourceReadFunction, "resource-read-function", data, nil) + funcMap := template.FuncMap{ + "RenderLocationsPangoToState": func() (string, error) { return RenderLocationsPangoToState(names, paramSpec) }, + } + + return processTemplate(resourceReadFunction, "resource-read-function", data, funcMap) } -func ResourceUpdateFunction(structName string, serviceName string, paramSpec interface{}, resourceSDKName string) (string, error) { +func ResourceUpdateFunction(names *NameProvider, serviceName string, paramSpec *properties.Normalization, resourceSDKName string) (string, error) { if strings.Contains(serviceName, "group") { serviceName = "group" } data := map[string]interface{}{ - "structName": structName, + "structName": names.ResourceStructName, "serviceName": naming.CamelCase("", serviceName, "", false), "resourceSDKName": resourceSDKName, } - return processTemplate(resourceUpdateFunction, "resource-update-function", data, nil) + funcMap := template.FuncMap{ + "RenderLocationsStateToPango": func() (string, error) { return RenderLocationsStateToPango(names, paramSpec) }, + } + + return processTemplate(resourceUpdateFunction, "resource-update-function", data, funcMap) } func ResourceDeleteFunction(structName string, serviceName string, paramSpec interface{}, resourceSDKName string) (string, error) { diff --git a/pkg/translate/terraform_provider/helper.go b/pkg/translate/terraform_provider/helper.go index 57fee1f7..84fac61c 100644 --- a/pkg/translate/terraform_provider/helper.go +++ b/pkg/translate/terraform_provider/helper.go @@ -12,6 +12,7 @@ import ( // Package-level function map to avoid repetition in each function var commonFuncMap = template.FuncMap{ "Map": templateMap, + "LowerCase": func(value string) string { return strings.ToLower(value) }, "CamelCaseName": func(paramName string) string { return naming.CamelCase("", paramName, "", true) }, "UnderscoreName": func(paramName string) string { return naming.Underscore("", paramName, "") }, "CamelCaseType": func(paramType string) string { return naming.CamelCase("", paramType, "", true) }, diff --git a/pkg/translate/terraform_provider/template.go b/pkg/translate/terraform_provider/template.go index defc2f8c..995af897 100644 --- a/pkg/translate/terraform_provider/template.go +++ b/pkg/translate/terraform_provider/template.go @@ -120,24 +120,24 @@ const resourceObj = ` // Generate Terraform Resource object var ( - _ resource.Resource = &{{ structName }}{} - _ resource.ResourceWithConfigure = &{{ structName }}{} - _ resource.ResourceWithImportState = &{{ structName }}{} + _ resource.Resource = &{{ resourceStructName }}{} + _ resource.ResourceWithConfigure = &{{ resourceStructName }}{} + _ resource.ResourceWithImportState = &{{ resourceStructName }}{} ) -func New{{ structName }}() resource.Resource { - return &{{ structName }}{} +func New{{ resourceStructName }}() resource.Resource { + return &{{ resourceStructName }}{} } -type {{ structName }} struct { +type {{ resourceStructName }} struct { client *pango.Client } -type {{ structName }}Tfid struct { +type {{ resourceStructName }}Tfid struct { {{ CreateTfIdStruct }} } -func (o *{{ structName }}Tfid) IsValid() error { +func (o *{{ resourceStructName }}Tfid) IsValid() error { if o.Name == "" { return fmt.Errorf("name is unspecified") } @@ -145,48 +145,37 @@ func (o *{{ structName }}Tfid) IsValid() error { return o.Location.IsValid() } -type {{ structName }}Location struct { -// TODO: Generate Location struct via function - {{- CreateLocationStruct structName }} -} +{{- RenderLocationStructs }} -type {{ structName }}VsysLocation struct { -// TODO: Generate Location struct via function - {{- CreateLocationVsysStruct structName }} -} +{{- RenderLocationSchemaGetter }} -type {{ structName }}DeviceGroupLocation struct { -// TODO: Generate Device Group struct via function - {{- CreateLocationDeviceGroupStruct structName }} -} - -type {{ structName }}Model struct { +type {{ resourceStructName }}Model struct { {{ CreateTfIdResourceModel }} Name types.String` + "`" + `tfsdk:"name"` + "`" + ` {{- range $pName, $pParam := $.Spec.Params}} - {{- ParamToModelResource $pName $pParam structName -}} + {{- ParamToModelResource $pName $pParam resourceStructName -}} {{- end}} {{- range $pName, $pParam := $.Spec.OneOf}} - {{- ParamToModelResource $pName $pParam structName -}} + {{- ParamToModelResource $pName $pParam resourceStructName -}} {{- end}} } {{- range $pName, $pParam := $.Spec.Params}} - {{ ModelNestedStruct $pName $pParam structName }} + {{ ModelNestedStruct $pName $pParam resourceStructName }} {{- end}} {{- range $pName, $pParam := $.Spec.OneOf}} - {{ ModelNestedStruct $pName $pParam structName }} + {{ ModelNestedStruct $pName $pParam resourceStructName }} {{- end}} -func (r *{{ structName }}) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { +func (r *{{ resourceStructName }}) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "{{ metaName }}" } -func (r *{{ structName }}) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { +func (r *{{ resourceStructName }}) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = rsschema.Schema{ Description: "", Attributes: map[string]rsschema.Attribute{ - {{- ResourceSchemaLocationAttribute }} + "location": {{ structName }}LocationsSchema(), "name": rsschema.StringAttribute{ Description: "The name of the resource.", Required: true, @@ -201,7 +190,7 @@ func (r *{{ structName }}) Schema(ctx context.Context, req resource.SchemaReques } } -func (r *{{ structName }}) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { +func (r *{{ resourceStructName }}) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { // Prevent panic if the provider has not been configured. if req.ProviderData == nil { return @@ -214,23 +203,23 @@ func (r *{{ structName }}) Configure(ctx context.Context, req resource.Configure {{ RenderCopyFromPangoFunctions }} -func (r *{{ structName }}) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - {{ ResourceCreateFunction structName serviceName}} +func (r *{{ resourceStructName }}) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + {{ ResourceCreateFunction resourceStructName serviceName}} } -func (r *{{ structName }}) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - {{ ResourceReadFunction structName serviceName}} +func (r *{{ resourceStructName }}) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + {{ ResourceReadFunction resourceStructName serviceName}} } -func (r *{{ structName }}) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - {{ ResourceUpdateFunction structName serviceName}} +func (r *{{ resourceStructName }}) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + {{ ResourceUpdateFunction resourceStructName serviceName}} } -func (r *{{ structName }}) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - {{ ResourceDeleteFunction structName serviceName}} +func (r *{{ resourceStructName }}) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + {{ ResourceDeleteFunction resourceStructName serviceName}} } -func (r *{{ structName }}) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { +func (r *{{ resourceStructName }}) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("tfid"), req, resp) } @@ -264,35 +253,7 @@ const resourceCreateFunction = ` loc := {{ .structName }}Tfid{Name: state.Name.ValueString()} // TODO: this needs to handle location structure for UUID style shared has nested structure type - {{- if .locations.shared }} - if !state.Location.Shared.IsNull() && state.Location.Shared.ValueBool() { - loc.Location.Shared = true - } - {{- end }} - - {{- if .locations.from_panorama_shared }} - if !state.Location.FromPanorama.IsNull() && state.Location.FromPanorama.ValueBool() { - loc.Location.FromPanoramaShared = true - } - {{- end }} - - {{- if .locations.vsys }} - if state.Location.Vsys != nil { - loc.Location.Vsys = &{{ .serviceName }}.VsysLocation{} - loc.Location.Vsys.NgfwDevice = state.Location.Vsys.NgfwDevice.ValueString() - } - {{- end }} - - {{- if .locations.panorama }} - if state.Location.DeviceGroup != nil { - loc.Location = {{ .resourceSDKName }}.Location{ - Panorama: &{{ .resourceSDKName }}.PanoramaLocation{ - PanoramaDevice: state.Location.DeviceGroup.PanoramaDevice.ValueString(), - }, - } - } - {{- end }} - + {{ RenderLocationsStateToPango }} if err := loc.IsValid(); err != nil { resp.Diagnostics.AddError("Invalid location", err.Error()) @@ -340,14 +301,14 @@ const resourceCreateFunction = ` ` const resourceReadFunction = ` - var savestate, state {{ .structName }}Model + var savestate, state {{ .resourceStructName }}Model resp.Diagnostics.Append(req.State.Get(ctx, &savestate)...) if resp.Diagnostics.HasError() { return } // Parse the location from tfid. - var loc {{ .structName }}Tfid + var loc {{ .resourceStructName }}Tfid if err := DecodeLocation(savestate.Tfid.ValueString(), &loc); err != nil { resp.Diagnostics.AddError("Error parsing tfid", err.Error()) return @@ -355,7 +316,7 @@ const resourceReadFunction = ` // Basic logging. tflog.Info(ctx, "performing resource read", map[string]any{ - "resource_name": "panos_{{ UnderscoreName .structName }}", + "resource_name": "panos_{{ UnderscoreName .resourceStructName }}", "function": "Read", "name": loc.Name, }) @@ -386,30 +347,7 @@ const resourceReadFunction = ` return } - // Save location to state. - {{- if .locations.shared }} - if loc.Location.Shared { - state.Location.Shared = types.BoolValue(true) - } - {{- end }} - {{- if .locations.from_panorama_shared }} - if loc.Location.FromPanoramaShared { - state.Location.FromPanorama = types.BoolValue(true) - } - {{- end }} - {{- if .locations.vsys }} - if loc.Location.Vsys != nil { - state.Location.Vsys = &{{ .structName }}VsysLocation{} - state.Location.Vsys.NgfwDevice = types.StringValue(loc.Location.Vsys.NgfwDevice) - } - {{- end }} - {{- if .locations.panorama }} - if loc.Location.Panorama != nil { - state.Location.DeviceGroup = &{{ .structName }}DeviceGroupLocation{ - PanoramaDevice: types.StringValue(loc.Location.Panorama.PanoramaDevice), - } - } - {{- end }} + {{ RenderLocationsPangoToState }} /* // Keep the timeouts. @@ -568,22 +506,6 @@ type {{ structName }}Filter struct { //TODO: Generate Data Source filter via function } -type {{ structName }}Location struct { -//TODO: Generate Data Source Location via function -} - -type {{ structName }}SharedLocation struct { -//TODO: Generate Data Source Location shared via function -} - -type {{ structName }}VsysLocation struct { -//TODO: Generate Data Source Location vsys via function -} - -type {{ structName }}DeviceGroupLocation struct { -//TODO: Generate Data Source Location Device Group via function -} - type {{ structName }}Entry struct { // TODO: Entry model struct via function } diff --git a/pkg/translate/terraform_provider/terraform_provider_file.go b/pkg/translate/terraform_provider/terraform_provider_file.go index 620abd6a..8d222b15 100644 --- a/pkg/translate/terraform_provider/terraform_provider_file.go +++ b/pkg/translate/terraform_provider/terraform_provider_file.go @@ -6,15 +6,19 @@ import ( "strings" "text/template" + "github.com/paloaltonetworks/pan-os-codegen/pkg/imports" "github.com/paloaltonetworks/pan-os-codegen/pkg/naming" "github.com/paloaltonetworks/pan-os-codegen/pkg/properties" ) // NameProvider encapsulates naming conventions for Terraform resources. type NameProvider struct { - TfName string - MetaName string - StructName string + TfName string + MetaName string + StructName string + DataSourceStructName string + ResourceStructName string + PackageName string } // NewNameProvider creates a new NameProvider based on given specifications. @@ -28,8 +32,11 @@ func NewNameProvider(spec *properties.Normalization, resourceName string) *NameP objectName = tfName } metaName := fmt.Sprintf("_%s", naming.Underscore("", strings.ToLower(objectName), "")) - structName := naming.CamelCase("", tfName, resourceName, true) - return &NameProvider{tfName, metaName, structName} + structName := naming.CamelCase("", tfName, "", true) + dataSourceStructName := naming.CamelCase("", tfName, "DataSource", true) + resourceStructName := naming.CamelCase("", tfName, "Resource", true) + packageName := spec.GoSdkPath[len(spec.GoSdkPath)-1] + return &NameProvider{tfName, metaName, structName, dataSourceStructName, resourceStructName, packageName} } // GenerateTerraformProvider handles the generation of Terraform resources and data sources. @@ -48,12 +55,12 @@ func (g *GenerateTerraformProvider) executeTemplate(template *template.Template, return fmt.Errorf("error executing %s template: %v", resourceType, err) } renderedTemplate.WriteString("\n") - return g.updateProviderFile(&renderedTemplate, terraformProvider, resourceType, names.StructName) + return g.updateProviderFile(&renderedTemplate, terraformProvider, resourceType, names) } // updateProviderFile updates the Terraform provider file by appending the rendered template // to the appropriate slice in the TerraformProviderFile based on the provided resourceType. -func (g *GenerateTerraformProvider) updateProviderFile(renderedTemplate *strings.Builder, terraformProvider *properties.TerraformProviderFile, resourceType string, structName string) error { +func (g *GenerateTerraformProvider) updateProviderFile(renderedTemplate *strings.Builder, terraformProvider *properties.TerraformProviderFile, resourceType string, names *NameProvider) error { switch resourceType { case "ProviderFile": terraformProvider.Code = renderedTemplate @@ -62,17 +69,17 @@ func (g *GenerateTerraformProvider) updateProviderFile(renderedTemplate *strings return fmt.Errorf("error writing %s template: %v", resourceType, err) } } - return g.appendResourceType(terraformProvider, resourceType, structName) + return g.appendResourceType(terraformProvider, resourceType, names) } // appendResourceType appends the given struct name to the appropriate slice in the TerraformProviderFile // based on the provided resourceType. -func (g *GenerateTerraformProvider) appendResourceType(terraformProvider *properties.TerraformProviderFile, resourceType string, structName string) error { +func (g *GenerateTerraformProvider) appendResourceType(terraformProvider *properties.TerraformProviderFile, resourceType string, names *NameProvider) error { switch resourceType { case "DataSource", "DataSourceList": - terraformProvider.DataSources = append(terraformProvider.DataSources, structName) + terraformProvider.DataSources = append(terraformProvider.DataSources, names.DataSourceStructName) case "Resource": - terraformProvider.Resources = append(terraformProvider.Resources, structName) + terraformProvider.Resources = append(terraformProvider.Resources, names.ResourceStructName) } return nil } @@ -95,28 +102,32 @@ func (g *GenerateTerraformProvider) GenerateTerraformResource(spec *properties.N resourceType := "Resource" names := NewNameProvider(spec, resourceType) funcMap := template.FuncMap{ - "metaName": func() string { return names.MetaName }, - "structName": func() string { return names.StructName }, - "serviceName": func() string { return names.TfName }, - "CreateTfIdStruct": func() (string, error) { return CreateTfIdStruct("entry", spec.GoSdkPath[len(spec.GoSdkPath)-1]) }, - "CreateTfIdResourceModel": func() (string, error) { return CreateTfIdResourceModel("entry", names.StructName) }, + "metaName": func() string { return names.MetaName }, + "structName": func() string { return names.StructName }, + "dataSourceStructName": func() string { return names.DataSourceStructName }, + "resourceStructName": func() string { return names.ResourceStructName }, + "serviceName": func() string { return names.TfName }, + "CreateTfIdStruct": func() (string, error) { return CreateTfIdStruct("entry", spec.GoSdkPath[len(spec.GoSdkPath)-1]) }, + "CreateTfIdResourceModel": func() (string, error) { return CreateTfIdResourceModel("entry", names.StructName) }, + "RenderLocationStructs": func() (string, error) { return RenderLocationStructs(names, spec) }, + "RenderLocationSchemaGetter": func() (string, error) { return RenderLocationSchemaGetter(names, spec) }, "RenderCopyToPangoFunctions": func() (string, error) { - return RenderCopyToPangoFunctions(spec.GoSdkPath[len(spec.GoSdkPath)-1], names.StructName, spec) + return RenderCopyToPangoFunctions(names.PackageName, names.ResourceStructName, spec) }, "RenderCopyFromPangoFunctions": func() (string, error) { - return RenderCopyFromPangoFunctions(spec.GoSdkPath[len(spec.GoSdkPath)-1], names.StructName, spec) + return RenderCopyFromPangoFunctions(names.PackageName, names.ResourceStructName, spec) }, "ResourceCreateFunction": func(structName string, serviceName string) (string, error) { - return ResourceCreateFunction(structName, serviceName, spec, terraformProvider, spec.GoSdkPath[len(spec.GoSdkPath)-1]) + return ResourceCreateFunction(names, serviceName, spec, terraformProvider, names.PackageName) }, "ResourceReadFunction": func(structName string, serviceName string) (string, error) { - return ResourceReadFunction(structName, serviceName, spec, spec.GoSdkPath[len(spec.GoSdkPath)-1]) + return ResourceReadFunction(names, serviceName, spec, names.PackageName) }, "ResourceUpdateFunction": func(structName string, serviceName string) (string, error) { - return ResourceUpdateFunction(structName, serviceName, spec.Spec, spec.GoSdkPath[len(spec.GoSdkPath)-1]) + return ResourceUpdateFunction(names, serviceName, spec, names.PackageName) }, "ResourceDeleteFunction": func(structName string, serviceName string) (string, error) { - return ResourceDeleteFunction(structName, serviceName, spec.Spec, spec.GoSdkPath[len(spec.GoSdkPath)-1]) + return ResourceDeleteFunction(structName, serviceName, spec.Spec, names.PackageName) }, "ParamToModelResource": ParamToModelResource, "ModelNestedStruct": ModelNestedStruct, @@ -170,18 +181,16 @@ func (g *GenerateTerraformProvider) GenerateTerraformResource(spec *properties.N // Generate Resource with entry style terraformProvider.ImportManager.AddSdkImport(sdkPkgPath(spec), "") + conditionallyAddValidators(terraformProvider.ImportManager, spec) + conditionallyAddModifiers(terraformProvider.ImportManager, spec) + terraformProvider.ImportManager.AddStandardImport("fmt", "") terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/path", "") terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/diag", "") terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/resource", "") terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/resource/schema", "rsschema") - terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier", "") - terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier", "") terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault", "") - terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier", "") - terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/schema/validator", "") terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/types", "") - terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator", "") terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-log/tflog", "") err := g.generateTerraformEntityTemplate(resourceType, names, spec, terraformProvider, resourceObj, funcMap) @@ -232,7 +241,7 @@ func (g *GenerateTerraformProvider) GenerateTerraformDataSource(spec *properties names := NewNameProvider(spec, resourceType) funcMap := template.FuncMap{ "metaName": func() string { return names.MetaName }, - "structName": func() string { return names.StructName }, + "structName": func() string { return names.DataSourceStructName }, } err := g.generateTerraformEntityTemplate(resourceType, names, spec, terraformProvider, dataSourceSingletonObj, funcMap) if err != nil { @@ -301,3 +310,19 @@ func sdkPkgPath(spec *properties.Normalization) string { return path } + +func conditionallyAddValidators(manager *imports.Manager, spec *properties.Normalization) { + +} + +func conditionallyAddModifiers(manager *imports.Manager, spec *properties.Normalization) { + manager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier", "") + for _, loc := range spec.Locations { + if len(loc.Vars) == 0 { + manager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier", "") + } else { + manager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier", "") + manager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier", "") + } + } +} diff --git a/pkg/translate/terraform_provider/terraform_provider_file_test.go b/pkg/translate/terraform_provider/terraform_provider_file_test.go index 1e2e7f38..655287f4 100644 --- a/pkg/translate/terraform_provider/terraform_provider_file_test.go +++ b/pkg/translate/terraform_provider/terraform_provider_file_test.go @@ -33,7 +33,7 @@ func TestExecuteTemplate(t *testing.T) { tmpl, _ := template.New("test").Parse("Name: {{.Name}}") spec := &properties.Normalization{Name: "testResource"} terraformProvider := &properties.TerraformProviderFile{Code: new(strings.Builder)} - names := &NameProvider{TfName: "testResource", MetaName: "_testResource", StructName: "TestResource"} + names := &NameProvider{TfName: "testResource", MetaName: "_testResource", ResourceStructName: "TestResource"} // When err := g.executeTemplate(tmpl, spec, terraformProvider, "Resource", names) @@ -46,7 +46,7 @@ func TestExecuteTemplate(t *testing.T) { func TestGenerateTerraformEntityTemplate(t *testing.T) { // Given g := &GenerateTerraformProvider{} - names := &NameProvider{TfName: "testResource", MetaName: "_testResource", StructName: "TestResource"} + names := &NameProvider{TfName: "testResource", MetaName: "_testResource", ResourceStructName: "TestResource"} spec := &properties.Normalization{Name: "testResource"} terraformProvider := &properties.TerraformProviderFile{Code: new(strings.Builder)} templateStr := "Name: {{.Name}}"