diff --git a/aws/provider.go b/aws/provider.go index 05757963745..d3809690a9a 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -325,6 +325,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(), diff --git a/aws/resource_aws_backup_selection.go b/aws/resource_aws_backup_selection.go new file mode 100644 index 00000000000..6db13f2e524 --- /dev/null +++ b/aws/resource_aws_backup_selection.go @@ -0,0 +1,180 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/backup" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" +) + +func resourceAwsBackupSelection() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsBackupSelectionCreate, + Read: resourceAwsBackupSelectionRead, + Delete: resourceAwsBackupSelectionDelete, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 50), + validation.StringMatch(regexp.MustCompile(`^[a-zA-Z0-9\-\_\.]+$`), "must contain only alphanumeric, hyphen, underscore, and period characters"), + ), + }, + "plan_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "iam_role_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + "selection_tag": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + backup.ConditionTypeStringequals, + }, false), + }, + "key": { + Type: schema.TypeString, + Required: true, + }, + "value": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "resources": { + Type: schema.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{ + IamRoleArn: aws.String(d.Get("iam_role_arn").(string)), + ListOfTags: expandBackupConditionTags(d.Get("selection_tag").(*schema.Set).List()), + Resources: expandStringSet(d.Get("resources").(*schema.Set)), + SelectionName: aws.String(d.Get("name").(string)), + } + + 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 isAWSErr(err, backup.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] Backup Selection (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + 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_arn", resp.BackupSelection.IamRoleArn) + + if resp.BackupSelection.ListOfTags != nil { + tags := make([]map[string]interface{}, 0) + + for _, r := range resp.BackupSelection.ListOfTags { + m := make(map[string]interface{}) + + m["type"] = aws.StringValue(r.ConditionType) + m["key"] = aws.StringValue(r.ConditionKey) + m["value"] = aws.StringValue(r.ConditionValue) + + tags = append(tags, m) + } + + if err := d.Set("selection_tag", tags); err != nil { + return fmt.Errorf("error setting selection tag: %s", err) + } + } + if resp.BackupSelection.Resources != nil { + if err := d.Set("resources", aws.StringValueSlice(resp.BackupSelection.Resources)); err != nil { + return fmt.Errorf("error setting resources: %s", err) + } + } + + 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 expandBackupConditionTags(tagList []interface{}) []*backup.Condition { + conditions := []*backup.Condition{} + + for _, i := range tagList { + 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 +} diff --git a/aws/resource_aws_backup_selection_test.go b/aws/resource_aws_backup_selection_test.go new file mode 100644 index 00000000000..655209b3b7d --- /dev/null +++ b/aws/resource_aws_backup_selection_test.go @@ -0,0 +1,247 @@ +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) { + var selection1 backup.GetBackupSelectionOutput + 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", &selection1), + ), + }, + }, + }) +} + +func TestAccAwsBackupSelection_disappears(t *testing.T) { + var selection1 backup.GetBackupSelectionOutput + 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", &selection1), + testAccCheckAwsBackupSelectionDisappears(&selection1), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAwsBackupSelection_withTags(t *testing.T) { + var selection1 backup.GetBackupSelectionOutput + 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", &selection1), + resource.TestCheckResourceAttr("aws_backup_selection.test", "selection_tag.#", "2"), + ), + }, + }, + }) +} + +func TestAccAwsBackupSelection_withResources(t *testing.T) { + var selection1 backup.GetBackupSelectionOutput + 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", &selection1), + 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, selection *backup.GetBackupSelectionOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("not found: %s, %v", name, s.RootModule().Resources) + } + + conn := testAccProvider.Meta().(*AWSClient).backupconn + + input := &backup.GetBackupSelectionInput{ + BackupPlanId: aws.String(rs.Primary.Attributes["plan_id"]), + SelectionId: aws.String(rs.Primary.ID), + } + + output, err := conn.GetBackupSelection(input) + + if err != nil { + return err + } + + *selection = *output + + return nil + } +} + +func testAccCheckAwsBackupSelectionDisappears(selection *backup.GetBackupSelectionOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).backupconn + + input := &backup.DeleteBackupSelectionInput{ + BackupPlanId: selection.BackupPlanId, + SelectionId: selection.SelectionId, + } + + _, err := conn.DeleteBackupSelection(input) + + return err + } +} + +func testAccBackupSelectionConfigBase(rInt int) string { + return fmt.Sprintf(` +data "aws_caller_identity" "current" {} + +data "aws_partition" "current" {} + +data "aws_region" "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 = "arn:${data.aws_partition.current.partition}:iam::${data.aws_caller_identity.current.account_id}:role/service-role/AWSBackupDefaultServiceRole" + + selection_tag { + type = "STRINGEQUALS" + key = "foo" + value = "bar" + } + + resources = [ + "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 = "arn:${data.aws_partition.current.partition}:iam::${data.aws_caller_identity.current.account_id}:role/service-role/AWSBackupDefaultServiceRole" + + selection_tag { + type = "STRINGEQUALS" + key = "foo" + value = "bar" + } + + selection_tag { + type = "STRINGEQUALS" + key = "boo" + value = "far" + } + + resources = [ + "arn:${data.aws_partition.current.partition}:ec2:${data.aws_region.current.name}:${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 = "arn:${data.aws_partition.current.partition}:iam::${data.aws_caller_identity.current.account_id}:role/service-role/AWSBackupDefaultServiceRole" + + selection_tag { + type = "STRINGEQUALS" + key = "foo" + value = "bar" + } + + resources = [ + "arn:${data.aws_partition.current.partition}:elasticfilesystem:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:file-system/", + "arn:${data.aws_partition.current.partition}:ec2:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:volume/" + ] +} +`, rInt) +} diff --git a/website/aws.erb b/website/aws.erb index bd0e5834330..a3a010de5a5 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -586,6 +586,9 @@ > aws_backup_plan + > + aws_backup_selection + > aws_backup_vault diff --git a/website/docs/r/backup_selection.html.markdown b/website/docs/r/backup_selection.html.markdown new file mode 100644 index 00000000000..fbc07c0920b --- /dev/null +++ b/website/docs/r/backup_selection.html.markdown @@ -0,0 +1,54 @@ +--- +layout: "aws" +page_title: "AWS: aws_backup_selection" +sidebar_current: "docs-aws-resource-backup-selection" +description: |- + Manages selection conditions for AWS Backup plan resources. +--- + +# aws_backup_selection + +Manages selection conditions for AWS Backup plan resources. + +## Example Usage + +```hcl +resource "aws_backup_selection" "example" { + plan_id = "${aws_backup_plan.example.id}" + + name = "tf_example_backup_selection" + iam_role = "arn:aws:iam::123456789012:role/service-role/AWSBackupDefaultServiceRole" + + tag { + type = "STRINGEQUALS" + key = "foo" + value = "bar" + } + + resources = [ + "arn:aws:ec2:us-east-1:123456789012:volume/" + ] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The display name of a resource selection document. +* `plan_id` - (Required) The backup plan ID to be associated with the selection of resources. +* `iam_role` - (Required) The ARN of the IAM role that AWS Backup uses to authenticate when restoring the target resource. +* `selection_tag` - (Optional) Tag-based conditions used to specify a set of resources to assign to a backup plan. +* `resources` - (Optional) An array of strings that either contain Amazon Resource Names (ARNs) or match patterns of resources to assign to a backup plan.. + +Tag conditions (`selection_tag`) support the following: + +* `type` - (Required) An operation, such as `StringEquals`, that is applied to a key-value pair used to filter resources in a selection. +* `key` - (Required) The key in a key-value pair. +* `value` - (Required) The value in a key-value pair. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - Backup Selection identifier