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

docs: default_tags contribution guidelines and resource support #19084

Merged
merged 4 commits into from
Apr 26, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/19084.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:note
provider: `default_tags` support generally available to all provider resources that support `tags` with the exception of `aws_autoscaling_group`
```
121 changes: 103 additions & 18 deletions docs/contributing/contribution-checklists.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,18 +241,21 @@ There are also functions `naming.TestCheckResourceAttrNameWithSuffixGenerated` a
## Adding Resource Policy Support

Some AWS components support [resource-based IAM policies](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_identity-vs-resource.html) to control permissions. When implementing this support in the Terraform AWS Provider, we typically prefer creating a separate resource, `aws_{SERVICE}_{THING}_policy` (e.g. `aws_s3_bucket_policy`). See the [New Resource section](#new-resource) for more information about implementing the separate resource and the [Provider Design page](provider-design.md) for rationale.

## Adding Resource Tagging Support

AWS provides key-value metadata across many services and resources, which can be used for a variety of use cases including billing, ownership, and more. See the [AWS Tagging Strategy page](https://aws.amazon.com/answers/account-management/aws-tagging-strategies/) for more information about tagging at a high level.

Implementing tagging support for Terraform AWS Provider resources requires the following, each with its own section below:
As of version 3.38.0 of the Terraform AWS Provider, resources that previously implemented tagging support via the argument `tags`, now support provide-wide default tagging.
anGie44 marked this conversation as resolved.
Show resolved Hide resolved

Thus, for in-flight and future contributions, implementing tagging support for Terraform AWS Provider resources requires the following, each with its own section below:

- [ ] _Generated Service Tagging Code_: In the internal code generators (e.g. `aws/internal/keyvaluetags`), implementation and customization of how a service handles tagging, which is standardized for the resources.
- [ ] _Resource Tagging Code Implementation_: In the resource code (e.g. `aws/resource_aws_service_thing.go`), implementation of `tags` schema attribute, along with handling in `Create`, `Read`, and `Update` functions.
- [ ] _Resource Tagging Code Implementation_: In the resource code (e.g. `aws/resource_aws_service_thing.go`), implementation of `tags` and `tags_all` schema attributes, along with implementation of `CustomizeDiff` in the resource definition and handling in `Create`, `Read`, and `Update` functions.
- [ ] _Resource Tagging Acceptance Testing Implementation_: In the resource acceptance testing (e.g. `aws/resource_aws_service_thing_test.go`), implementation of new acceptance test function and configurations to exercise new tagging logic.
- [ ] _Resource Tagging Documentation Implementation_: In the resource documentation (e.g. `website/docs/r/service_thing.html.markdown`), addition of `tags` argument
- [ ] _Resource Tagging Documentation Implementation_: In the resource documentation (e.g. `website/docs/r/service_thing.html.markdown`), addition of `tags` argument and `tags_all` attribute.

See also a [full example pull request for implementing EKS tagging](https://github.com/hashicorp/terraform-provider-aws/pull/10307).
See also a [full example pull request for implementing resource tags with default tags support](https://github.com/hashicorp/terraform-provider-aws/pull/18861).

### Adding Service to Tag Generating Code

Expand Down Expand Up @@ -281,89 +284,165 @@ More details about this code generation, including fixes for potential error mes
### Resource Tagging Code Implementation

- In the resource Go file (e.g. `aws/resource_aws_eks_cluster.go`), add the following Go import: `"github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags"`
- In the resource schema, add `"tags": tagsSchema(),`
- In the resource schema, add `"tags": tagsSchema(),` and `"tags_all": tagsSchemaComputed(),`
- In the `schema.Resource` struct definition, add the `CustomizeDiff: SetTagsDiff` handling essential to resource support for default tags:

```go
func resourceAwsEksCluster() *schema.Resource {
return &schema.Resource{
/* ... other configuration ... */
CustomizeDiff: SetTagsDiff,
}
}
```

If the resource already contains a `CustomizeDiff` function, append the `SetTagsDiff` via the `customdiff.Sequence` method:

```go
func resourceAwsExample() *schema.Resource {
return &schema.Resource{
/* ... other configuration ... */
CustomizeDiff: customdiff.Sequence(
resourceAwsExampleCustomizeDiff,
SetTagsDiff,
),
}
}
```

- If the API supports tagging on creation (the `Input` struct accepts a `Tags` field), in the resource `Create` function, implement the logic to convert the configuration tags into the service tags, e.g. with EKS Clusters:

```go
// Typically declared near conn := /* ... */
defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig
tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{})))

