diff --git a/.changelog/35264.txt b/.changelog/35264.txt new file mode 100644 index 000000000000..6265dd36b809 --- /dev/null +++ b/.changelog/35264.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +provider: Allow the `ignore_tags.keys` and `ignore_tags.key_prefixes` to be set by environment variables +``` diff --git a/internal/provider/fwprovider/provider.go b/internal/provider/fwprovider/provider.go index 2b3e5a70d004..5435b5031738 100644 --- a/internal/provider/fwprovider/provider.go +++ b/internal/provider/fwprovider/provider.go @@ -280,12 +280,14 @@ func (p *fwprovider) Schema(ctx context.Context, req provider.SchemaRequest, res "key_prefixes": schema.SetAttribute{ ElementType: types.StringType, Optional: true, - Description: "Resource tag key prefixes to ignore across all resources.", + Description: "Resource tag key prefixes to ignore across all resources. " + + "Can also be configured with the " + tftags.IgnoreTagsKeyPrefixesEnvVar + " environment variable.", }, "keys": schema.SetAttribute{ ElementType: types.StringType, Optional: true, - Description: "Resource tag keys to ignore across all resources.", + Description: "Resource tag keys to ignore across all resources. " + + "Can also be configured with the " + tftags.IgnoreTagsKeysEnvVar + " environment variable.", }, }, }, diff --git a/internal/provider/provider.go b/internal/provider/provider.go index bd1e0df26cdb..cc2fcc4ed23c 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -114,16 +114,18 @@ func New(ctx context.Context) (*schema.Provider, error) { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "keys": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Description: "Resource tag keys to ignore across all resources.", + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: "Resource tag keys to ignore across all resources. " + + "Can also be configured with the " + tftags.IgnoreTagsKeysEnvVar + " environment variable.", }, "key_prefixes": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Description: "Resource tag key prefixes to ignore across all resources.", + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: "Resource tag key prefixes to ignore across all resources. " + + "Can also be configured with the " + tftags.IgnoreTagsKeyPrefixesEnvVar + " environment variable.", }, }, }, @@ -568,6 +570,8 @@ func configure(ctx context.Context, provider *schema.Provider, d *schema.Resourc if v, ok := d.GetOk("ignore_tags"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { config.IgnoreTagsConfig = expandIgnoreTags(ctx, v.([]interface{})[0].(map[string]interface{})) + } else { + config.IgnoreTagsConfig = expandIgnoreTags(ctx, nil) } if v, ok := d.GetOk("max_retries"); ok { @@ -845,8 +849,7 @@ func expandDefaultTags(ctx context.Context, tfMap map[string]interface{}) *tftag tags := make(map[string]interface{}) for _, ev := range os.Environ() { k, v, _ := strings.Cut(ev, "=") - before, tk, ok := strings.Cut(k, tftags.DefaultTagsEnvVarPrefix) - if ok && before == "" { + if before, tk, ok := strings.Cut(k, tftags.DefaultTagsEnvVarPrefix); ok && before == "" { tags[tk] = v } } @@ -867,18 +870,48 @@ func expandDefaultTags(ctx context.Context, tfMap map[string]interface{}) *tftag } func expandIgnoreTags(ctx context.Context, tfMap map[string]interface{}) *tftags.IgnoreConfig { - if tfMap == nil { - return nil + var keys, keyPrefixes []interface{} + + if tfMap != nil { + if v, ok := tfMap["keys"].(*schema.Set); ok { + keys = v.List() + } + if v, ok := tfMap["key_prefixes"].(*schema.Set); ok { + keyPrefixes = v.List() + } } - ignoreConfig := &tftags.IgnoreConfig{} + if v := os.Getenv(tftags.IgnoreTagsKeysEnvVar); v != "" { + for _, k := range strings.Split(v, ",") { + if trimmed := strings.TrimSpace(k); trimmed != "" { + keys = append(keys, trimmed) + } + } + } + + if v := os.Getenv(tftags.IgnoreTagsKeyPrefixesEnvVar); v != "" { + for _, kp := range strings.Split(v, ",") { + if trimmed := strings.TrimSpace(kp); trimmed != "" { + keyPrefixes = append(keyPrefixes, trimmed) + } + } + } - if v, ok := tfMap["keys"].(*schema.Set); ok { - ignoreConfig.Keys = tftags.New(ctx, v.List()) + // To preseve behavior prior to supporting environment variables: + // + // - Return nil when no keys or prefixes are set + // - For a non-nil return, `keys` or `key_prefixes` should be + // nil if empty (versus a zero-value `KeyValueTags` struct) + if len(keys) == 0 && len(keyPrefixes) == 0 { + return nil } - if v, ok := tfMap["key_prefixes"].(*schema.Set); ok { - ignoreConfig.KeyPrefixes = tftags.New(ctx, v.List()) + ignoreConfig := &tftags.IgnoreConfig{} + if len(keys) > 0 { + ignoreConfig.Keys = tftags.New(ctx, keys) + } + if len(keyPrefixes) > 0 { + ignoreConfig.KeyPrefixes = tftags.New(ctx, keyPrefixes) } return ignoreConfig diff --git a/internal/provider/provider_acc_test.go b/internal/provider/provider_acc_test.go index 41f67015a5a9..9caca6701a2a 100644 --- a/internal/provider/provider_acc_test.go +++ b/internal/provider/provider_acc_test.go @@ -419,6 +419,94 @@ func TestAccProvider_IgnoreTagsKeys_multiple(t *testing.T) { }) } +func TestAccProvider_IgnoreTagsKeys_envVarOnly(t *testing.T) { + ctx := acctest.Context(t) + var provider *schema.Provider + + t.Setenv(tftags.IgnoreTagsKeysEnvVar, "test3,test4") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t), + ProtoV5ProviderFactories: testAccProtoV5ProviderFactoriesInternal(ctx, t, &provider), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccProviderConfig_ignoreTagsKeys0(), + Check: resource.ComposeTestCheckFunc( + testAccCheckIgnoreTagsKeys(ctx, t, &provider, []string{"test3", "test4"}), + ), + }, + }, + }) +} + +func TestAccProvider_IgnoreTagsKeys_envVarMerged(t *testing.T) { + ctx := acctest.Context(t) + var provider *schema.Provider + + t.Setenv(tftags.IgnoreTagsKeysEnvVar, "test3,test4") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t), + ProtoV5ProviderFactories: testAccProtoV5ProviderFactoriesInternal(ctx, t, &provider), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccProviderConfig_ignoreTagsKeys2("test1", "test2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckIgnoreTagsKeys(ctx, t, &provider, []string{"test1", "test2", "test3", "test4"}), + ), + }, + }, + }) +} + +func TestAccProvider_IgnoreTagsKeyPrefixes_envVarOnly(t *testing.T) { + ctx := acctest.Context(t) + var provider *schema.Provider + + t.Setenv(tftags.IgnoreTagsKeyPrefixesEnvVar, "test3,test4") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t), + ProtoV5ProviderFactories: testAccProtoV5ProviderFactoriesInternal(ctx, t, &provider), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccProviderConfig_ignoreTagsKeyPrefixes0(), + Check: resource.ComposeTestCheckFunc( + testAccCheckIgnoreTagsKeyPrefixes(ctx, t, &provider, []string{"test3", "test4"}), + ), + }, + }, + }) +} + +func TestAccProvider_IgnoreTagsKeyPrefixes_envVarMerged(t *testing.T) { + ctx := acctest.Context(t) + var provider *schema.Provider + + t.Setenv(tftags.IgnoreTagsKeyPrefixesEnvVar, "test3,test4") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t), + ProtoV5ProviderFactories: testAccProtoV5ProviderFactoriesInternal(ctx, t, &provider), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccProviderConfig_ignoreTagsKeyPrefixes2("test1", "test2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckIgnoreTagsKeyPrefixes(ctx, t, &provider, []string{"test1", "test2", "test3", "test4"}), + ), + }, + }, + }) +} + func TestAccProvider_Region_c2s(t *testing.T) { ctx := acctest.Context(t) var provider *schema.Provider diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index b03d15033466..8548da001147 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -12,6 +12,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/names" @@ -292,6 +293,119 @@ func TestExpandDefaultTags(t *testing.T) { //nolint:paralleltest } } +func TestExpandIgnoreTags(t *testing.T) { //nolint:paralleltest + ctx := context.Background() + testcases := []struct { + keys []interface{} + keyPrefixes []interface{} + envvars map[string]string + expectedIgnoreConfig *tftags.IgnoreConfig + }{ + { + keys: nil, + keyPrefixes: nil, + envvars: map[string]string{}, + expectedIgnoreConfig: nil, + }, + { + envvars: map[string]string{ + tftags.IgnoreTagsKeysEnvVar: "env1", + tftags.IgnoreTagsKeyPrefixesEnvVar: "env2", + }, + expectedIgnoreConfig: &tftags.IgnoreConfig{ + Keys: tftags.New(ctx, []interface{}{"env1"}), + KeyPrefixes: tftags.New(ctx, []interface{}{"env2"}), + }, + }, + { + envvars: map[string]string{ + tftags.IgnoreTagsKeysEnvVar: "env1,env2", + tftags.IgnoreTagsKeyPrefixesEnvVar: "env3,env4", + }, + expectedIgnoreConfig: &tftags.IgnoreConfig{ + Keys: tftags.New(ctx, []interface{}{"env1", "env2"}), + KeyPrefixes: tftags.New(ctx, []interface{}{"env3", "env4"}), + }, + }, + { + envvars: map[string]string{ + tftags.IgnoreTagsKeysEnvVar: "env1,env1", + }, + expectedIgnoreConfig: &tftags.IgnoreConfig{ + Keys: tftags.New(ctx, []interface{}{"env1"}), + }, + }, + { + envvars: map[string]string{ + tftags.IgnoreTagsKeyPrefixesEnvVar: "env1,env1", + }, + expectedIgnoreConfig: &tftags.IgnoreConfig{ + KeyPrefixes: tftags.New(ctx, []interface{}{"env1"}), + }, + }, + { + keys: []interface{}{"config1", "config2"}, + keyPrefixes: []interface{}{"config3", "config4"}, + envvars: map[string]string{}, + expectedIgnoreConfig: &tftags.IgnoreConfig{ + Keys: tftags.New(ctx, []interface{}{"config1", "config2"}), + KeyPrefixes: tftags.New(ctx, []interface{}{"config3", "config4"}), + }, + }, + { + keys: []interface{}{"config1", "config2"}, + keyPrefixes: []interface{}{"config3", "config4"}, + envvars: map[string]string{ + tftags.IgnoreTagsKeysEnvVar: "env1,env2", + tftags.IgnoreTagsKeyPrefixesEnvVar: "env3,env4", + }, + expectedIgnoreConfig: &tftags.IgnoreConfig{ + Keys: tftags.New(ctx, []interface{}{"env1", "env2", "config1", "config2"}), + KeyPrefixes: tftags.New(ctx, []interface{}{"env3", "env4", "config3", "config4"}), + }, + }, + { + keys: []interface{}{"example1", "example2"}, + envvars: map[string]string{ + tftags.IgnoreTagsKeysEnvVar: "example1,example3", + }, + expectedIgnoreConfig: &tftags.IgnoreConfig{ + Keys: tftags.New(ctx, []interface{}{"example1", "example2", "example3"}), + }, + }, + { + keyPrefixes: []interface{}{"example1", "example2"}, + envvars: map[string]string{ + tftags.IgnoreTagsKeyPrefixesEnvVar: "example1,example3", + }, + expectedIgnoreConfig: &tftags.IgnoreConfig{ + KeyPrefixes: tftags.New(ctx, []interface{}{"example1", "example2", "example3"}), + }, + }, + } + + for _, testcase := range testcases { + oldEnv := stashEnv() + defer popEnv(oldEnv) + for k, v := range testcase.envvars { + os.Setenv(k, v) + } + + results := expandIgnoreTags(ctx, map[string]interface{}{ + "keys": schema.NewSet(schema.HashString, testcase.keys), + "key_prefixes": schema.NewSet(schema.HashString, testcase.keyPrefixes), + }) + + if results == nil && testcase.expectedIgnoreConfig != nil { + t.Errorf("Expected ignore tags config to be %v, got nil", testcase.expectedIgnoreConfig) + } + + if diff := cmp.Diff(testcase.expectedIgnoreConfig, results); diff != "" { + t.Errorf("Unexpected ignore_tags diff: %s", diff) + } + } +} + func stashEnv() []string { env := os.Environ() os.Clearenv() diff --git a/internal/tags/key_value_tags.go b/internal/tags/key_value_tags.go index 5d7b92e2fe4a..bf4e92dd50db 100644 --- a/internal/tags/key_value_tags.go +++ b/internal/tags/key_value_tags.go @@ -29,8 +29,25 @@ const ( NameTagKey = `Name` ServerlessApplicationRepositoryTagKeyPrefix = `serverlessrepo:` - // Environment variables prefixed with this string will be treated as default_tags. + // Environment variables with this prefix will be treated as a `default_tags` key value pair + // + // The environment variable name after this suffix will be treated as the tag key. The + // value of the variable will be treated as the tag value. Empty values are permitted. DefaultTagsEnvVarPrefix = "TF_AWS_DEFAULT_TAGS_" + + // Environment variable specifying a list of tag keys to be ignored + // + // Values read from this environment variable are merged with those specified in the + // provider configuration. When multiple keys are provided, the values are + // comma-separated. + IgnoreTagsKeysEnvVar = "TF_AWS_IGNORE_TAGS_KEYS" + + // Environment variable specifying a list of tag key prefixes to be ignored + // + // Values read from this environment variable are merged with those specified in the + // provider configuration. When multiple key prefixes are provided, the values are + // comma-separated. + IgnoreTagsKeyPrefixesEnvVar = "TF_AWS_IGNORE_TAGS_KEY_PREFIXES" ) // DefaultConfig contains tags to default across all resources. diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index 11ad85f59668..8f9301507676 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -692,7 +692,7 @@ vpc_all_tags = tomap({ The `default_tags` configuration block supports the following argument: * `tags` - (Optional) Key-value map of tags to apply to all resources. -Default tags will also be read from environment variables matching the pattern `TF_AWS_DEFAULT_TAGS_=`. +Default tags can also be provided via environment variables matching the pattern `TF_AWS_DEFAULT_TAGS_=`. If a tag is present in both an environment variable and this argument, the value in the provider configuration takes precedence. ### ignore_tags Configuration Block @@ -709,8 +709,18 @@ provider "aws" { The `ignore_tags` configuration block supports the following arguments: -* `keys` - (Optional) List of exact resource tag keys to ignore across all resources handled by this provider. This configuration prevents Terraform from returning the tag in any `tags` attributes and displaying any configuration difference for the tag value. If any resource configuration still has this tag key configured in the `tags` argument, it will display a perpetual difference until the tag is removed from the argument or [`ignore_changes`](https://www.terraform.io/docs/configuration/meta-arguments/lifecycle.html#ignore_changes) is also used. -* `key_prefixes` - (Optional) List of resource tag key prefixes to ignore across all resources handled by this provider. This configuration prevents Terraform from returning any tag key matching the prefixes in any `tags` attributes and displaying any configuration difference for those tag values. If any resource configuration still has a tag matching one of the prefixes configured in the `tags` argument, it will display a perpetual difference until the tag is removed from the argument or [`ignore_changes`](https://www.terraform.io/docs/configuration/meta-arguments/lifecycle.html#ignore_changes) is also used. +* `keys` - (Optional) List of exact resource tag keys to ignore across all resources handled by this provider. +Ignored tag keys can also be provided via the `TF_AWS_IGNORE_TAGS_KEYS` environment variable. +When supplying multiple keys, the values should be comma delimited. +If both this argument and the corresponding environment variable are set, values from both sources are merged into a single list. +This configuration prevents Terraform from returning the tag in any `tags` attributes and displaying any configuration difference for the tag value. +If any resource configuration still has this tag key configured in the `tags` argument, it will display a perpetual difference until the tag is removed from the argument or [`ignore_changes`](https://www.terraform.io/docs/configuration/meta-arguments/lifecycle.html#ignore_changes) is also used. +* `key_prefixes` - (Optional) List of resource tag key prefixes to ignore across all resources handled by this provider. +Ignored tag key prefixes can also be provided via the `TF_AWS_IGNORE_TAGS_KEY_PREFIXES` environment variable. +When supplying multiple key prefixes, the values should be comma delimited. +If both this argument and the corresponding environment variable are set, values from both sources are merged into a single list. +This configuration prevents Terraform from returning any tag key matching the prefixes in any `tags` attributes and displaying any configuration difference for those tag values. +If any resource configuration still has a tag matching one of the prefixes configured in the `tags` argument, it will display a perpetual difference until the tag is removed from the argument or [`ignore_changes`](https://www.terraform.io/docs/configuration/meta-arguments/lifecycle.html#ignore_changes) is also used. ## Getting the Account ID