-
Notifications
You must be signed in to change notification settings - Fork 9.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
internal/tagresource: Initial generator for individual tag resources,…
… 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
Showing
10 changed files
with
595 additions
and
89 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
15
aws/internal/tagresource/service_generation_customizations.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.