input := &eks.CreateClusterInput{
/* ... other configuration ... */
Tags: keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().EksTags(),
Tags: tags.IgnoreAws().EksTags(),
}
```

If the service API does not allow passing an empty list, the logic can be adjusted similar to:

```go
// Typically declared near conn := /* ... */
defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig
tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{})))

input := &eks.CreateClusterInput{
/* ... other configuration ... */
}

if v := d.Get("tags").(map[string]interface{}); len(v) > 0 {
input.Tags = keyvaluetags.New(v).IgnoreAws().EksTags()
if len(tags) > 0 {
input.Tags = tags.IgnoreAws().EksTags()
}
```

- Otherwise if the API does not support tagging on creation (the `Input` struct does not accept a `Tags` field), in the resource `Create` function, implement the logic to convert the configuration tags into the service API call to tag a resource, e.g. with ElasticSearch Domain:

```go
if v := d.Get("tags").(map[string]interface{}); len(v) > 0 {
if err := keyvaluetags.ElasticsearchserviceUpdateTags(conn, d.Id(), nil, v); err != nil {
// Typically declared near conn := /* ... */
defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig
tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{})))

if len(tags) > 0 {
if err := keyvaluetags.ElasticsearchserviceUpdateTags(conn, d.Id(), nil, tags); err != nil {
return fmt.Errorf("error adding Elasticsearch Cluster (%s) tags: %s", d.Id(), err)
}
}
```

- Some EC2 resources (for example [`aws_ec2_fleet`](https://www.terraform.io/docs/providers/aws/r/ec2_fleet.html)) have a `TagsSpecification` field in the `InputStruct` instead of a `Tags` field. In these cases the `ec2TagSpecificationsFromMap()` helper function should be used, e.g.:
- Some EC2 resources (for example [`aws_ec2_fleet`](https://www.terraform.io/docs/providers/aws/r/ec2_fleet.html)) have a `TagsSpecification` field in the `InputStruct` instead of a `Tags` field. In these cases the `ec2TagSpecificationsFromKeyValueTags()` helper function should be used, e.g.:

```go
// Typically declared near conn := /* ... */
defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig
tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{})))

input := &ec2.CreateFleetInput{
/* ... other configuration ... */
TagSpecifications: ec2TagSpecificationsFromMap(d.Get("tags").(map[string]interface{}), ec2.ResourceTypeFleet),
TagSpecifications: ec2TagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeFleet),
}
```

- In the resource `Read` function, implement the logic to convert the service tags to save them into the Terraform state for drift detection, e.g. with EKS Clusters (which had the tags available in the DescribeCluster API call):

```go
// Typically declared near conn := /* ... */
defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig
ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig

/* ... other d.Set(...) logic ... */

