diff --git a/aws/provider.go b/aws/provider.go index 1f01be702f0a..fe3f1c7ca963 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -324,6 +324,7 @@ func Provider() terraform.ResourceProvider { "aws_autoscaling_notification": resourceAwsAutoscalingNotification(), "aws_autoscaling_policy": resourceAwsAutoscalingPolicy(), "aws_autoscaling_schedule": resourceAwsAutoscalingSchedule(), + "aws_backup_plan": resourceAwsBackupPlan(), "aws_backup_vault": resourceAwsBackupVault(), "aws_budgets_budget": resourceAwsBudgetsBudget(), "aws_cloud9_environment_ec2": resourceAwsCloud9EnvironmentEc2(), diff --git a/aws/resource_aws_backup_plan.go b/aws/resource_aws_backup_plan.go new file mode 100644 index 000000000000..8c7f052b4d08 --- /dev/null +++ b/aws/resource_aws_backup_plan.go @@ -0,0 +1,360 @@ +package aws + +import ( + "bytes" + "fmt" + "log" + + "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 resourceAwsBackupPlan() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsBackupPlanCreate, + Read: resourceAwsBackupPlanRead, + Update: resourceAwsBackupPlanUpdate, + Delete: resourceAwsBackupPlanDelete, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "rule": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "rule_name": { + Type: schema.TypeString, + Required: true, + }, + "target_vault_name": { + Type: schema.TypeString, + Required: true, + }, + "schedule": { + Type: schema.TypeString, + Optional: true, + }, + "start_window": { + Type: schema.TypeInt, + Optional: true, + Default: 60, + }, + "completion_window": { + Type: schema.TypeInt, + Optional: true, + Default: 180, + }, + "lifecycle": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cold_storage_after": { + Type: schema.TypeInt, + Optional: true, + }, + "delete_after": { + Type: schema.TypeInt, + Optional: true, + }, + }, + }, + }, + "recovery_point_tags": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + }, + Set: resourceAwsPlanRuleHash, + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "version": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchema(), + }, + } +} + +func resourceAwsBackupPlanCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).backupconn + + plan := &backup.PlanInput{ + BackupPlanName: aws.String(d.Get("name").(string)), + } + + rules := expandBackupPlanRules(d.Get("rule").(*schema.Set).List()) + + plan.Rules = rules + + input := &backup.CreateBackupPlanInput{ + BackupPlan: plan, + } + + if v, ok := d.GetOk("tags"); ok { + input.BackupPlanTags = tagsFromMapGeneric(v.(map[string]interface{})) + } + + resp, err := conn.CreateBackupPlan(input) + if err != nil { + return fmt.Errorf("error creating Backup Plan: %s", err) + } + + d.SetId(*resp.BackupPlanId) + + return resourceAwsBackupPlanRead(d, meta) +} + +func resourceAwsBackupPlanRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).backupconn + + input := &backup.GetBackupPlanInput{ + BackupPlanId: aws.String(d.Id()), + } + + resp, err := conn.GetBackupPlan(input) + if isAWSErr(err, backup.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] Backup Plan (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading Backup Plan: %s", err) + } + + rule := &schema.Set{F: resourceAwsPlanRuleHash} + + for _, r := range resp.BackupPlan.Rules { + m := make(map[string]interface{}) + + m["completion_window"] = aws.Int64Value(r.CompletionWindowMinutes) + m["recovery_point_tags"] = aws.StringValueMap(r.RecoveryPointTags) + m["rule_name"] = aws.StringValue(r.RuleName) + m["schedule"] = aws.StringValue(r.ScheduleExpression) + m["start_window"] = aws.Int64Value(r.StartWindowMinutes) + m["target_vault_name"] = aws.StringValue(r.TargetBackupVaultName) + + if r.Lifecycle != nil { + l := make(map[string]interface{}) + l["delete_after"] = aws.Int64Value(r.Lifecycle.DeleteAfterDays) + l["cold_storage_after"] = aws.Int64Value(r.Lifecycle.MoveToColdStorageAfterDays) + m["lifecycle"] = []interface{}{l} + } + + rule.Add(m) + } + if err := d.Set("rule", rule); err != nil { + return fmt.Errorf("error setting rule: %s", err) + } + + tagsOutput, err := conn.ListTags(&backup.ListTagsInput{ + ResourceArn: resp.BackupPlanArn, + }) + if err != nil { + return fmt.Errorf("error listing tags AWS Backup plan %s: %s", d.Id(), err) + } + + if err := d.Set("tags", tagsToMapGeneric(tagsOutput.Tags)); err != nil { + return fmt.Errorf("error setting tags on AWS Backup plan %s: %s", d.Id(), err) + } + + d.Set("arn", resp.BackupPlanArn) + d.Set("version", resp.VersionId) + + return nil +} + +func resourceAwsBackupPlanUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).backupconn + + plan := &backup.PlanInput{ + BackupPlanName: aws.String(d.Get("name").(string)), + } + + rules := expandBackupPlanRules(d.Get("rule").(*schema.Set).List()) + + plan.Rules = rules + + input := &backup.UpdateBackupPlanInput{ + BackupPlanId: aws.String(d.Id()), + BackupPlan: plan, + } + + _, err := conn.UpdateBackupPlan(input) + if err != nil { + return fmt.Errorf("error updating Backup Plan: %s", err) + } + + if d.HasChange("tags") { + resourceArn := d.Get("arn").(string) + oraw, nraw := d.GetChange("tags") + create, remove := diffTagsGeneric(oraw.(map[string]interface{}), nraw.(map[string]interface{})) + + if len(remove) > 0 { + log.Printf("[DEBUG] Removing tags: %#v", remove) + keys := make([]*string, 0, len(remove)) + for k := range remove { + keys = append(keys, aws.String(k)) + } + + _, err := conn.UntagResource(&backup.UntagResourceInput{ + ResourceArn: aws.String(resourceArn), + TagKeyList: keys, + }) + if isAWSErr(err, backup.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] Backup Plan %s not found, removing from state", d.Id()) + d.SetId("") + return nil + } + if err != nil { + return fmt.Errorf("Error removing tags for (%s): %s", d.Id(), err) + } + } + if len(create) > 0 { + log.Printf("[DEBUG] Creating tags: %#v", create) + _, err := conn.TagResource(&backup.TagResourceInput{ + ResourceArn: aws.String(resourceArn), + Tags: create, + }) + if isAWSErr(err, backup.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] Backup Plan %s not found, removing from state", d.Id()) + d.SetId("") + return nil + } + if err != nil { + return fmt.Errorf("Error setting tags for (%s): %s", d.Id(), err) + } + } + } + + return resourceAwsBackupPlanRead(d, meta) +} + +func resourceAwsBackupPlanDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).backupconn + + input := &backup.DeleteBackupPlanInput{ + BackupPlanId: aws.String(d.Id()), + } + + _, err := conn.DeleteBackupPlan(input) + if isAWSErr(err, backup.ErrCodeResourceNotFoundException, "") { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting Backup Plan: %s", err) + } + + return nil +} + +func expandBackupPlanRules(l []interface{}) []*backup.RuleInput { + rules := []*backup.RuleInput{} + + for _, i := range l { + item := i.(map[string]interface{}) + rule := &backup.RuleInput{} + + if item["rule_name"] != "" { + rule.RuleName = aws.String(item["rule_name"].(string)) + } + if item["target_vault_name"] != "" { + rule.TargetBackupVaultName = aws.String(item["target_vault_name"].(string)) + } + if item["schedule"] != "" { + rule.ScheduleExpression = aws.String(item["schedule"].(string)) + } + if item["start_window"] != nil { + rule.StartWindowMinutes = aws.Int64(int64(item["start_window"].(int))) + } + if item["completion_window"] != nil { + rule.CompletionWindowMinutes = aws.Int64(int64(item["completion_window"].(int))) + } + + if item["recovery_point_tags"] != nil { + rule.RecoveryPointTags = tagsFromMapGeneric(item["recovery_point_tags"].(map[string]interface{})) + } + + var lifecycle map[string]interface{} + if i.(map[string]interface{})["lifecycle"] != nil { + lifecycleRaw := i.(map[string]interface{})["lifecycle"].([]interface{}) + if len(lifecycleRaw) == 1 { + lifecycle = lifecycleRaw[0].(map[string]interface{}) + lcValues := &backup.Lifecycle{} + if lifecycle["delete_after"] != nil { + lcValues.DeleteAfterDays = aws.Int64(int64(lifecycle["delete_after"].(int))) + } + if lifecycle["cold_storage_after"] != nil { + lcValues.MoveToColdStorageAfterDays = aws.Int64(int64(lifecycle["cold_storage_after"].(int))) + } + rule.Lifecycle = lcValues + } + + } + + rules = append(rules, rule) + } + + return rules +} + +func resourceAwsPlanRuleHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + + if v.(map[string]interface{})["lifecycle"] != nil { + lcRaw := v.(map[string]interface{})["lifecycle"].([]interface{}) + if len(lcRaw) == 1 { + l := lcRaw[0].(map[string]interface{}) + if w, ok := l["delete_after"]; ok { + buf.WriteString(fmt.Sprintf("%v-", w)) + } + + if w, ok := l["cold_storage_after"]; ok { + buf.WriteString(fmt.Sprintf("%v-", w)) + } + } + } + + if v, ok := m["completion_window"]; ok { + buf.WriteString(fmt.Sprintf("%d-", v.(interface{}))) + } + + if v, ok := m["recovery_point_tags"]; ok { + buf.WriteString(fmt.Sprintf("%v-", v)) + } + + if v, ok := m["rule_name"]; ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + + if v, ok := m["schedule"]; ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + + if v, ok := m["start_window"]; ok { + buf.WriteString(fmt.Sprintf("%d-", v.(interface{}))) + } + + if v, ok := m["target_vault_name"]; ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + + return hashcode.String(buf.String()) +} diff --git a/aws/resource_aws_backup_plan_test.go b/aws/resource_aws_backup_plan_test.go new file mode 100644 index 000000000000..4f99b39affc3 --- /dev/null +++ b/aws/resource_aws_backup_plan_test.go @@ -0,0 +1,363 @@ +package aws + +import ( + "fmt" + "regexp" + "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 TestAccAwsBackupPlan_basic(t *testing.T) { + var plan backup.GetBackupPlanOutput + rInt := acctest.RandInt() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsBackupPlanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBackupPlanConfig(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBackupPlanExists("aws_backup_plan.test", &plan), + testAccMatchResourceAttrRegionalARN("aws_backup_plan.test", "arn", "backup", regexp.MustCompile(`backup-plan:.+`)), + resource.TestCheckResourceAttrSet("aws_backup_plan.test", "version"), + ), + }, + }, + }) +} + +func TestAccAwsBackupPlan_withTags(t *testing.T) { + var plan backup.GetBackupPlanOutput + rInt := acctest.RandInt() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsBackupPlanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBackupPlanWithTag(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBackupPlanExists("aws_backup_plan.test", &plan), + resource.TestCheckResourceAttr("aws_backup_plan.test", "tags.%", "1"), + resource.TestCheckResourceAttr("aws_backup_plan.test", "tags.env", "test"), + ), + }, + { + Config: testAccBackupPlanWithTags(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBackupPlanExists("aws_backup_plan.test", &plan), + resource.TestCheckResourceAttr("aws_backup_plan.test", "tags.%", "2"), + resource.TestCheckResourceAttr("aws_backup_plan.test", "tags.env", "test"), + resource.TestCheckResourceAttr("aws_backup_plan.test", "tags.app", "widget"), + ), + }, + { + Config: testAccBackupPlanWithTag(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBackupPlanExists("aws_backup_plan.test", &plan), + resource.TestCheckResourceAttr("aws_backup_plan.test", "tags.%", "1"), + resource.TestCheckResourceAttr("aws_backup_plan.test", "tags.env", "test"), + ), + }, + }, + }) +} + +func TestAccAwsBackupPlan_withRules(t *testing.T) { + var plan backup.GetBackupPlanOutput + rInt := acctest.RandInt() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsBackupPlanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBackupPlanWithRules(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBackupPlanExists("aws_backup_plan.test", &plan), + resource.TestCheckResourceAttr("aws_backup_plan.test", "rule.#", "2"), + ), + }, + }, + }) +} + +func TestAccAwsBackupPlan_withRuleRemove(t *testing.T) { + var plan backup.GetBackupPlanOutput + rInt := acctest.RandInt() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsBackupPlanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBackupPlanWithRules(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBackupPlanExists("aws_backup_plan.test", &plan), + resource.TestCheckResourceAttr("aws_backup_plan.test", "rule.#", "2"), + ), + }, + { + Config: testAccBackupPlanConfig(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBackupPlanExists("aws_backup_plan.test", &plan), + resource.TestCheckResourceAttr("aws_backup_plan.test", "rule.#", "1"), + ), + }, + }, + }) +} + +func TestAccAwsBackupPlan_withRuleAdd(t *testing.T) { + var plan backup.GetBackupPlanOutput + rInt := acctest.RandInt() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsBackupPlanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBackupPlanConfig(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBackupPlanExists("aws_backup_plan.test", &plan), + resource.TestCheckResourceAttr("aws_backup_plan.test", "rule.#", "1"), + ), + }, + { + Config: testAccBackupPlanWithRules(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBackupPlanExists("aws_backup_plan.test", &plan), + resource.TestCheckResourceAttr("aws_backup_plan.test", "rule.#", "2"), + ), + }, + }, + }) +} + +func TestAccAwsBackupPlan_withLifecycle(t *testing.T) { + var plan backup.GetBackupPlanOutput + rStr := "lifecycle_policy" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsBackupPlanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBackupPlanWithLifecycle(rStr), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBackupPlanExists("aws_backup_plan.test", &plan), + resource.TestCheckResourceAttr("aws_backup_plan.test", "rule.1028372010.lifecycle.#", "1"), + ), + }, + }, + }) +} + +func TestAccAwsBackupPlan_disappears(t *testing.T) { + var plan backup.GetBackupPlanOutput + rInt := acctest.RandInt() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsBackupPlanDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBackupPlanConfig(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBackupPlanExists("aws_backup_plan.test", &plan), + testAccCheckAwsBackupPlanDisappears(&plan), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckAwsBackupPlanDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).backupconn + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_backup_plan" { + continue + } + + input := &backup.GetBackupPlanInput{ + BackupPlanId: aws.String(rs.Primary.ID), + } + + resp, err := conn.GetBackupPlan(input) + + if err == nil { + if *resp.BackupPlanId == rs.Primary.ID { + return fmt.Errorf("Plane '%s' was not deleted properly", rs.Primary.ID) + } + } + } + + return nil +} + +func testAccCheckAwsBackupPlanDisappears(backupPlan *backup.GetBackupPlanOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).backupconn + + input := &backup.DeleteBackupPlanInput{ + BackupPlanId: backupPlan.BackupPlanId, + } + + _, err := conn.DeleteBackupPlan(input) + + return err + } +} + +func testAccCheckAwsBackupPlanExists(name string, plan *backup.GetBackupPlanOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("Resource ID is not set") + } + + conn := testAccProvider.Meta().(*AWSClient).backupconn + + input := &backup.GetBackupPlanInput{ + BackupPlanId: aws.String(rs.Primary.ID), + } + + output, err := conn.GetBackupPlan(input) + + if err != nil { + return err + } + + *plan = *output + + return nil + } +} + +func testAccBackupPlanConfig(randInt int) string { + return fmt.Sprintf(` +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 * * ? *)" + } +} +`, randInt, randInt, randInt) +} + +func testAccBackupPlanWithTag(randInt int) string { + return fmt.Sprintf(` +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 * * ? *)" + } + + tags { + env = "test" + } +} +`, randInt, randInt, randInt) +} + +func testAccBackupPlanWithTags(randInt int) string { + return fmt.Sprintf(` +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 * * ? *)" + } + + tags { + env = "test" + app = "widget" + } +} +`, randInt, randInt, randInt) +} + +func testAccBackupPlanWithLifecycle(stringID string) string { + return fmt.Sprintf(` +resource "aws_backup_vault" "test" { + name = "tf_acc_test_backup_vault_%s" +} + +resource "aws_backup_plan" "test" { + name = "tf_acc_test_backup_plan_%s" + + rule { + rule_name = "tf_acc_test_backup_rule_%s" + target_vault_name = "${aws_backup_vault.test.name}" + schedule = "cron(0 12 * * ? *)" + lifecycle { + cold_storage_after = 30 + delete_after = 160 + } + } +} +`, stringID, stringID, stringID) +} + +func testAccBackupPlanWithRules(randInt int) string { + return fmt.Sprintf(` +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 * * ? *)" + } + + rule { + rule_name = "tf_acc_test_backup_rule_%d_2" + target_vault_name = "${aws_backup_vault.test.name}" + schedule = "cron(0 6 * * ? *)" + } +} +`, randInt, randInt, randInt, randInt) +} diff --git a/website/aws.erb b/website/aws.erb index e9c9687fcb1d..4bb7b5e472fd 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -570,6 +570,9 @@ > Backup Resources