Skip to content

Commit

Permalink
Merge pull request #33339 from jtdoepke/f-default_tags-env-vars
Browse files Browse the repository at this point in the history
Allow default_tags to be set by environment variables
  • Loading branch information
jar-b authored Aug 2, 2024
2 parents 3ef548f + e15edec commit f94b2eb
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 16 deletions.
3 changes: 3 additions & 0 deletions .changelog/33339.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
provider: Allow `default_tags` to be set by environment variables
```
3 changes: 2 additions & 1 deletion internal/provider/fwprovider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,8 @@ func (p *fwprovider) Schema(ctx context.Context, req provider.SchemaRequest, res
"tags": schema.MapAttribute{
ElementType: types.StringType,
Optional: true,
Description: "Resource tags to default across all resources",
Description: "Resource tags to default across all resources. " +
"Can also be configured with environment variables like `" + tftags.DefaultTagsEnvVarPrefix + "<tag_name>`.",
},
},
},
Expand Down
34 changes: 24 additions & 10 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,11 @@ func New(ctx context.Context) (*schema.Provider, error) {
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"tags": {
Type: schema.TypeMap,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Description: "Resource tags to default across all resources",
Type: schema.TypeMap,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
Description: "Resource tags to default across all resources. " +
"Can also be configured with environment variables like `" + tftags.DefaultTagsEnvVarPrefix + "<tag_name>`.",
},
},
},
Expand Down Expand Up @@ -534,6 +535,8 @@ func configure(ctx context.Context, provider *schema.Provider, d *schema.Resourc

if v, ok := d.GetOk("default_tags"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil {
config.DefaultTagsConfig = expandDefaultTags(ctx, v.([]interface{})[0].(map[string]interface{}))
} else {
config.DefaultTagsConfig = expandDefaultTags(ctx, nil)
}

v := d.Get("endpoints")
Expand Down Expand Up @@ -839,17 +842,28 @@ func expandAssumeRoleWithWebIdentity(_ context.Context, tfMap map[string]interfa
}

func expandDefaultTags(ctx context.Context, tfMap map[string]interface{}) *tftags.DefaultConfig {
if tfMap == nil {
return nil
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 == "" {
tags[tk] = v
}
}

defaultConfig := &tftags.DefaultConfig{}
if cfgTags, ok := tfMap["tags"].(map[string]interface{}); ok {
for k, v := range cfgTags {
tags[k] = v
}
}

if v, ok := tfMap["tags"].(map[string]interface{}); ok {
defaultConfig.Tags = tftags.New(ctx, v)
if len(tags) > 0 {
return &tftags.DefaultConfig{
Tags: tftags.New(ctx, tags),
}
}

return defaultConfig
return nil
}

func expandIgnoreTags(ctx context.Context, tfMap map[string]interface{}) *tftags.IgnoreConfig {
Expand Down
27 changes: 27 additions & 0 deletions internal/provider/provider_acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/hashicorp/terraform-provider-aws/internal/acctest"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
"github.com/hashicorp/terraform-provider-aws/internal/provider"
tftags "github.com/hashicorp/terraform-provider-aws/internal/tags"
"github.com/hashicorp/terraform-provider-aws/names"
)

Expand Down Expand Up @@ -110,6 +111,32 @@ func TestAccProvider_DefaultTagsTags_multiple(t *testing.T) {
})
}

func TestAccProvider_DefaultTagsTags_envVars(t *testing.T) {
ctx := acctest.Context(t)
var p *schema.Provider

t.Setenv(tftags.DefaultTagsEnvVarPrefix+"test1", "envValue1")
t.Setenv(tftags.DefaultTagsEnvVarPrefix+"test2", "envValue2")

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t),
ProtoV5ProviderFactories: testAccProtoV5ProviderFactoriesInternal(ctx, t, &p),
CheckDestroy: nil,
Steps: []resource.TestStep{
{ // nosemgrep:ci.test-config-funcs-correct-form
Config: acctest.ConfigDefaultTags_Tags1("test1", "value1"),
Check: resource.ComposeTestCheckFunc(
testAccCheckProviderDefaultTags_Tags(ctx, t, &p, map[string]string{
"test1": "value1",
"test2": "envValue2",
}),
),
},
},
})
}

func TestAccProvider_DefaultAndIgnoreTags_emptyBlocks(t *testing.T) {
ctx := acctest.Context(t)
var provider *schema.Provider
Expand Down
81 changes: 76 additions & 5 deletions internal/provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"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 @@ -217,6 +218,80 @@ func TestEndpointEnvVarPrecedence(t *testing.T) { //nolint:paralleltest
}
}

func TestExpandDefaultTags(t *testing.T) { //nolint:paralleltest
ctx := context.Background()
testcases := []struct {
tags map[string]interface{}
envvars map[string]string
expectedDefaultConfig *tftags.DefaultConfig
}{
{
tags: nil,
envvars: map[string]string{},
expectedDefaultConfig: nil,
},
{
tags: nil,
envvars: map[string]string{
tftags.DefaultTagsEnvVarPrefix + "Owner": "my-team",
},
expectedDefaultConfig: &tftags.DefaultConfig{
Tags: tftags.New(ctx, map[string]string{
"Owner": "my-team",
}),
},
},
{
tags: map[string]interface{}{
"Owner": "my-team",
},
envvars: map[string]string{},
expectedDefaultConfig: &tftags.DefaultConfig{
Tags: tftags.New(ctx, map[string]string{
"Owner": "my-team",
}),
},
},
{
tags: map[string]interface{}{
"Application": "foobar",
},
envvars: map[string]string{
tftags.DefaultTagsEnvVarPrefix + "Application": "my-app",
tftags.DefaultTagsEnvVarPrefix + "Owner": "my-team",
},
expectedDefaultConfig: &tftags.DefaultConfig{
Tags: tftags.New(ctx, map[string]string{
"Application": "foobar",
"Owner": "my-team",
}),
},
},
}

for _, testcase := range testcases {
oldEnv := stashEnv()
defer popEnv(oldEnv)
for k, v := range testcase.envvars {
os.Setenv(k, v)
}

results := expandDefaultTags(ctx, map[string]interface{}{
"tags": testcase.tags,
})

if results == nil {
if testcase.expectedDefaultConfig == nil {
return
} else {
t.Errorf("Expected default tags config to be %v, got nil", testcase.expectedDefaultConfig)
}
} else if !testcase.expectedDefaultConfig.TagsEqual(results.Tags) {
t.Errorf("Expected default tags config to be %v, got %v", testcase.expectedDefaultConfig, results)
}
}
}

func stashEnv() []string {
env := os.Environ()
os.Clearenv()
Expand All @@ -227,11 +302,7 @@ func popEnv(env []string) {
os.Clearenv()

for _, e := range env {
p := strings.SplitN(e, "=", 2)
k, v := p[0], ""
if len(p) > 1 {
v = p[1]
}
k, v, _ := strings.Cut(e, "=")
os.Setenv(k, v)
}
}
3 changes: 3 additions & 0 deletions internal/tags/key_value_tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ const (
ElasticbeanstalkTagKeyPrefix = `elasticbeanstalk:`
NameTagKey = `Name`
ServerlessApplicationRepositoryTagKeyPrefix = `serverlessrepo:`

// Environment variables prefixed with this string will be treated as default_tags.
DefaultTagsEnvVarPrefix = "TF_AWS_DEFAULT_TAGS_"
)

// DefaultConfig contains tags to default across all resources.
Expand Down
40 changes: 40 additions & 0 deletions website/docs/index.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -651,9 +651,49 @@ vpc_resource_level_tags = tomap({
})
```

Example: Default tags from environment variables

```terraform
provider "aws" {
default_tags {
tags = {
Name = "Provider Tag"
}
}
}
resource "aws_vpc" "example" {
# ..other configuration...
}
output "vpc_resource_level_tags" {
value = aws_vpc.example.tags
}
output "vpc_all_tags" {
value = aws_vpc.example.tags_all
}
```

Outputs:

```console
$ export TF_AWS_DEFAULT_TAGS_Environment=Test
$ terraform apply
...
Outputs:

vpc_all_tags = tomap({
"Environment" = "Test"
"Name" = "Provider Tag"
})
```

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_<tag_key>=<tag_value>`.
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

Expand Down

0 comments on commit f94b2eb

Please sign in to comment.