Skip to content

Commit

Permalink
internal/tagresource: Initial generator for individual tag resources,…
Browse files Browse the repository at this point in the history
… switch aws_ec2_tag implementation

Reference: #9061

Output from acceptance testing:

```
--- PASS: TestAccAWSEc2Tag_disappears (429.66s)
--- PASS: TestAccAWSEc2Tag_Value (530.85s)
--- PASS: TestAccAWSEc2Tag_basic (537.38s)
```
  • Loading branch information
bflad committed Jun 17, 2020
1 parent 3bbb4ed commit 89761c5
Show file tree
Hide file tree
Showing 10 changed files with 595 additions and 89 deletions.
1 change: 1 addition & 0 deletions GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ build: fmtcheck
go install

gen:
rm -f aws/*_gen.go aws/*_gen_test.go
rm -f aws/internal/keyvaluetags/*_gen.go
go generate ./...

Expand Down
13 changes: 13 additions & 0 deletions aws/internal/tagresource/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# tagresource

The `tagresource` package is designed to provide a generator and consistent interface for Terraform AWS Provider resources that handle individual resource tags. Most of the heavy lifting is done by the `keyvaluetags` package to smooth over inconsistencies across AWS service APIs, but this generator does implement some final user experience improvements.

## Code Structure

```text
aws/internal/tagresource
├── generator/main.go (generates tag resource)
├── tag_resources_test.go (unit tests for shared tag resource logic)
├── tag_resources.go (shared tag resource logic)
└── service_generation_customizations.go (AWS Go SDK service customizations for generator)
```
288 changes: 288 additions & 0 deletions aws/internal/tagresource/generator/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
// +build ignore

package main

import (
"bytes"
"flag"
"fmt"
"go/format"
"log"
"os"
"strings"
"text/template"

"github.com/terraform-providers/terraform-provider-aws/aws/internal/tagresource"
)

var (
serviceName = flag.String("servicename", "", "lowercase service name")
)

type TemplateData struct {
ServiceName string
}

func main() {
flag.Parse()

if len(*serviceName) == 0 {
flag.Usage()
os.Exit(2)
}

resourceName := fmt.Sprintf("aws_%s_tag", *serviceName)
resourceFilename := fmt.Sprintf("resource_%s_gen.go", resourceName)
resourceTestFilename := fmt.Sprintf("resource_%s_gen_test.go", resourceName)

templateData := TemplateData{
ServiceName: *serviceName,
}
templateFuncMap := template.FuncMap{
"IdentifierAttributeName": tagresource.ServiceIdentifierAttributeName,
"Title": strings.Title,
}

if err := generateTemplateFile(resourceFilename, resourceTemplateBody, templateFuncMap, templateData); err != nil {
log.Fatal(err)
}

if err := generateTemplateFile(resourceTestFilename, resourceTestTemplateBody, templateFuncMap, templateData); err != nil {
log.Fatal(err)
}
}

func generateTemplateFile(filename string, templateBody string, templateFuncs template.FuncMap, templateData interface{}) error {
tmpl, err := template.New(filename).Funcs(templateFuncs).Parse(templateBody)

if err != nil {
return fmt.Errorf("error parsing template: %w", err)
}

var buffer bytes.Buffer
err = tmpl.Execute(&buffer, templateData)

if err != nil {
return fmt.Errorf("error executing template: %w", err)
}

generatedFileContents, err := format.Source(buffer.Bytes())

if err != nil {
return fmt.Errorf("error formatting generated file: %w", err)
}

f, err := os.Create(filename)

if err != nil {
return fmt.Errorf("error creating file (%s): %w", filename, err)
}

defer f.Close()

_, err = f.Write(generatedFileContents)

if err != nil {
return fmt.Errorf("error writing to file (%s): %w", filename, err)
}

return nil
}

const (
resourceTemplateBody = `
// Code generated by internal/tagresource/generator/main.go; DO NOT EDIT.
package aws
import (
"fmt"
"log"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tagresource"
)
func resourceAws{{ .ServiceName | Title }}Tag() *schema.Resource {
return &schema.Resource{
Create: resourceAws{{ .ServiceName | Title }}TagCreate,
Read: resourceAws{{ .ServiceName | Title }}TagRead,
Update: resourceAws{{ .ServiceName | Title }}TagUpdate,
Delete: resourceAws{{ .ServiceName | Title }}TagDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"{{ .ServiceName | IdentifierAttributeName }}": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"key": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"value": {
Type: schema.TypeString,
Required: true,
},
},
}
}
func resourceAws{{ .ServiceName | Title }}TagCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).{{ .ServiceName }}conn
identifier := d.Get("{{ .ServiceName | IdentifierAttributeName }}").(string)
key := d.Get("key").(string)
value := d.Get("value").(string)
{{ if eq .ServiceName "ec2" }}
if err := keyvaluetags.{{ .ServiceName | Title }}CreateTags(conn, identifier, map[string]string{key: value}); err != nil {
{{- else }}
if err := keyvaluetags.{{ .ServiceName | Title }}UpdateTags(conn, identifier, nil, map[string]string{key: value}); err != nil {
{{- end }}
fmt.Errorf("error creating {{ .ServiceName }} resource (%s) tag (%s): %w", identifier, key, err)
}
d.SetId(tagresource.SetResourceId(identifier, key))
return resourceAws{{ .ServiceName | Title }}TagRead(d, meta)
}
func resourceAws{{ .ServiceName | Title }}TagRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).{{ .ServiceName }}conn
identifier, key, err := tagresource.GetResourceId(d.Id())
if err != nil {
return err
}
exists, value, err := keyvaluetags.{{ .ServiceName | Title }}GetTag(conn, identifier, key)
if err != nil {
fmt.Errorf("error reading {{ .ServiceName }} resource (%s) tag (%s): %w", identifier, key, err)
}
if !exists {
log.Printf("[WARN] {{ .ServiceName }} resource (%s) tag (%s) not found, removing from state", identifier, key)
d.SetId("")
return nil
}
d.Set("{{ .ServiceName | IdentifierAttributeName }}", identifier)
d.Set("key", key)
d.Set("value", value)
return nil
}
func resourceAws{{ .ServiceName | Title }}TagUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).{{ .ServiceName }}conn
identifier, key, err := tagresource.GetResourceId(d.Id())
if err != nil {
return err
}
if err := keyvaluetags.{{ .ServiceName | Title }}UpdateTags(conn, identifier, nil, map[string]string{key: d.Get("value").(string)}); err != nil {
return fmt.Errorf("error updating {{ .ServiceName }} resource (%s) tag (%s): %w", identifier, key, err)
}
return resourceAws{{ .ServiceName | Title }}TagRead(d, meta)
}
func resourceAws{{ .ServiceName | Title }}TagDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).{{ .ServiceName }}conn
identifier, key, err := tagresource.GetResourceId(d.Id())
if err != nil {
return err
}
if err := keyvaluetags.{{ .ServiceName | Title }}UpdateTags(conn, identifier, map[string]string{key: d.Get("value").(string)}, nil); err != nil {
return fmt.Errorf("error deleting {{ .ServiceName }} resource (%s) tag (%s): %w", identifier, key, err)
}
return nil
}
`
resourceTestTemplateBody = `
// Code generated by internal/tagresource/generator/main.go; DO NOT EDIT.
package aws
import (
"fmt"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tagresource"
)
func testAccCheck{{ .ServiceName | Title }}TagDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).{{ .ServiceName }}conn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_{{ .ServiceName }}_tag" {
continue
}
identifier, key, err := tagresource.GetResourceId(rs.Primary.ID)
if err != nil {
return err
}
exists, _, err := keyvaluetags.{{ .ServiceName | Title }}GetTag(conn, identifier, key)
if err != nil {
return err
}
if exists {
return fmt.Errorf("resource (%s) tag (%s) still exists", identifier, key)
}
}
return nil
}
func testAccCheck{{ .ServiceName | Title }}TagExists(resourceName string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[resourceName]
if !ok {
return fmt.Errorf("not found: %s", resourceName)
}
if rs.Primary.ID == "" {
return fmt.Errorf("%s: missing resource ID", resourceName)
}
identifier, key, err := tagresource.GetResourceId(rs.Primary.ID)
if err != nil {
return err
}
conn := testAccProvider.Meta().(*AWSClient).{{ .ServiceName }}conn
exists, _, err := keyvaluetags.{{ .ServiceName | Title }}GetTag(conn, identifier, key)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("resource (%s) tag (%s) not found", identifier, key)
}
return nil
}
}
`
)
15 changes: 15 additions & 0 deletions aws/internal/tagresource/service_generation_customizations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package tagresource

import (
"github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags"
)

// ServiceIdentifierAttributeName determines the schema identifier attribute name.
func ServiceIdentifierAttributeName(serviceName string) string {
switch serviceName {
case "ec2":
return "resource_id"
default:
return toSnakeCase(keyvaluetags.ServiceTagInputIdentifierField(serviceName))
}
}
38 changes: 38 additions & 0 deletions aws/internal/tagresource/tag_resources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package tagresource

import (
"fmt"
"regexp"
"strings"
)

const (
// Separator used in resource identifiers
ResourceIdSeparator = `,`
)

// GetResourceId parses a given resource identifier for tag identifier and tag key.
func GetResourceId(resourceId string) (string, string, error) {
parts := strings.SplitN(resourceId, ",", 2)

if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return "", "", fmt.Errorf("invalid resource identifier (%s), expected ID,KEY", resourceId)
}

return parts[0], parts[1], nil
}

// SetResourceId creates a resource identifier given a tag identifier and a tag key.
func SetResourceId(identifier string, key string) string {
return identifier + ResourceIdSeparator + key
}

// toSnakeCase converts a string to snake case.
//
// For example, AWS Go SDK field names are in PascalCase,
// while Terraform schema attribute names are in snake_case.
func toSnakeCase(str string) string {
result := regexp.MustCompile("(.)([A-Z][a-z]+)").ReplaceAllString(str, "${1}_${2}")
result = regexp.MustCompile("([a-z0-9])([A-Z])").ReplaceAllString(result, "${1}_${2}")
return strings.ToLower(result)
}
Loading

0 comments on commit 89761c5

Please sign in to comment.