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

r/aws_backup_selection: Adding resource to manage selections for AWS Backup plans #7382

Merged
merged 11 commits into from
Apr 4, 2019
1 change: 1 addition & 0 deletions aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ func Provider() terraform.ResourceProvider {
"aws_autoscaling_policy": resourceAwsAutoscalingPolicy(),
"aws_autoscaling_schedule": resourceAwsAutoscalingSchedule(),
"aws_backup_plan": resourceAwsBackupPlan(),
"aws_backup_selection": resourceAwsBackupSelection(),
"aws_backup_vault": resourceAwsBackupVault(),
"aws_budgets_budget": resourceAwsBudgetsBudget(),
"aws_cloud9_environment_ec2": resourceAwsCloud9EnvironmentEc2(),
Expand Down
182 changes: 182 additions & 0 deletions aws/resource_aws_backup_selection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package aws

import (
"bytes"
"fmt"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/backup"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
)

func resourceAwsBackupSelection() *schema.Resource {
return &schema.Resource{
Create: resourceAwsBackupSelectionCreate,
Read: resourceAwsBackupSelectionRead,
Delete: resourceAwsBackupSelectionDelete,

Schema: map[string]*schema.Schema{
"name": {
slapula marked this conversation as resolved.
Show resolved Hide resolved
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"plan_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"iam_role": {
slapula marked this conversation as resolved.
Show resolved Hide resolved
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"tag": {
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"type": {
slapula marked this conversation as resolved.
Show resolved Hide resolved
Type: schema.TypeString,
Required: true,
},
"key": {
Type: schema.TypeString,
Required: true,
},
"value": {
Type: schema.TypeString,
Required: true,
},
},
},
Set: resourceAwsConditionTagHash,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should simplify this attribute by removing the custom Set function and allowing Terraform to use its default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it matter if this resource is using these tags as a parameter to make backup plan selections vs. the normal tag/untag operations that Terraform handles? That's what I noticed when I first planned this out but I will admit I didn't try using the default functions first because I thought they wouldn't work in this context.

Copy link
Contributor

@bflad bflad Apr 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I must admit I'm not sure what you're referring to? 😅 All Terraform schema definitions are unrelated and the default TypeSet Set function applies to any TypeSet attribute. We happen to use a tags TypeMap for most Terraform AWS Provider resources as the resource tagging attribute, but that is only a convention to make it simpler for operators.

If you would like to clear up any confusion between a tag configuration block here and the typical tags map argument across the provider, you can always switch the naming here to selection_tag or something. It won't have any bearing on the TypeSet Set function either which way though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I have this working without the custom hash function this all makes sense. I think I misinterpreted your initial comment to imply that I use tagSchema() which doesn't make sense. Oops! Anyway, thanks for the clarification :-)

},
"resources": {
Type: schema.TypeList,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API seems to be unordered for this attribute:

--- FAIL: TestAccAwsBackupSelection_withResources (14.15s)
    testing.go:538: Step 0 error: After applying this step, the plan was not empty:

        DIFF:

        DESTROY/CREATE: aws_backup_selection.test
          iam_role_arn:                   "arn:aws:iam::123456789012:role/service-role/AWSBackupDefaultServiceRole" => "arn:aws:iam::187416307283:role/service-role/AWSBackupDefaultServiceRole"
          name:                           "tf_acc_test_backup_selection_5994421555613158377" => "tf_acc_test_backup_selection_5994421555613158377"
          plan_id:                        "7e69f127-aab4-4217-90cc-437ec8d9e19a" => "7e69f127-aab4-4217-90cc-437ec8d9e19a"
          resources.#:                    "2" => "2"
          resources.0:                    "arn:aws:ec2:us-west-2:123456789012:volume/" => "arn:aws:elasticfilesystem:us-west-2:123456789012:file-system/" (forces new resource)
          resources.1:                    "arn:aws:elasticfilesystem:us-west-2:123456789012:file-system/" => "arn:aws:ec2:us-west-2:123456789012:volume/" (forces new resource)
          selection_tag.#:                "1" => "1"
          selection_tag.3487478888.key:   "foo" => "foo"
          selection_tag.3487478888.type:  "STRINGEQUALS" => "STRINGEQUALS"
          selection_tag.3487478888.value: "bar" => "bar"

So this can be switched to TypeSet 👍

Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
}
}

func resourceAwsBackupSelectionCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).backupconn

selection := &backup.Selection{}

selection.SelectionName = aws.String(d.Get("name").(string))
slapula marked this conversation as resolved.
Show resolved Hide resolved
selection.IamRoleArn = aws.String(d.Get("iam_role").(string))
selection.ListOfTags = gatherConditionTags(d)
selection.Resources = expandStringList(d.Get("resources").([]interface{}))

input := &backup.CreateBackupSelectionInput{
BackupPlanId: aws.String(d.Get("plan_id").(string)),
BackupSelection: selection,
}

resp, err := conn.CreateBackupSelection(input)
if err != nil {
return fmt.Errorf("error creating Backup Selection: %s", err)
}

d.SetId(*resp.SelectionId)

return resourceAwsBackupSelectionRead(d, meta)
}

func resourceAwsBackupSelectionRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).backupconn

input := &backup.GetBackupSelectionInput{
BackupPlanId: aws.String(d.Get("plan_id").(string)),
SelectionId: aws.String(d.Id()),
}

resp, err := conn.GetBackupSelection(input)
if err != nil {
slapula marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("error reading Backup Selection: %s", err)
}

d.Set("plan_id", resp.BackupPlanId)
d.Set("name", resp.BackupSelection.SelectionName)
d.Set("iam_role", resp.BackupSelection.IamRoleArn)

if resp.BackupSelection.ListOfTags != nil {
tag := &schema.Set{F: resourceAwsConditionTagHash}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use a []interface{} variable here instead with append() below 👍


for _, r := range resp.BackupSelection.ListOfTags {
m := make(map[string]interface{})

m["type"] = *r.ConditionType
slapula marked this conversation as resolved.
Show resolved Hide resolved
m["key"] = *r.ConditionKey
m["value"] = *r.ConditionValue

tag.Add(m)
}

d.Set("tag", tag)
slapula marked this conversation as resolved.
Show resolved Hide resolved
}
if resp.BackupSelection.Resources != nil {
d.Set("resources", resp.BackupSelection.Resources)
slapula marked this conversation as resolved.
Show resolved Hide resolved
}

return nil
}

func resourceAwsBackupSelectionDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).backupconn

input := &backup.DeleteBackupSelectionInput{
BackupPlanId: aws.String(d.Get("plan_id").(string)),
SelectionId: aws.String(d.Id()),
}

_, err := conn.DeleteBackupSelection(input)
if err != nil {
return fmt.Errorf("error deleting Backup Selection: %s", err)
}

return nil
}

func gatherConditionTags(d *schema.ResourceData) []*backup.Condition {
slapula marked this conversation as resolved.
Show resolved Hide resolved
conditions := []*backup.Condition{}
selectionTags := d.Get("tag").(*schema.Set).List()

for _, i := range selectionTags {
item := i.(map[string]interface{})
tag := &backup.Condition{}

tag.ConditionType = aws.String(item["type"].(string))
tag.ConditionKey = aws.String(item["key"].(string))
tag.ConditionValue = aws.String(item["value"].(string))

conditions = append(conditions, tag)
}

return conditions
}

func resourceAwsConditionTagHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})

if v, ok := m["type"]; ok {
buf.WriteString(fmt.Sprintf("%s-", v.(string)))
}

if v, ok := m["key"]; ok {
buf.WriteString(fmt.Sprintf("%s-", v.(string)))
}

if v, ok := m["value"]; ok {
buf.WriteString(fmt.Sprintf("%s-", v.(string)))
}

return hashcode.String(buf.String())
}
189 changes: 189 additions & 0 deletions aws/resource_aws_backup_selection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package aws

import (
"fmt"
"testing"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/backup"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)

func TestAccAwsBackupSelection_basic(t *testing.T) {
rInt := acctest.RandInt()
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAwsBackupSelectionDestroy,
Steps: []resource.TestStep{
{
Config: testAccBackupSelectionConfigBasic(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsBackupSelectionExists("aws_backup_selection.test"),
),
},
},
})
}

func TestAccAwsBackupSelection_withTags(t *testing.T) {
rInt := acctest.RandInt()
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAwsBackupSelectionDestroy,
Steps: []resource.TestStep{
{
Config: testAccBackupSelectionConfigWithTags(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsBackupSelectionExists("aws_backup_selection.test"),
resource.TestCheckResourceAttr("aws_backup_selection.test", "tag.#", "2"),
),
},
},
})
}

func TestAccAwsBackupSelection_withResources(t *testing.T) {
rInt := acctest.RandInt()
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAwsBackupSelectionDestroy,
Steps: []resource.TestStep{
{
Config: testAccBackupSelectionConfigWithResources(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsBackupSelectionExists("aws_backup_selection.test"),
resource.TestCheckResourceAttr("aws_backup_selection.test", "resources.#", "2"),
),
},
},
})
}

func testAccCheckAwsBackupSelectionDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).backupconn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_backup_selection" {
continue
}

input := &backup.GetBackupSelectionInput{
BackupPlanId: aws.String(rs.Primary.Attributes["plan_id"]),
SelectionId: aws.String(rs.Primary.ID),
}

resp, err := conn.GetBackupSelection(input)

if err == nil {
if *resp.SelectionId == rs.Primary.ID {
return fmt.Errorf("Selection '%s' was not deleted properly", rs.Primary.ID)
}
}
}

return nil
}

func testAccCheckAwsBackupSelectionExists(name string) resource.TestCheckFunc {
slapula marked this conversation as resolved.
Show resolved Hide resolved
return func(s *terraform.State) error {
_, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("not found: %s, %v", name, s.RootModule().Resources)
}
return nil
}
}

func testAccBackupSelectionConfigBase(rInt int) string {
return fmt.Sprintf(`
data "aws_caller_identity" "current" {}

resource "aws_backup_vault" "test" {
name = "tf_acc_test_backup_vault_%d"
}

resource "aws_backup_plan" "test" {
name = "tf_acc_test_backup_plan_%d"

rule {
rule_name = "tf_acc_test_backup_rule_%d"
target_vault_name = "${aws_backup_vault.test.name}"
schedule = "cron(0 12 * * ? *)"
}
}
`, rInt, rInt, rInt)
}

func testAccBackupSelectionConfigBasic(rInt int) string {
return testAccBackupSelectionConfigBase(rInt) + fmt.Sprintf(`
resource "aws_backup_selection" "test" {
plan_id = "${aws_backup_plan.test.id}"

name = "tf_acc_test_backup_selection_%d"
iam_role = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/service-role/AWSBackupDefaultServiceRole"

tag {
type = "STRINGEQUALS"
key = "foo"
value = "bar"
}

resources = [
"arn:aws:ec2:us-east-1:${data.aws_caller_identity.current.account_id}:volume/"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The acceptance testing runs in us-west-2 by default:

https://github.com/terraform-providers/terraform-provider-aws/blob/26fea575f2100d68307e063a8b1c590d8de622d6/aws/provider_test.go#L181-L185

--- FAIL: TestAccAwsBackupSelection_withResources (13.48s)
    testing.go:538: Step 0 error: Error applying: 1 error occurred:
        	* aws_backup_selection.test: 1 error occurred:
        	* aws_backup_selection.test: error creating Backup Selection: InvalidParameterValueException: Invalid ARN: arn:aws:elasticfilesystem:us-east-1:--OMITTED--:file-system/. Expected region is us-west-2
        	status code: 400, request id: 8c64a59c-7b8c-46de-8e7e-6fa1a7879b3a

--- FAIL: TestAccAwsBackupSelection_disappears (13.53s)
    testing.go:538: Step 0 error: Error applying: 1 error occurred:
        	* aws_backup_selection.test: 1 error occurred:
        	* aws_backup_selection.test: error creating Backup Selection: InvalidParameterValueException: Invalid ARN: arn:aws:ec2:us-east-1:--OMITTED--:volume/. Expected region is us-west-2
        	status code: 400, request id: bb483272-0b15-4978-88ab-0a5cf2400302

--- FAIL: TestAccAwsBackupSelection_basic (13.60s)
    testing.go:538: Step 0 error: Error applying: 1 error occurred:
        	* aws_backup_selection.test: 1 error occurred:
        	* aws_backup_selection.test: error creating Backup Selection: InvalidParameterValueException: Invalid ARN: arn:aws:ec2:us-east-1:--OMITTED--:volume/. Expected region is us-west-2
        	status code: 400, request id: 6e4c8dda-6cc8-4d63-be62-b4b156e4c30b

--- FAIL: TestAccAwsBackupSelection_withTags (13.66s)
    testing.go:538: Step 0 error: Error applying: 1 error occurred:
        	* aws_backup_selection.test: 1 error occurred:
        	* aws_backup_selection.test: error creating Backup Selection: InvalidParameterValueException: Invalid ARN: arn:aws:ec2:us-east-1:--OMITTED--:volume/. Expected region is us-west-2
        	status code: 400, request id: a1229860-b1f7-4462-9b22-68d46cf62e31

We generally prefer using the aws_partition and aws_region data sources in this case to make the testing region and partition agnostic:

data "aws_partition" "current" {}

data "aws_region" "current" {}

// ...
"arn:${data.aws_partition.current.partition}:ec2:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:volume/"

]
}
`, rInt)
}

func testAccBackupSelectionConfigWithTags(rInt int) string {
return testAccBackupSelectionConfigBase(rInt) + fmt.Sprintf(`
resource "aws_backup_selection" "test" {
plan_id = "${aws_backup_plan.test.id}"

name = "tf_acc_test_backup_selection_%d"
iam_role = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/service-role/AWSBackupDefaultServiceRole"

tag {
type = "STRINGEQUALS"
key = "foo"
value = "bar"
}

tag {
type = "STRINGEQUALS"
key = "boo"
value = "far"
}

resources = [
"arn:aws:ec2:us-east-1:${data.aws_caller_identity.current.account_id}:volume/"
]
}
`, rInt)
}

func testAccBackupSelectionConfigWithResources(rInt int) string {
return testAccBackupSelectionConfigBase(rInt) + fmt.Sprintf(`
resource "aws_backup_selection" "test" {
plan_id = "${aws_backup_plan.test.id}"

name = "tf_acc_test_backup_selection_%d"
iam_role = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/service-role/AWSBackupDefaultServiceRole"

tag {
type = "STRINGEQUALS"
key = "foo"
value = "bar"
}

resources = [
"arn:aws:ec2:us-east-1:${data.aws_caller_identity.current.account_id}:volume/",
"arn:aws:elasticfilesystem:us-east-1:${data.aws_caller_identity.current.account_id}:file-system/"
]
}
`, rInt)
}
3 changes: 3 additions & 0 deletions website/aws.erb
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,9 @@
<li<%= sidebar_current("docs-aws-resource-backup-plan") %>>
<a href="/docs/providers/aws/r/backup_plan.html">aws_backup_plan</a>
</li>
<li<%= sidebar_current("docs-aws-resource-backup-selection") %>>
<a href="/docs/providers/aws/r/backup_selection.html">aws_backup_selection</a>
</li>
<li<%= sidebar_current("docs-aws-resource-backup-vault") %>>
<a href="/docs/providers/aws/r/backup_vault.html">aws_backup_vault</a>
</li>
Expand Down
Loading