if err := d.Set("tags", keyvaluetags.EksKeyValueTags(cluster.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil {
tags := keyvaluetags.EksKeyValueTags(cluster.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig)

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)
}
```

If the service API does not return the tags directly from reading the resource and requires a separate API call, its possible to use the `keyvaluetags` functionality like the following, e.g. with Athena Workgroups:

```go
// Typically declared near conn := /* ... */
defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig
ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig

/* ... other d.Set(...) logic ... */

tags, err := keyvaluetags.AthenaListTags(conn, arn.String())

if err != nil {
return fmt.Errorf("error listing tags for resource (%s): %s", arn, err)
}

if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil {
tags = tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig)

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)
}
```

- In the resource `Update` function (this may be the first functionality requiring the creation of the `Update` function), implement the logic to handle tagging updates, e.g. with EKS Clusters:

```go
if d.HasChange("tags") {
o, n := d.GetChange("tags")
if d.HasChange("tags_all") {
o, n := d.GetChange("tags_all")
ewbankkit marked this conversation as resolved.
Show resolved Hide resolved
if err := keyvaluetags.EksUpdateTags(conn, d.Get("arn").(string), o, n); err != nil {
return fmt.Errorf("error updating tags: %s", err)
}
}
```

If the resource `Update` function applies specific updates to attributes regardless of changes to tags, implement the following e.g. with IAM Policy:

```go
if d.HasChangesExcept("tags", "tags_all") {
/* ... other logic ...*/
request := &iam.CreatePolicyVersionInput{
PolicyArn: aws.String(d.Id()),
PolicyDocument: aws.String(d.Get("policy").(string)),
SetAsDefault: aws.Bool(true),
}

if _, err := conn.CreatePolicyVersion(request); err != nil {
return fmt.Errorf("error updating IAM policy %s: %w", d.Id(), err)
}
}
```

### Resource Tagging Acceptance Testing Implementation

- In the resource testing (e.g. `aws/resource_aws_eks_cluster_test.go`), verify that existing resources without tagging are unaffected and do not have tags saved into their Terraform state. This should be done in the `_basic` acceptance test by adding a line similar to `resource.TestCheckResourceAttr(resourceName, "tags.%s", "0"),`
- In the resource testing (e.g. `aws/resource_aws_eks_cluster_test.go`), verify that existing resources without tagging are unaffected and do not have tags saved into their Terraform state. This should be done in the `_basic` acceptance test by adding one line similar to `resource.TestCheckResourceAttr(resourceName, "tags.%s", "0"),` and one similar to `resource.TestCheckResourceAttr(resourceName, "tags_all.%s", "0"),`
anGie44 marked this conversation as resolved.
Show resolved Hide resolved
- In the resource testing, implement a new test named `_Tags` with associated configurations, that verifies creating the resource with tags and updating tags. e.g. EKS Clusters:

```go
Expand Down Expand Up @@ -459,7 +538,13 @@ More details about this code generation, including fixes for potential error mes
- In the resource documentation (e.g. `website/docs/r/eks_cluster.html.markdown`), add the following to the arguments reference:

```markdown
* `tags` - (Optional) Key-value mapping of resource tags
* `tags` - (Optional) Key-value mapping of resource tags. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level.
```

- In the resource documentation (e.g. `website/docs/r/eks_cluster.html.markdown`), add the following to the attributes reference:

```markdown
* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block).
```

## New Resource
Expand Down
32 changes: 32 additions & 0 deletions website/docs/guides/resource-tagging.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Many AWS services implement [resource tags](https://docs.aws.amazon.com/general/
- [Ignoring Changes in Individual Resources](#ignoring-changes-in-individual-resources)
- [Ignoring Changes in All Resources](#ignoring-changes-in-all-resources)
- [Managing Individual Resource Tags](#managing-individual-resource-tags)
- [Propagating Tags to All Resources](#propagating-tags-to-all-resources)

<!-- /TOC -->

Expand Down Expand Up @@ -171,3 +172,34 @@ resource "aws_ec2_tag" "example" {
```

The inline map provided to `for_each` in the example above is used for brevity, but other Terraform configuration language features similar to those noted at the beginning of this guide can be used to make the example more extensible.

### Propagating Tags to All Resources

As of version 3.38.0 of the Terraform AWS Provider, the Terraform Configuration language also enables provider-level tagging as an alternative to the methods described in the [Getting Started with Resource Tags](#getting-started-with-resource-tags) section above.
This functionality is available for all Terraform AWS Provider resources that currently support `tags`, with the exception of the [`aws_autoscaling_group`](/docs/providers/aws/r/autoscaling_group.html.markdown) resource. Refactoring the use of [variables](https://www.terraform.io/docs/configuration/variables.html) or [locals](https://www.terraform.io/docs/configuration/locals.html) may look like:

```terraform
# Terraform 0.12 and later syntax
provider "aws" {
# ... other configuration ...
default_tags {
tags = {
Environment = "Production"
Owner = "Ops"
}
}
}

resource "aws_vpc" "example" {
# ... other configuration ...

# This configuration by default will internally combine tags defined
# within the provider configuration block and those defined here
tags = {
Name = "MyVPC"
}
}
```

In this example, the `Environment` and `Owner` tags defined within the provider configuration block will be added to the VPC on resource creation, in addition to the `Name` tag defined within the VPC resource configuration.
To access all the tags applied to the VPC resource, use the read-only attribute `tags_all`, e.g. `aws_vpc.example.tags_all`.
9 changes: 3 additions & 6 deletions website/docs/index.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,10 @@ for more information about connecting to alternate AWS endpoints or AWS compatib
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).
* `default_tags` - (Optional) 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`
This functionality is supported in all resources that implement `tags`, with the exception of the `aws_autoscaling_group` resource.

* `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.

Expand Down Expand Up @@ -513,7 +510,7 @@ vpc_resource_level_tags = tomap({

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.
* `tags` - (Optional) Key-value map of tags to apply to all resources.

### ignore_tags Configuration Block

Expand Down
4 changes: 2 additions & 2 deletions website/docs/r/subnet.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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.
* `tags` - (Optional) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level.

## Attributes Reference

Expand All @@ -73,7 +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).
* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block).

## Timeouts

Expand Down
Loading