Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement configuring ignore_tags via environment variables #35264

Merged
merged 8 commits into from
Aug 6, 2024
3 changes: 3 additions & 0 deletions .changelog/35264.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
provider: Allow the `ignore_tags.keys` and `ignore_tags.key_prefixes` to be set by environment variables
```
6 changes: 4 additions & 2 deletions internal/provider/fwprovider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
},
},
},
Expand Down
67 changes: 50 additions & 17 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
},
},
},
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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
Expand Down
88 changes: 88 additions & 0 deletions internal/provider/provider_acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
114 changes: 114 additions & 0 deletions internal/provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
Expand Down
19 changes: 18 additions & 1 deletion internal/tags/key_value_tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading