diff --git a/.gitignore b/.gitignore index 66fd13c..14fdb8f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,8 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out -# Dependency directories (remove the comment below to include it) -# vendor/ +# Dependency directories +vendor/ + +#generated outputs +internal/ \ No newline at end of file diff --git a/.go-version b/.go-version new file mode 100644 index 0000000..de646d2 --- /dev/null +++ b/.go-version @@ -0,0 +1 @@ +1.16.6 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..22d364a --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/tfpdk.iml b/.idea/tfpdk.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/tfpdk.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index cc2d6fb..c436c34 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ # tfpdk Terraform Provider Development Kit + +## TODO +- [ ] untyped resource template +- [ ] `go fmt` outputs +- [ ] typed and untyped Data Sources +- [ ] init a new provider - git clone [scaffold](https://github.com/hashicorp/terraform-provider-scaffolding)? +- [ ] optionally populate the Typed SDK into the init step? +- [ ] Dynamically read provider name for item generation +- [ ] Populate `IDValidationFunc()` in template for IDs (Pandora) diff --git a/commands/common.go b/commands/common.go new file mode 100644 index 0000000..252e30d --- /dev/null +++ b/commands/common.go @@ -0,0 +1,17 @@ +package commands + +import ( + "strings" + "text/template" + + "github.com/iancoleman/strcase" + "github.com/jackofallops/tfpdk/helpers" +) + +var TplFuncMap = template.FuncMap{ + "ToLower": strings.ToLower, + "ToTitle": strings.Title, + "ToCamel": strcase.ToCamel, + "ToSnake": strcase.ToSnake, + "TfName": helpers.TerraformResourceName, +} diff --git a/commands/initialise.go b/commands/initialise.go new file mode 100644 index 0000000..85bf852 --- /dev/null +++ b/commands/initialise.go @@ -0,0 +1,16 @@ +package commands + +type InitialiseCommand struct{} + +func (i InitialiseCommand) Run(args []string) int { + // TODO - Clone the scaffold project to path from `args`? + return 0 +} + +func (InitialiseCommand) Help() string { + return "" +} + +func (InitialiseCommand) Synopsis() string { + return "" +} diff --git a/commands/resource.go b/commands/resource.go new file mode 100644 index 0000000..acc1371 --- /dev/null +++ b/commands/resource.go @@ -0,0 +1,107 @@ +package commands + +import ( + "fmt" + "os" + "strings" + "text/template" + + "github.com/iancoleman/strcase" +) + +type ResourceCommand struct { +} + +type TypedResourceData struct { + Name string + ServicePackage string + NoUpdate bool + ProviderName string + Typed bool +} + +func (r ResourceCommand) Run(args []string) int { + data := TypedResourceData{ + ProviderName: "azurerm", // Defaulting for now, will need to eval from os.path later + Typed: true, //defaulting to true for now, since AzureRM is the prototype target + } + if len(args) == 0 { + fmt.Print(r.Help()) + return 1 + } + for _, v := range args { + arg := strings.Split(v, "=") + if len(arg) > 2 { + fmt.Printf("malformed argument %q", arg) + return 1 + } + + switch strings.ToLower(strings.TrimLeft(arg[0], "-")) { + case "name": + data.Name = arg[1] + case "no-update": + data.NoUpdate = true + case "servicepackage": + data.ServicePackage = arg[1] + case "typed": + data.Typed = true + default: + fmt.Printf("unrecognised option %q", arg[0]) + return 1 + } + } + + tpl := template.Must(template.New("resource.gotpl").Funcs(TplFuncMap).ParseFS(Templatedir, "templates/resource.gotpl")) + + // output file - Note we assume that the PATH to the file already exists, having been through init and, optionally, service package creation + outputPath := "" + if data.ServicePackage != "" { + outputPath = fmt.Sprintf("%s/internal/services/%s/%s_resource.go", strings.ToLower(data.ProviderName), strings.ToLower(strcase.ToCamel(data.ServicePackage)), strcase.ToSnake(data.Name)) + } else { + outputPath = fmt.Sprintf("%s/internal/%s_resource.go", data.ProviderName, strcase.ToSnake(data.Name)) + } + + if _, err := os.Stat(outputPath); err == nil { + fmt.Printf("Error: A resource with this name already exists and will not be overwritten. Please remove this file if you wish to regenerate.") + return 1 + } + f, err := os.Create(outputPath) + if err != nil { + fmt.Printf("Error: failed opening output resource file for writing: %+v", err.Error()) + return 1 + } + + err = tpl.Execute(f, data) + if err != nil { + fmt.Println(err.Error()) + return 1 + } + if err := f.Close(); err != nil { + fmt.Printf("Error: Writing to file: %+v", err.Error()) + } + + return 0 +} + +func (r ResourceCommand) Help() string { + return fmt.Sprintf(` +Usage: tfpdk resource [options] + +Generates a scaffolded resource, optionally under a service package for this provider + +Options: + +-name=string (Required) the name of the new resource, can be in the form resource_name, ResourceName, or resource-name + +-servicepackage=string (Optional) place the resource under the named service package + +-no-update (Optional) Don't generate an update func. Use this for resources that cannot be updated in place. Note all schema properties must be 'ForceNew: true' + +-typed (Optional) Generate a resource for use with the Typed Resource SDK + +`) +} + +func (r ResourceCommand) Synopsis() string { + return "" +} diff --git a/commands/templatefs.go b/commands/templatefs.go new file mode 100644 index 0000000..df4b5a0 --- /dev/null +++ b/commands/templatefs.go @@ -0,0 +1,9 @@ +package commands + +import ( + "embed" +) + +//go:embed templates/* +var Templatedir embed.FS + diff --git a/commands/templates/resource.gotpl b/commands/templates/resource.gotpl new file mode 100644 index 0000000..2f7029b --- /dev/null +++ b/commands/templates/resource.gotpl @@ -0,0 +1,90 @@ +package {{if .Typed}} {{- ToLower .ServicePackage}} + +import ( + "context" + "time" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/sdk" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/pluginsdk" +) + +type {{ToCamel .Name}}Resource struct{} + +type {{ToCamel .Name}}Model struct { + // TODO - Schema in Go Types format here +} + +{{if .NoUpdate}}var _ sdk.Resource = {{else}}var _ sdk.ResourceWithUpdate = {{end}}{{ToCamel .Name}}Resource{} + +func (r {{ToCamel .Name}}Resource) ModelObject() interface{} { + return {{ToCamel .Name}}Model{} +} + +func (r {{ToCamel .Name}}Resource) ResourceType() string { + return "{{- TfName .ProviderName .Name -}}" +} + +func (r {{ToCamel .Name}}Resource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + panic("Implement me") // TODO - Add Validation func return here +} + +func (r {{ToCamel .Name}}Resource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + /* + TODO - This sections is for configurable items, `Required: true` items first, followed by`Optional: true`, + both in alphabetical order + */ + } +} + +func (r {{ToCamel .Name}}Resource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + /* + TODO - This section is for `Computed: true` only items, i.e. useful values that are returned by the resource + that can be used as outputs or passed programmatically to other resources or data sources. + */ + } +} + +func (r {{ToCamel .Name}}Resource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + // TODO - Create Func + return nil + }, + } +} + +func (r {{ToCamel .Name}}Resource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + // TODO - Read Func + return nil + }, + } +} + +func (r {{ToCamel .Name}}Resource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + // TODO - Delete Func + return nil + }, + } +} +{{if not .NoUpdate}} +func (r {{ToCamel .Name}}Resource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + // TODO - Delete Func + return nil + }, + } +} +{{end}} +{{else}} {{- ToLower .ProviderName}} {{end}} +{{/* TODO - Untyped resource */}} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4cfe692 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/jackofallops/tfpdk + +go 1.16 + +require ( + github.com/iancoleman/strcase v0.2.0 + github.com/mitchellh/cli v1.1.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..345183c --- /dev/null +++ b/go.sum @@ -0,0 +1,56 @@ +github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= +github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mitchellh/cli v1.1.2 h1:PvH+lL2B7IQ101xQL63Of8yFS2y+aDlsFcsqNc+u/Kw= +github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helpers/formatters.go b/helpers/formatters.go new file mode 100644 index 0000000..dbcd55c --- /dev/null +++ b/helpers/formatters.go @@ -0,0 +1,13 @@ +package helpers + +import ( + "fmt" + "strings" + + "github.com/iancoleman/strcase" +) + +func TerraformResourceName(provider, resourceName string) string { + fmtStr := "%s_%s" + return fmt.Sprintf(fmtStr, strings.ToLower(provider), strcase.ToSnake(resourceName)) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..03e4553 --- /dev/null +++ b/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "log" + "os" + + "github.com/jackofallops/tfpdk/commands" + "github.com/mitchellh/cli" +) + +var Commands map[string]cli.CommandFactory + +func main() { + args := os.Args[1:] + Commands = map[string]cli.CommandFactory{ + "init": func() (cli.Command, error) { + return &commands.InitialiseCommand{}, nil + }, + "resource": func() (cli.Command, error) { + return &commands.ResourceCommand{}, nil + }, + } + + tfpdk := cli.CLI{ + Args: args, + Commands: Commands, + Name: "tfpdk", + Version: "0.1.0", + } + + exitStatus, err := tfpdk.Run() + if err != nil { + log.Println(err) + } + + os.Exit(exitStatus) +}