diff --git a/.changelog/17974.txt b/.changelog/17974.txt new file mode 100644 index 00000000000..6370c123b4e --- /dev/null +++ b/.changelog/17974.txt @@ -0,0 +1,15 @@ +```release-note:note +provider: New `default_tags` argument as a public preview for applying tags across all resources under a provider. Support for the functionality must be added to individual resources in the codebase and is only implemented for the `aws_subnet` and `aws_vpc` resources at this time. Until a general availability announcement, no compatibility promises are made with these provider arguments and their functionality. +``` + +```release-note:enhancement +provider: Add `default_tags` argument (in public preview, see note above) + + +```release-note:enhancement +resource/aws_subnet: Support provider-wide default tags (in public preview, see note above) +``` + +```release-note:enhancement +resource/aws_vpc: Support provider-wide default tags (in public preview, see note above) +``` diff --git a/aws/config.go b/aws/config.go index 0493b8d71a0..fae75f76583 100644 --- a/aws/config.go +++ b/aws/config.go @@ -191,9 +191,10 @@ type Config struct { AllowedAccountIds []string ForbiddenAccountIds []string - Endpoints map[string]string - IgnoreTagsConfig *keyvaluetags.IgnoreConfig - Insecure bool + DefaultTagsConfig *keyvaluetags.DefaultConfig + Endpoints map[string]string + IgnoreTagsConfig *keyvaluetags.IgnoreConfig + Insecure bool SkipCredsValidation bool SkipGetEC2Platforms bool @@ -249,6 +250,7 @@ type AWSClient struct { datapipelineconn *datapipeline.DataPipeline datasyncconn *datasync.DataSync daxconn *dax.DAX + DefaultTagsConfig *keyvaluetags.DefaultConfig devicefarmconn *devicefarm.DeviceFarm dlmconn *dlm.DLM dmsconn *databasemigrationservice.DatabaseMigrationService @@ -492,6 +494,7 @@ func (c *Config) Client() (interface{}, error) { datapipelineconn: datapipeline.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["datapipeline"])})), datasyncconn: datasync.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["datasync"])})), daxconn: dax.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["dax"])})), + DefaultTagsConfig: c.DefaultTagsConfig, devicefarmconn: devicefarm.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["devicefarm"])})), dlmconn: dlm.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["dlm"])})), dmsconn: databasemigrationservice.New(sess.Copy(&aws.Config{Endpoint: aws.String(c.Endpoints["dms"])})), diff --git a/aws/internal/keyvaluetags/key_value_tags.go b/aws/internal/keyvaluetags/key_value_tags.go index f9af201a7c0..1f48f3c1cd6 100644 --- a/aws/internal/keyvaluetags/key_value_tags.go +++ b/aws/internal/keyvaluetags/key_value_tags.go @@ -25,6 +25,11 @@ const ( ServerlessApplicationRepositoryTagKeyPrefix = `serverlessrepo:` ) +// DefaultConfig contains tags to default across all resources. +type DefaultConfig struct { + Tags KeyValueTags +} + // IgnoreConfig contains various options for removing resource tags. type IgnoreConfig struct { Keys KeyValueTags @@ -50,6 +55,36 @@ func (tags KeyValueTags) IgnoreAws() KeyValueTags { return result } +// MergeTags returns the result of keyvaluetags.Merge() on the given +// DefaultConfig.Tags with KeyValueTags provided as an argument, +// overriding the value of any tag with a matching key. +func (dc *DefaultConfig) MergeTags(tags KeyValueTags) KeyValueTags { + if dc == nil || dc.Tags == nil { + return tags + } + + return dc.Tags.Merge(tags) +} + +// TagsEqual returns true if the given configuration's Tags +// are equal to those passed in as an argument; +// otherwise returns false +func (dc *DefaultConfig) TagsEqual(tags KeyValueTags) bool { + if dc == nil || dc.Tags == nil { + return tags == nil + } + + if tags == nil { + return false + } + + if len(tags) == 0 { + return len(dc.Tags) == 0 + } + + return dc.Tags.ContainsAll(tags) +} + // IgnoreConfig returns any tags not removed by a given configuration. func (tags KeyValueTags) IgnoreConfig(config *IgnoreConfig) KeyValueTags { if config == nil { @@ -401,6 +436,27 @@ func (tags KeyValueTags) Hash() int { return hash } +// RemoveDefaultConfig returns tags not present in a DefaultConfig object +// in addition to tags with key/value pairs that override those in a DefaultConfig; +// however, if all tags present in the DefaultConfig object are equivalent to those +// in the given KeyValueTags, then the KeyValueTags are returned, effectively +// bypassing the need to remove differing tags. +func (tags KeyValueTags) RemoveDefaultConfig(dc *DefaultConfig) KeyValueTags { + if dc == nil || dc.Tags == nil { + return tags + } + + result := make(KeyValueTags) + + for k, v := range tags { + if defaultVal, ok := dc.Tags[k]; !ok || !v.Equal(defaultVal) { + result[k] = v + } + } + + return result +} + // String returns the default string representation of the KeyValueTags. func (tags KeyValueTags) String() string { var builder strings.Builder diff --git a/aws/internal/keyvaluetags/key_value_tags_test.go b/aws/internal/keyvaluetags/key_value_tags_test.go index 08a9376a2bc..39717e69757 100644 --- a/aws/internal/keyvaluetags/key_value_tags_test.go +++ b/aws/internal/keyvaluetags/key_value_tags_test.go @@ -4,6 +4,254 @@ import ( "testing" ) +func TestKeyValueTagsDefaultConfigMergeTags(t *testing.T) { + testCases := []struct { + name string + tags KeyValueTags + defaultConfig *DefaultConfig + want map[string]string + }{ + { + name: "empty config", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{}, + want: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "no config", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: nil, + want: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "no tags", + tags: New(map[string]string{}), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + }, + want: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "keys all matching", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + }, + want: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "keys some matching", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + }), + }, + want: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "keys some overridden", + tags: New(map[string]string{ + "key1": "value2", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + }), + }, + want: map[string]string{ + "key1": "value2", + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "keys none matching", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key4": "value4", + "key5": "value5", + "key6": "value6", + }), + }, + want: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + "key5": "value5", + "key6": "value6", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := testCase.defaultConfig.MergeTags(testCase.tags) + testKeyValueTagsVerifyMap(t, got.Map(), testCase.want) + }) + } +} + +func TestKeyValueTagsDefaultConfigTagsEqual(t *testing.T) { + testCases := []struct { + name string + tags KeyValueTags + defaultConfig *DefaultConfig + want bool + }{ + { + name: "empty config", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{}, + want: false, + }, + { + name: "no config", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: nil, + want: false, + }, + { + name: "empty tags", + tags: New(map[string]string{}), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + }, + want: false, + }, + { + name: "no tags", + tags: nil, + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + }, + want: false, + }, + { + name: "empty config and no tags", + tags: nil, + defaultConfig: &DefaultConfig{}, + want: true, + }, + { + name: "no config and tags", + tags: nil, + defaultConfig: nil, + want: true, + }, + { + name: "keys and values all matching", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + }, + want: true, + }, + { + name: "only keys matching", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value0", + "key2": "value1", + "key3": "value2", + }), + }, + want: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := testCase.defaultConfig.TagsEqual(testCase.tags) + + if got != testCase.want { + t.Errorf("got %t; want %t", got, testCase.want) + } + }) + } +} + func TestKeyValueTagsIgnoreAws(t *testing.T) { testCases := []struct { name string @@ -1578,6 +1826,135 @@ func TestKeyValueTagsHash(t *testing.T) { } } +func TestKeyValueTagsRemoveDefaultConfig(t *testing.T) { + testCases := []struct { + name string + tags KeyValueTags + defaultConfig *DefaultConfig + want map[string]string + }{ + { + name: "empty config", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{}, + want: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "no config", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: nil, + want: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "no tags", + tags: New(map[string]string{}), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + }, + want: map[string]string{}, + }, + { + name: "keys all matching", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + }, + want: map[string]string{}, + }, + { + name: "keys some matching", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + }), + }, + want: map[string]string{ + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "keys some overridden", + tags: New(map[string]string{ + "key1": "value2", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key1": "value1", + }), + }, + want: map[string]string{ + "key1": "value2", + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "keys none matching", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + defaultConfig: &DefaultConfig{ + Tags: New(map[string]string{ + "key4": "value4", + "key5": "value5", + "key6": "value6", + }), + }, + want: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := testCase.tags.RemoveDefaultConfig(testCase.defaultConfig) + + testKeyValueTagsVerifyMap(t, got.Map(), testCase.want) + }) + } +} + func TestKeyValueTagsUrlEncode(t *testing.T) { testCases := []struct { name string diff --git a/aws/provider.go b/aws/provider.go index 5de81c4636c..3010d126756 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -89,6 +89,23 @@ func Provider() *schema.Provider { Set: schema.HashString, }, + "default_tags": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "Configuration block with settings to default resource tags across all resources.", + 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", + }, + }, + }, + }, + "endpoints": endpointsSchema(), "ignore_tags": { @@ -1337,6 +1354,7 @@ func providerConfigure(d *schema.ResourceData, terraformVersion string) (interfa Token: d.Get("token").(string), Region: d.Get("region").(string), CredsFilename: d.Get("shared_credentials_file").(string), + DefaultTagsConfig: expandProviderDefaultTags(d.Get("default_tags").([]interface{})), Endpoints: make(map[string]string), MaxRetries: d.Get("max_retries").(int), IgnoreTagsConfig: expandProviderIgnoreTags(d.Get("ignore_tags").([]interface{})), @@ -1522,6 +1540,20 @@ func endpointsSchema() *schema.Schema { } } +func expandProviderDefaultTags(l []interface{}) *keyvaluetags.DefaultConfig { + if len(l) == 0 || l[0] == nil { + return nil + } + + defaultConfig := &keyvaluetags.DefaultConfig{} + m := l[0].(map[string]interface{}) + + if v, ok := m["tags"].(map[string]interface{}); ok { + defaultConfig.Tags = keyvaluetags.New(v) + } + return defaultConfig +} + func expandProviderIgnoreTags(l []interface{}) *keyvaluetags.IgnoreConfig { if len(l) == 0 || l[0] == nil { return nil diff --git a/aws/provider_test.go b/aws/provider_test.go index 70af59809db..69f5d987c2e 100644 --- a/aws/provider_test.go +++ b/aws/provider_test.go @@ -888,6 +888,38 @@ func testAccMultipleRegionProviderConfig(regions int) string { return config.String() } +func testAccProviderConfigDefaultAndIgnoreTagsKeyPrefixes1(key1, value1, keyPrefix1 string) string { + //lintignore:AT004 + return fmt.Sprintf(` +provider "aws" { + default_tags { + tags = { + %q = %q + } + } + ignore_tags { + key_prefixes = [%q] + } +} +`, key1, value1, keyPrefix1) +} + +func testAccProviderConfigDefaultAndIgnoreTagsKeys1(key1, value1 string) string { + //lintignore:AT004 + return fmt.Sprintf(` +provider "aws" { + default_tags { + tags = { + %[1]q = %q + } + } + ignore_tags { + keys = [%[1]q] + } +} +`, key1, value1) +} + func testAccProviderConfigIgnoreTagsKeyPrefixes1(keyPrefix1 string) string { //lintignore:AT004 return fmt.Sprintf(` @@ -1118,6 +1150,101 @@ func testSweepSkipResourceError(err error) bool { return tfawserr.ErrCodeContains(err, "AccessDenied") } +func TestAccProvider_DefaultTags_EmptyConfigurationBlock(t *testing.T) { + var providers []*schema.Provider + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSProviderConfigDefaultTagsEmptyConfigurationBlock(), + Check: resource.ComposeTestCheckFunc( + testAccCheckProviderDefaultTags_Tags(&providers, map[string]string{}), + ), + }, + }, + }) +} + +func TestAccAWSProvider_DefaultTags_Tags_None(t *testing.T) { + var providers []*schema.Provider + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSProviderConfigDefaultTags_Tags0(), + Check: resource.ComposeTestCheckFunc( + testAccCheckProviderDefaultTags_Tags(&providers, map[string]string{}), + ), + }, + }, + }) +} + +func TestAccAWSProvider_DefaultTags_Tags_One(t *testing.T) { + var providers []*schema.Provider + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSProviderConfigDefaultTags_Tags1("test", "value"), + Check: resource.ComposeTestCheckFunc( + testAccCheckProviderDefaultTags_Tags(&providers, map[string]string{"test": "value"}), + ), + }, + }, + }) +} + +func TestAccAWSProvider_DefaultTags_Tags_Multiple(t *testing.T) { + var providers []*schema.Provider + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSProviderConfigDefaultTags_Tags2("test1", "value1", "test2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckProviderDefaultTags_Tags(&providers, map[string]string{ + "test1": "value1", + "test2": "value2", + }), + ), + }, + }, + }) +} + +func TestAccAWSProvider_DefaultAndIgnoreTags_EmptyConfigurationBlocks(t *testing.T) { + var providers []*schema.Provider + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccAWSProviderConfigDefaultAndIgnoreTagsEmptyConfigurationBlock(), + Check: resource.ComposeTestCheckFunc( + testAccCheckProviderDefaultTags_Tags(&providers, map[string]string{}), + testAccCheckAWSProviderIgnoreTagsKeys(&providers, []string{}), + testAccCheckAWSProviderIgnoreTagsKeyPrefixes(&providers, []string{}), + ), + }, + }, + }) +} + func TestAccAWSProvider_Endpoints(t *testing.T) { var providers []*schema.Provider var endpoints strings.Builder @@ -1613,6 +1740,69 @@ func testAccCheckAWSProviderIgnoreTagsKeys(providers *[]*schema.Provider, expect } } +func testAccCheckProviderDefaultTags_Tags(providers *[]*schema.Provider, expectedTags map[string]string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if providers == nil { + return fmt.Errorf("no providers initialized") + } + + for _, provider := range *providers { + if provider == nil || provider.Meta() == nil || provider.Meta().(*AWSClient) == nil { + continue + } + + providerClient := provider.Meta().(*AWSClient) + defaultTagsConfig := providerClient.DefaultTagsConfig + + if defaultTagsConfig == nil || len(defaultTagsConfig.Tags) == 0 { + if len(expectedTags) != 0 { + return fmt.Errorf("expected keys (%d) length, got: 0", len(expectedTags)) + } + + continue + } + + actualTags := defaultTagsConfig.Tags + + if len(actualTags) != len(expectedTags) { + return fmt.Errorf("expected tags (%d) length, got: %d", len(expectedTags), len(actualTags)) + } + + for _, expectedElement := range expectedTags { + var found bool + + for _, actualElement := range actualTags { + if *actualElement.Value == expectedElement { + found = true + break + } + } + + if !found { + return fmt.Errorf("expected tags element, but was missing: %s", expectedElement) + } + } + + for _, actualElement := range actualTags { + var found bool + + for _, expectedElement := range expectedTags { + if *actualElement.Value == expectedElement { + found = true + break + } + } + + if !found { + return fmt.Errorf("unexpected tags element: %s", actualElement) + } + } + } + + return nil + } +} + func testAccCheckAWSProviderPartition(providers *[]*schema.Provider, expectedPartition string) resource.TestCheckFunc { return func(s *terraform.State) error { if providers == nil { @@ -1709,72 +1899,147 @@ func testAccDefaultSubnetCount(t *testing.T) int { return len(output.Subnets) } -func testAccAWSProviderConfigEndpoints(endpoints string) string { +func testAccAWSProviderConfigDefaultTags_Tags0() string { //lintignore:AT004 - return fmt.Sprintf(` + return composeConfig( + testAccProviderConfigBase, + ` provider "aws" { skip_credentials_validation = true skip_get_ec2_platforms = true skip_metadata_api_check = true skip_requesting_account_id = true +} +`) +} - endpoints { - %[1]s +func testAccAWSProviderConfigDefaultTags_Tags1(tag1, value1 string) string { + //lintignore:AT004 + return composeConfig( + testAccProviderConfigBase, + fmt.Sprintf(` +provider "aws" { + default_tags { + tags = { + %q = %q + } } + + skip_credentials_validation = true + skip_get_ec2_platforms = true + skip_metadata_api_check = true + skip_requesting_account_id = true +} +`, tag1, value1)) } -data "aws_partition" "provider_test" {} +func testAccAWSProviderConfigDefaultTags_Tags2(tag1, value1, tag2, value2 string) string { + //lintignore:AT004 + return composeConfig( + testAccProviderConfigBase, + fmt.Sprintf(` +provider "aws" { + default_tags { + tags = { + %q = %q + %q = %q + } + } -# Required to initialize the provider -data "aws_arn" "test" { - arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" + skip_credentials_validation = true + skip_get_ec2_platforms = true + skip_metadata_api_check = true + skip_requesting_account_id = true } -`, endpoints) +`, tag1, value1, tag2, value2)) } -func testAccAWSProviderConfigIgnoreTagsEmptyConfigurationBlock() string { +func testAccAWSProviderConfigDefaultTagsEmptyConfigurationBlock() string { //lintignore:AT004 - return ` + return composeConfig( + testAccProviderConfigBase, + ` provider "aws" { - ignore_tags {} + default_tags {} skip_credentials_validation = true skip_get_ec2_platforms = true skip_metadata_api_check = true skip_requesting_account_id = true } +`) +} -data "aws_partition" "provider_test" {} +func testAccAWSProviderConfigDefaultAndIgnoreTagsEmptyConfigurationBlock() string { + //lintignore:AT004 + return composeConfig( + testAccProviderConfigBase, + ` +provider "aws" { + default_tags {} + ignore_tags {} -# Required to initialize the provider -data "aws_arn" "test" { - arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" + skip_credentials_validation = true + skip_get_ec2_platforms = true + skip_metadata_api_check = true + skip_requesting_account_id = true } -` +`) } -func testAccAWSProviderConfigIgnoreTagsKeyPrefixes0() string { +func testAccAWSProviderConfigEndpoints(endpoints string) string { //lintignore:AT004 - return ` + return composeConfig( + testAccProviderConfigBase, + fmt.Sprintf(` provider "aws" { skip_credentials_validation = true skip_get_ec2_platforms = true skip_metadata_api_check = true skip_requesting_account_id = true + + endpoints { + %[1]s + } +} +`, endpoints)) } -data "aws_partition" "provider_test" {} +func testAccAWSProviderConfigIgnoreTagsEmptyConfigurationBlock() string { + //lintignore:AT004 + return composeConfig( + testAccProviderConfigBase, + ` +provider "aws" { + ignore_tags {} -# Required to initialize the provider -data "aws_arn" "test" { - arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" + skip_credentials_validation = true + skip_get_ec2_platforms = true + skip_metadata_api_check = true + skip_requesting_account_id = true } -` +`) +} + +func testAccAWSProviderConfigIgnoreTagsKeyPrefixes0() string { + //lintignore:AT004 + return composeConfig( + testAccProviderConfigBase, + ` +provider "aws" { + skip_credentials_validation = true + skip_get_ec2_platforms = true + skip_metadata_api_check = true + skip_requesting_account_id = true +} +`) } func testAccAWSProviderConfigIgnoreTagsKeyPrefixes1(tagPrefix1 string) string { //lintignore:AT004 - return fmt.Sprintf(` + return composeConfig( + testAccProviderConfigBase, + fmt.Sprintf(` provider "aws" { ignore_tags { key_prefixes = [%[1]q] @@ -1785,19 +2050,14 @@ provider "aws" { skip_metadata_api_check = true skip_requesting_account_id = true } - -data "aws_partition" "provider_test" {} - -# Required to initialize the provider -data "aws_arn" "test" { - arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" -} -`, tagPrefix1) +`, tagPrefix1)) } func testAccAWSProviderConfigIgnoreTagsKeyPrefixes2(tagPrefix1, tagPrefix2 string) string { //lintignore:AT004 - return fmt.Sprintf(` + return composeConfig( + testAccProviderConfigBase, + fmt.Sprintf(` provider "aws" { ignore_tags { key_prefixes = [%[1]q, %[2]q] @@ -1808,38 +2068,28 @@ provider "aws" { skip_metadata_api_check = true skip_requesting_account_id = true } - -data "aws_partition" "provider_test" {} - -# Required to initialize the provider -data "aws_arn" "test" { - arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" -} -`, tagPrefix1, tagPrefix2) +`, tagPrefix1, tagPrefix2)) } func testAccAWSProviderConfigIgnoreTagsKeys0() string { //lintignore:AT004 - return ` + return composeConfig( + testAccProviderConfigBase, + ` provider "aws" { skip_credentials_validation = true skip_get_ec2_platforms = true skip_metadata_api_check = true skip_requesting_account_id = true } - -data "aws_partition" "provider_test" {} - -# Required to initialize the provider -data "aws_arn" "test" { - arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" -} -` +`) } func testAccAWSProviderConfigIgnoreTagsKeys1(tag1 string) string { //lintignore:AT004 - return fmt.Sprintf(` + return composeConfig( + testAccProviderConfigBase, + fmt.Sprintf(` provider "aws" { ignore_tags { keys = [%[1]q] @@ -1850,19 +2100,14 @@ provider "aws" { skip_metadata_api_check = true skip_requesting_account_id = true } - -data "aws_partition" "provider_test" {} - -# Required to initialize the provider -data "aws_arn" "test" { - arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" -} -`, tag1) +`, tag1)) } func testAccAWSProviderConfigIgnoreTagsKeys2(tag1, tag2 string) string { //lintignore:AT004 - return fmt.Sprintf(` + return composeConfig( + testAccProviderConfigBase, + fmt.Sprintf(` provider "aws" { ignore_tags { keys = [%[1]q, %[2]q] @@ -1873,19 +2118,14 @@ provider "aws" { skip_metadata_api_check = true skip_requesting_account_id = true } - -data "aws_partition" "provider_test" {} - -# Required to initialize the provider -data "aws_arn" "test" { - arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" -} -`, tag1, tag2) +`, tag1, tag2)) } func testAccAWSProviderConfigRegion(region string) string { //lintignore:AT004 - return fmt.Sprintf(` + return composeConfig( + testAccProviderConfigBase, + fmt.Sprintf(` provider "aws" { region = %[1]q skip_credentials_validation = true @@ -1893,14 +2133,7 @@ provider "aws" { skip_metadata_api_check = true skip_requesting_account_id = true } - -data "aws_partition" "provider_test" {} - -# Required to initialize the provider -data "aws_arn" "test" { - arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" -} -`, region) +`, region)) } func testAccAssumeRoleARNPreCheck(t *testing.T) { @@ -1928,6 +2161,15 @@ provider "aws" { data "aws_caller_identity" "current" {} ` //lintignore:AT004 +const testAccProviderConfigBase = ` +data "aws_partition" "provider_test" {} + +# Required to initialize the provider +data "aws_arn" "test" { + arn = "arn:${data.aws_partition.provider_test.partition}:s3:::test" +} +` + func testCheckResourceAttrIsSortedCsv(resourceName, attributeName string) resource.TestCheckFunc { return func(s *terraform.State) error { is, err := primaryInstanceState(s, resourceName) diff --git a/aws/resource_aws_subnet.go b/aws/resource_aws_subnet.go index d93e6ee1257..46e98235d3c 100644 --- a/aws/resource_aws_subnet.go +++ b/aws/resource_aws_subnet.go @@ -24,6 +24,8 @@ func resourceAwsSubnet() *schema.Resource { State: schema.ImportStatePassthrough, }, + CustomizeDiff: SetTagsDiff, + Timeouts: &schema.ResourceTimeout{ Create: schema.DefaultTimeout(10 * time.Minute), Delete: schema.DefaultTimeout(20 * time.Minute), @@ -111,6 +113,8 @@ func resourceAwsSubnet() *schema.Resource { "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), + "owner_id": { Type: schema.TypeString, Computed: true, @@ -121,13 +125,15 @@ func resourceAwsSubnet() *schema.Resource { func resourceAwsSubnetCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).ec2conn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) createOpts := &ec2.CreateSubnetInput{ AvailabilityZone: aws.String(d.Get("availability_zone").(string)), AvailabilityZoneId: aws.String(d.Get("availability_zone_id").(string)), CidrBlock: aws.String(d.Get("cidr_block").(string)), VpcId: aws.String(d.Get("vpc_id").(string)), - TagSpecifications: ec2TagSpecificationsFromMap(d.Get("tags").(map[string]interface{}), ec2.ResourceTypeSubnet), + TagSpecifications: ec2TagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeSubnet), } if v, ok := d.GetOk("ipv6_cidr_block"); ok { @@ -223,6 +229,7 @@ func resourceAwsSubnetCreate(d *schema.ResourceData, meta interface{}) error { func resourceAwsSubnetRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).ec2conn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig resp, err := conn.DescribeSubnets(&ec2.DescribeSubnetsInput{ @@ -267,10 +274,17 @@ func resourceAwsSubnetRead(d *schema.ResourceData, meta interface{}) error { d.Set("arn", subnet.SubnetArn) - if err := d.Set("tags", keyvaluetags.Ec2KeyValueTags(subnet.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + tags := keyvaluetags.Ec2KeyValueTags(subnet.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { return fmt.Errorf("error setting tags: %w", err) } + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) + } + d.Set("owner_id", subnet.OwnerId) return nil @@ -279,8 +293,8 @@ func resourceAwsSubnetRead(d *schema.ResourceData, meta interface{}) error { func resourceAwsSubnetUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).ec2conn - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.Ec2UpdateTags(conn, d.Id(), o, n); err != nil { return fmt.Errorf("error updating EC2 Subnet (%s) tags: %w", d.Id(), err) diff --git a/aws/resource_aws_subnet_test.go b/aws/resource_aws_subnet_test.go index fed7b085741..10215d5c9b2 100644 --- a/aws/resource_aws_subnet_test.go +++ b/aws/resource_aws_subnet_test.go @@ -203,6 +203,328 @@ func TestAccAWSSubnet_tags(t *testing.T) { }) } +func TestAccAWSSubnet_defaultTags_providerOnly(t *testing.T) { + var providers []*schema.Provider + var subnet ec2.Subnet + resourceName := "aws_subnet.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: testAccCheckSubnetDestroy, + Steps: []resource.TestStep{ + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("providerkey1", "providervalue1"), + testAccSubnetConfig, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckSubnetExists(resourceName, &subnet), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.providerkey1", "providervalue1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags2("providerkey1", "providervalue1", "providerkey2", "providervalue2"), + testAccSubnetConfig, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckSubnetExists(resourceName, &subnet), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.providerkey1", "providervalue1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.providerkey2", "providervalue2"), + ), + }, + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("providerkey1", "value1"), + testAccSubnetConfig, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckSubnetExists(resourceName, &subnet), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.providerkey1", "value1"), + ), + }, + }, + }) +} + +func TestAccAWSSubnet_defaultTags_updateToProviderOnly(t *testing.T) { + var providers []*schema.Provider + var subnet ec2.Subnet + resourceName := "aws_subnet.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: testAccCheckSubnetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSubnetTagsConfig1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckSubnetExists(resourceName, &subnet), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key1", "value1"), + ), + }, + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("key1", "value1"), + testAccSubnetConfig, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckSubnetExists(resourceName, &subnet), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSSubnet_defaultTags_updateToResourceOnly(t *testing.T) { + var providers []*schema.Provider + var subnet ec2.Subnet + resourceName := "aws_subnet.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: testAccCheckSubnetDestroy, + Steps: []resource.TestStep{ + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("key1", "value1"), + testAccSubnetConfig, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckSubnetExists(resourceName, &subnet), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key1", "value1"), + ), + }, + { + Config: testAccSubnetTagsConfig1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckSubnetExists(resourceName, &subnet), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSSubnet_defaultTags_providerAndResource_nonOverlappingTag(t *testing.T) { + var providers []*schema.Provider + var subnet ec2.Subnet + resourceName := "aws_subnet.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: testAccCheckSubnetDestroy, + Steps: []resource.TestStep{ + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("providerkey1", "providervalue1"), + testAccSubnetTagsConfig1(rName, "resourcekey1", "resourcevalue1"), + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckSubnetExists(resourceName, &subnet), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.resourcekey1", "resourcevalue1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.providerkey1", "providervalue1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.resourcekey1", "resourcevalue1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("providerkey1", "providervalue1"), + testAccSubnetTagsConfig2(rName, "resourcekey1", "resourcevalue1", "resourcekey2", "resourcevalue2"), + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckSubnetExists(resourceName, &subnet), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "3"), + resource.TestCheckResourceAttr(resourceName, "tags.resourcekey1", "resourcevalue1"), + resource.TestCheckResourceAttr(resourceName, "tags.resourcekey2", "resourcevalue2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.providerkey1", "providervalue1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.resourcekey1", "resourcevalue1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.resourcekey2", "resourcevalue2"), + ), + }, + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("providerkey2", "providervalue2"), + testAccSubnetTagsConfig1(rName, "resourcekey3", "resourcevalue3"), + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckSubnetExists(resourceName, &subnet), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.resourcekey3", "resourcevalue3"), + resource.TestCheckResourceAttr(resourceName, "tags_all.providerkey2", "providervalue2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.resourcekey3", "resourcevalue3"), + ), + }, + }, + }) +} + +func TestAccAWSSubnet_defaultTags_providerAndResource_overlappingTag(t *testing.T) { + var providers []*schema.Provider + var subnet ec2.Subnet + resourceName := "aws_subnet.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: testAccCheckSubnetDestroy, + Steps: []resource.TestStep{ + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("overlapkey1", "providervalue1"), + testAccSubnetTagsConfig1(rName, "overlapkey1", "resourcevalue1"), + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckSubnetExists(resourceName, &subnet), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.overlapkey1", "resourcevalue1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags2("overlapkey1", "providervalue1", "overlapkey2", "providervalue2"), + testAccSubnetTagsConfig2(rName, "overlapkey1", "resourcevalue1", "overlapkey2", "resourcevalue2"), + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckSubnetExists(resourceName, &subnet), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.overlapkey1", "resourcevalue1"), + resource.TestCheckResourceAttr(resourceName, "tags.overlapkey2", "resourcevalue2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.overlapkey1", "resourcevalue1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.overlapkey2", "resourcevalue2"), + ), + }, + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("overlapkey1", "providervalue1"), + testAccSubnetTagsConfig1(rName, "overlapkey1", "resourcevalue2"), + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckSubnetExists(resourceName, &subnet), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.overlapkey1", "resourcevalue2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.overlapkey1", "resourcevalue2"), + ), + }, + }, + }) +} + +func TestAccAWSSubnet_defaultTags_providerAndResource_duplicateTag(t *testing.T) { + var providers []*schema.Provider + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("overlapkey", "overlapvalue"), + testAccSubnetTagsConfig1(rName, "overlapkey", "overlapvalue"), + ), + PlanOnly: true, + ExpectError: regexp.MustCompile(`"tags" are identical to those in the "default_tags" configuration block`), + }, + }, + }) +} + +func TestAccAWSSubnet_defaultAndIgnoreTags(t *testing.T) { + var providers []*schema.Provider + var subnet ec2.Subnet + resourceName := "aws_subnet.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: testAccCheckSubnetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSubnetTagsConfig1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckSubnetExists(resourceName, &subnet), + testAccCheckSubnetUpdateTags(&subnet, nil, map[string]string{"defaultkey1": "defaultvalue1"}), + ), + ExpectNonEmptyPlan: true, + }, + { + Config: composeConfig( + testAccProviderConfigDefaultAndIgnoreTagsKeyPrefixes1("defaultkey1", "defaultvalue1", "defaultkey"), + testAccSubnetTagsConfig1(rName, "key1", "value1"), + ), + PlanOnly: true, + }, + { + Config: composeConfig( + testAccProviderConfigDefaultAndIgnoreTagsKeys1("defaultkey1", "defaultvalue1"), + testAccSubnetTagsConfig1(rName, "key1", "value1"), + ), + PlanOnly: true, + }, + }, + }) +} + func TestAccAWSSubnet_ignoreTags(t *testing.T) { var providers []*schema.Provider var subnet ec2.Subnet diff --git a/aws/resource_aws_vpc.go b/aws/resource_aws_vpc.go index b84b1bd87d7..8b2a82b740d 100644 --- a/aws/resource_aws_vpc.go +++ b/aws/resource_aws_vpc.go @@ -10,6 +10,7 @@ import ( "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -28,7 +29,11 @@ func resourceAwsVpc() *schema.Resource { Importer: &schema.ResourceImporter{ State: resourceAwsVpcInstanceImport, }, - CustomizeDiff: resourceAwsVpcCustomizeDiff, + + CustomizeDiff: customdiff.All( + resourceAwsVpcCustomizeDiff, + SetTagsDiff, + ), SchemaVersion: 1, MigrateState: resourceAwsVpcMigrateState, @@ -120,6 +125,8 @@ func resourceAwsVpc() *schema.Resource { "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), + "owner_id": { Type: schema.TypeString, Computed: true, @@ -130,13 +137,15 @@ func resourceAwsVpc() *schema.Resource { func resourceAwsVpcCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).ec2conn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) // Create the VPC createOpts := &ec2.CreateVpcInput{ CidrBlock: aws.String(d.Get("cidr_block").(string)), InstanceTenancy: aws.String(d.Get("instance_tenancy").(string)), AmazonProvidedIpv6CidrBlock: aws.Bool(d.Get("assign_generated_ipv6_cidr_block").(bool)), - TagSpecifications: ec2TagSpecificationsFromMap(d.Get("tags").(map[string]interface{}), ec2.ResourceTypeVpc), + TagSpecifications: ec2TagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeVpc), } log.Printf("[DEBUG] VPC create config: %#v", *createOpts) @@ -238,6 +247,7 @@ func resourceAwsVpcCreate(d *schema.ResourceData, meta interface{}) error { func resourceAwsVpcRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).ec2conn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig // Refresh the VPC state @@ -267,10 +277,17 @@ func resourceAwsVpcRead(d *schema.ResourceData, meta interface{}) error { }.String() d.Set("arn", arn) - if err := d.Set("tags", keyvaluetags.Ec2KeyValueTags(vpc.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + tags := keyvaluetags.Ec2KeyValueTags(vpc.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { return fmt.Errorf("error setting tags: %s", err) } + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %s", err) + } + d.Set("owner_id", vpc.OwnerId) // Make sure those values are set, if an IPv6 block exists it'll be set in the loop @@ -513,8 +530,8 @@ func resourceAwsVpcUpdate(d *schema.ResourceData, meta interface{}) error { } } - if d.HasChange("tags") { - o, n := d.GetChange("tags") + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") if err := keyvaluetags.Ec2UpdateTags(conn, d.Id(), o, n); err != nil { return fmt.Errorf("error updating tags: %s", err) diff --git a/aws/resource_aws_vpc_test.go b/aws/resource_aws_vpc_test.go index d02d3480ff0..8b98bd3bde6 100644 --- a/aws/resource_aws_vpc_test.go +++ b/aws/resource_aws_vpc_test.go @@ -159,6 +159,322 @@ func TestAccAWSVpc_disappears(t *testing.T) { }) } +func TestAccAWSVpc_defaultTags_providerOnly(t *testing.T) { + var providers []*schema.Provider + var vpc ec2.Vpc + resourceName := "aws_vpc.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: testAccCheckVpcDestroy, + Steps: []resource.TestStep{ + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("providerkey1", "providervalue1"), + testAccVpcConfig, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.providerkey1", "providervalue1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags2("providerkey1", "providervalue1", "providerkey2", "providervalue2"), + testAccVpcConfig, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.providerkey1", "providervalue1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.providerkey2", "providervalue2"), + ), + }, + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("providerkey1", "value1"), + testAccVpcConfig, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.providerkey1", "value1"), + ), + }, + }, + }) +} + +func TestAccAWSVpc_defaultTags_updateToProviderOnly(t *testing.T) { + var providers []*schema.Provider + var vpc ec2.Vpc + resourceName := "aws_vpc.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: testAccCheckVpcDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSVPCConfigTags1("key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key1", "value1"), + ), + }, + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("key1", "value1"), + testAccVpcConfig, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSVpc_defaultTags_updateToResourceOnly(t *testing.T) { + var providers []*schema.Provider + var vpc ec2.Vpc + resourceName := "aws_vpc.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: testAccCheckVpcDestroy, + Steps: []resource.TestStep{ + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("key1", "value1"), + testAccVpcConfig, + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key1", "value1"), + ), + }, + { + Config: testAccAWSVPCConfigTags1("key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSVpc_defaultTags_providerAndResource_nonOverlappingTag(t *testing.T) { + var providers []*schema.Provider + var vpc ec2.Vpc + resourceName := "aws_vpc.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: testAccCheckVpcDestroy, + Steps: []resource.TestStep{ + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("providerkey1", "providervalue1"), + testAccAWSVPCConfigTags1("resourcekey1", "resourcevalue1"), + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.resourcekey1", "resourcevalue1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.providerkey1", "providervalue1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.resourcekey1", "resourcevalue1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("providerkey1", "providervalue1"), + testAccAWSVPCConfigTags2("resourcekey1", "resourcevalue1", "resourcekey2", "resourcevalue2"), + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "3"), + resource.TestCheckResourceAttr(resourceName, "tags.resourcekey1", "resourcevalue1"), + resource.TestCheckResourceAttr(resourceName, "tags.resourcekey2", "resourcevalue2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.providerkey1", "providervalue1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.resourcekey1", "resourcevalue1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.resourcekey2", "resourcevalue2"), + ), + }, + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("providerkey2", "providervalue2"), + testAccAWSVPCConfigTags1("resourcekey3", "resourcevalue3"), + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.resourcekey3", "resourcevalue3"), + resource.TestCheckResourceAttr(resourceName, "tags_all.providerkey2", "providervalue2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.resourcekey3", "resourcevalue3"), + ), + }, + }, + }) +} + +func TestAccAWSVpc_defaultTags_providerAndResource_overlappingTag(t *testing.T) { + var providers []*schema.Provider + var vpc ec2.Vpc + resourceName := "aws_vpc.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: testAccCheckVpcDestroy, + Steps: []resource.TestStep{ + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("overlapkey1", "providervalue1"), + testAccAWSVPCConfigTags1("overlapkey1", "resourcevalue1"), + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.overlapkey1", "resourcevalue1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags2("overlapkey1", "providervalue1", "overlapkey2", "providervalue2"), + testAccAWSVPCConfigTags2("overlapkey1", "resourcevalue1", "overlapkey2", "resourcevalue2"), + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.overlapkey1", "resourcevalue1"), + resource.TestCheckResourceAttr(resourceName, "tags.overlapkey2", "resourcevalue2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.overlapkey1", "resourcevalue1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.overlapkey2", "resourcevalue2"), + ), + }, + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("overlapkey1", "providervalue1"), + testAccAWSVPCConfigTags1("overlapkey1", "resourcevalue2"), + ), + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists(resourceName, &vpc), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.overlapkey1", "resourcevalue2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.overlapkey1", "resourcevalue2"), + ), + }, + }, + }) +} + +func TestAccAWSVpc_defaultTags_providerAndResource_duplicateTag(t *testing.T) { + var providers []*schema.Provider + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: composeConfig( + testAccAWSProviderConfigDefaultTags_Tags1("overlapkey", "overlapvalue"), + testAccAWSVPCConfigTags1("overlapkey", "overlapvalue"), + ), + PlanOnly: true, + ExpectError: regexp.MustCompile(`"tags" are identical to those in the "default_tags" configuration block`), + }, + }, + }) +} + +func TestAccAWSVpc_defaultAndIgnoreTags(t *testing.T) { + var providers []*schema.Provider + var vpc ec2.Vpc + resourceName := "aws_vpc.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactoriesInternal(&providers), + CheckDestroy: testAccCheckVpcDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSVPCConfigTags1("key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcExists(resourceName, &vpc), + testAccCheckVpcUpdateTags(&vpc, nil, map[string]string{"defaultkey1": "defaultvalue1"}), + ), + ExpectNonEmptyPlan: true, + }, + { + Config: composeConfig( + testAccProviderConfigDefaultAndIgnoreTagsKeyPrefixes1("defaultkey1", "defaultvalue1", "defaultkey"), + testAccAWSVPCConfigTags1("key1", "value1"), + ), + PlanOnly: true, + }, + { + Config: composeConfig( + testAccProviderConfigDefaultAndIgnoreTagsKeys1("defaultkey1", "defaultvalue1"), + testAccAWSVPCConfigTags1("key1", "value1"), + ), + PlanOnly: true, + }, + }, + }) +} + func TestAccAWSVpc_ignoreTags(t *testing.T) { var providers []*schema.Provider var vpc ec2.Vpc diff --git a/aws/tags.go b/aws/tags.go index cf9ed21dfe9..d7b5f96580a 100644 --- a/aws/tags.go +++ b/aws/tags.go @@ -1,6 +1,9 @@ package aws import ( + "context" + "fmt" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -75,3 +78,42 @@ func ec2TagSpecificationsFromMap(m map[string]interface{}, t string) []*ec2.TagS }, } } + +// ec2TagSpecificationsFromKeyValueTags returns the tag specifications for the given KeyValueTags object and resource type. +func ec2TagSpecificationsFromKeyValueTags(tags keyvaluetags.KeyValueTags, t string) []*ec2.TagSpecification { + if len(tags) == 0 { + return nil + } + + return []*ec2.TagSpecification{ + { + ResourceType: aws.String(t), + Tags: tags.IgnoreAws().Ec2Tags(), + }, + } +} + +// SetTagsDiff sets the new plan difference with the result of +// merging resource tags on to those defined at the provider-level; +// returns an error if unsuccessful or if the resource tags are identical +// to those configured at the provider-level to avoid non-empty plans +// after resource READ operations as resource and provider-level tags +// will be indistinguishable when returned from an AWS API. +func SetTagsDiff(_ context.Context, diff *schema.ResourceDiff, meta interface{}) error { + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + resourceTags := keyvaluetags.New(diff.Get("tags").(map[string]interface{})) + + if defaultTagsConfig.TagsEqual(resourceTags) { + return fmt.Errorf(`"tags" are identical to those in the "default_tags" configuration block of the provider: please de-duplicate and try again`) + } + + allTags := defaultTagsConfig.MergeTags(resourceTags).IgnoreConfig(ignoreTagsConfig) + + if err := diff.SetNew("tags_all", allTags.Map()); err != nil { + return fmt.Errorf("error setting new tags_all diff: %w", err) + } + + return nil +} diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index ee1f9fed757..c7617475a27 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -215,6 +215,14 @@ for more information about connecting to alternate AWS endpoints or AWS compatib AWS account IDs to prevent you from mistakenly using the wrong one (and potentially end up destroying a live environment). Conflicts with `allowed_account_ids`. + +* `default_tags` - (Optional) **NOTE: This functionality is in public preview and there are no compatibility promises with future versions of the Terraform AWS Provider until a general availability announcement.** +Map of tags to apply across all resources handled by this provider (see the [Terraform multiple provider instances documentation](/docs/configuration/providers.html#alias-multiple-provider-instances) for more information about additional provider configurations). +This is designed to replace redundant per-resource `tags` configurations. At this time, tags defined within this configuration block can be overridden with new values, but not excluded from specific resources. To override tag values defined within this configuration block, use the `tags` argument within a resource to configure new tag values for matching keys. +See the [`default_tags`](#default_tags-configuration-block) Configuration Block section below for example usage and available arguments. +This functionality is only supported in the following resources: + - `aws_subnet` + - `aws_vpc` * `ignore_tags` - (Optional) Configuration block with resource tag settings to ignore across all resources handled by this provider (except any individual service tag resources such as `aws_ec2_tag`) for situations where external systems are managing certain resource tags. Arguments to the configuration block are described below in the `ignore_tags` Configuration Block section. See the [Terraform multiple provider instances documentation](https://www.terraform.io/docs/configuration/providers.html#alias-multiple-provider-configurations) for more information about additional provider configurations. @@ -382,6 +390,139 @@ The `assume_role` configuration block supports the following optional arguments: * `tags` - (Optional) Map of assume role session tags. * `transitive_tag_keys` - (Optional) Set of assume role session tag keys to pass to any subsequent sessions. +### default_tags Configuration Block + +Example: Resource with provider default tags + +```hcl +provider "aws" { + default_tags { + tags = { + Environment = "Test" + 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 +$ terraform apply +... +Outputs: + +vpc_all_tags = tomap({ + "Environment" = "Test" + "Name" = "Provider Tag" +}) +``` + +Example: Resource with tags and provider default tags + +```hcl +provider "aws" { + default_tags { + tags = { + Environment = "Test" + Name = "Provider Tag" + } + } +} + +resource "aws_vpc" "example" { + # ..other configuration... + tags = { + Owner = "example" + } +} + +output "vpc_resource_level_tags" { + value = aws_vpc.example.tags +} + +output "vpc_all_tags" { + value = aws_vpc.example.tags_all +} +``` + +Outputs: + +```console +$ terraform apply +... +Outputs: + +vpc_all_tags = tomap({ + "Environment" = "Test" + "Name" = "Provider Tag" + "Owner" = "example" +}) +vpc_resource_level_tags = tomap({ + "Owner" = "example" +}) +``` + +Example: Resource overriding provider default tags + +```hcl +provider "aws" { + default_tags { + tags = { + Environment = "Test" + Name = "Provider Tag" + } + } +} + +resource "aws_vpc" "example" { + # ..other configuration... + tags = { + Environment = "Production" + } +} + +output "vpc_resource_level_tags" { + value = aws_vpc.example.tags +} + +output "vpc_all_tags" { + value = aws_vpc.example.tags_all +} +``` + +Outputs: + +```console +$ terraform apply +... +Outputs: + +vpc_all_tags = tomap({ + "Environment" = "Production" + "Name" = "Provider Tag" +}) +vpc_resource_level_tags = tomap({ + "Environment" = "Production" +}) +``` + +The `default_tags` configuration block supports the following argument: + +* `tags` - (Optional) **NOTE: This functionality is in public preview and only supported by the [`aws_subnet`](/docs/providers/aws/r/subnet.html) and [`aws_vpc`](/docs/providers/aws/r/vpc.html) resources.** Key-value map of tags to apply to all resources. + ### ignore_tags Configuration Block Example: diff --git a/website/docs/r/subnet.html.markdown b/website/docs/r/subnet.html.markdown index 3c1d0e9b213..2d62279911e 100644 --- a/website/docs/r/subnet.html.markdown +++ b/website/docs/r/subnet.html.markdown @@ -63,7 +63,7 @@ The following arguments are supported: that network interfaces created in the specified subnet should be assigned an IPv6 address. Default is `false` * `vpc_id` - (Required) The VPC ID. -* `tags` - (Optional) A map of tags to assign to the resource. +* `tags` - (Optional) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](https://www.terraform.io/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. ## Attributes Reference @@ -73,6 +73,7 @@ In addition to all arguments above, the following attributes are exported: * `arn` - The ARN of the subnet. * `ipv6_cidr_block_association_id` - The association ID for the IPv6 CIDR block. * `owner_id` - The ID of the AWS account that owns the subnet. +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://www.terraform.io/docs/providers/aws/index.html#default_tags-configuration-block). ## Timeouts diff --git a/website/docs/r/vpc.html.markdown b/website/docs/r/vpc.html.markdown index 53f9f6dba53..5b4b7c3feb4 100644 --- a/website/docs/r/vpc.html.markdown +++ b/website/docs/r/vpc.html.markdown @@ -50,7 +50,7 @@ The following arguments are supported: * `assign_generated_ipv6_cidr_block` - (Optional) Requests an Amazon-provided IPv6 CIDR block with a /56 prefix length for the VPC. You cannot specify the range of IP addresses, or the size of the CIDR block. Default is `false`. -* `tags` - (Optional) A map of tags to assign to the resource. +* `tags` - (Optional) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](https://www.terraform.io/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. ## Attributes Reference @@ -72,7 +72,7 @@ In addition to all arguments above, the following attributes are exported: * `ipv6_association_id` - The association ID for the IPv6 CIDR block. * `ipv6_cidr_block` - The IPv6 CIDR block. * `owner_id` - The ID of the AWS account that owns the VPC. - +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://www.terraform.io/docs/providers/aws/index.html#default_tags-configuration-block). [1]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/vpc-classiclink.html