diff --git a/aws/provider.go b/aws/provider.go index 34e5c292584..d5101b1c8ff 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -433,6 +433,7 @@ func Provider() *schema.Provider { "aws_backup_plan": resourceAwsBackupPlan(), "aws_backup_selection": resourceAwsBackupSelection(), "aws_backup_vault": resourceAwsBackupVault(), + "aws_backup_vault_notifications": resourceAwsBackupVaultNotifications(), "aws_budgets_budget": resourceAwsBudgetsBudget(), "aws_cloud9_environment_ec2": resourceAwsCloud9EnvironmentEc2(), "aws_cloudformation_stack": resourceAwsCloudFormationStack(), diff --git a/aws/resource_aws_backup_vault_notifications.go b/aws/resource_aws_backup_vault_notifications.go new file mode 100644 index 00000000000..6efed482944 --- /dev/null +++ b/aws/resource_aws_backup_vault_notifications.go @@ -0,0 +1,115 @@ +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-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceAwsBackupVaultNotifications() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsBackupVaultNotificationsCreate, + Read: resourceAwsBackupVaultNotificationsRead, + Delete: resourceAwsBackupVaultNotificationsDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "backup_vault_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`^[a-zA-Z0-9\-\_\.]{1,50}$`), "must consist of lowercase letters, numbers, and hyphens."), + }, + "sns_topic_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + "backup_vault_events": { + Type: schema.TypeSet, + Required: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice(backup.VaultEvent_Values(), false), + }, + }, + "backup_vault_arn": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsBackupVaultNotificationsCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).backupconn + + input := &backup.PutBackupVaultNotificationsInput{ + BackupVaultName: aws.String(d.Get("backup_vault_name").(string)), + SNSTopicArn: aws.String(d.Get("sns_topic_arn").(string)), + BackupVaultEvents: expandStringSet(d.Get("backup_vault_events").(*schema.Set)), + } + + _, err := conn.PutBackupVaultNotifications(input) + if err != nil { + return fmt.Errorf("error creating Backup Vault Notifications (%s): %w", d.Id(), err) + } + + d.SetId(d.Get("backup_vault_name").(string)) + + return resourceAwsBackupVaultNotificationsRead(d, meta) +} + +func resourceAwsBackupVaultNotificationsRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).backupconn + + input := &backup.GetBackupVaultNotificationsInput{ + BackupVaultName: aws.String(d.Id()), + } + + resp, err := conn.GetBackupVaultNotifications(input) + if isAWSErr(err, backup.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] Backup Vault Notifcations %s not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading Backup Vault Notifications (%s): %w", d.Id(), err) + } + d.Set("backup_vault_name", resp.BackupVaultName) + d.Set("sns_topic_arn", resp.SNSTopicArn) + d.Set("backup_vault_arn", resp.BackupVaultArn) + if err := d.Set("backup_vault_events", flattenStringSet(resp.BackupVaultEvents)); err != nil { + return fmt.Errorf("error setting backup_vault_events: %w", err) + } + + return nil +} + +func resourceAwsBackupVaultNotificationsDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).backupconn + + input := &backup.DeleteBackupVaultNotificationsInput{ + BackupVaultName: aws.String(d.Id()), + } + + _, err := conn.DeleteBackupVaultNotifications(input) + if err != nil { + if isAWSErr(err, backup.ErrCodeResourceNotFoundException, "") { + return nil + } + return fmt.Errorf("error deleting Backup Vault Notifications (%s): %w", d.Id(), err) + } + + return nil +} diff --git a/aws/resource_aws_backup_vault_notifications_test.go b/aws/resource_aws_backup_vault_notifications_test.go new file mode 100644 index 00000000000..0f74ae226e3 --- /dev/null +++ b/aws/resource_aws_backup_vault_notifications_test.go @@ -0,0 +1,210 @@ +package aws + +import ( + "fmt" + "log" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/backup" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func init() { + resource.AddTestSweepers("aws_backup_vault_notifications", &resource.Sweeper{ + Name: "aws_backup_vault_notifications", + F: testSweepBackupVaultNotifications, + }) +} + +func testSweepBackupVaultNotifications(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("Error getting client: %w", err) + } + conn := client.(*AWSClient).backupconn + var sweeperErrs *multierror.Error + + input := &backup.ListBackupVaultsInput{} + + for { + output, err := conn.ListBackupVaults(input) + if err != nil { + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping Backup Vault Notifications sweep for %s: %s", region, err) + return nil + } + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error retrieving Backup Vault Notifications: %w", err)) + return sweeperErrs.ErrorOrNil() + } + + if len(output.BackupVaultList) == 0 { + log.Print("[DEBUG] No Backup Vault Notifications to sweep") + return nil + } + + for _, rule := range output.BackupVaultList { + name := aws.StringValue(rule.BackupVaultName) + + log.Printf("[INFO] Deleting Backup Vault Notifications %s", name) + _, err := conn.DeleteBackupVaultNotifications(&backup.DeleteBackupVaultNotificationsInput{ + BackupVaultName: aws.String(name), + }) + if err != nil { + sweeperErr := fmt.Errorf("error deleting Backup Vault Notifications %s: %w", name, err) + log.Printf("[ERROR] %s", sweeperErr) + sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) + continue + } + } + + if output.NextToken == nil { + break + } + input.NextToken = output.NextToken + } + + return sweeperErrs.ErrorOrNil() +} + +func TestAccAwsBackupVaultNotification_basic(t *testing.T) { + var vault backup.GetBackupVaultNotificationsOutput + + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_backup_vault_notifications.test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsBackupVaultNotificationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBackupVaultNotificationConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBackupVaultNotificationExists(resourceName, &vault), + resource.TestCheckResourceAttr(resourceName, "backup_vault_events.#", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsBackupVaultNotification_disappears(t *testing.T) { + var vault backup.GetBackupVaultNotificationsOutput + + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_backup_vault_notifications.test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSBackup(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsBackupVaultNotificationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBackupVaultNotificationConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsBackupVaultNotificationExists(resourceName, &vault), + testAccCheckResourceDisappears(testAccProvider, resourceAwsBackupVaultNotifications(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckAwsBackupVaultNotificationDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).backupconn + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_backup_vault_notifications" { + continue + } + + input := &backup.GetBackupVaultNotificationsInput{ + BackupVaultName: aws.String(rs.Primary.ID), + } + + resp, err := conn.GetBackupVaultNotifications(input) + + if err == nil { + if aws.StringValue(resp.BackupVaultName) == rs.Primary.ID { + return fmt.Errorf("Backup Plan notifications '%s' was not deleted properly", rs.Primary.ID) + } + } + } + + return nil +} + +func testAccCheckAwsBackupVaultNotificationExists(name string, vault *backup.GetBackupVaultNotificationsOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + conn := testAccProvider.Meta().(*AWSClient).backupconn + params := &backup.GetBackupVaultNotificationsInput{ + BackupVaultName: aws.String(rs.Primary.ID), + } + resp, err := conn.GetBackupVaultNotifications(params) + if err != nil { + return err + } + + *vault = *resp + + return nil + } +} + +func testAccBackupVaultNotificationConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_backup_vault" "test" { + name = %[1]q +} + +resource "aws_sns_topic" "test" { + name = %[1]q +} + +data "aws_iam_policy_document" "test" { + policy_id = "__default_policy_ID" + + statement { + actions = [ + "SNS:Publish", + ] + + effect = "Allow" + + principals { + type = "Service" + identifiers = ["backup.amazonaws.com"] + } + + resources = [ + "${aws_sns_topic.test.arn}", + ] + + sid = "__default_statement_ID" + } +} + +resource "aws_sns_topic_policy" "test" { + arn = aws_sns_topic.test.arn + policy = data.aws_iam_policy_document.test.json +} + +resource "aws_backup_vault_notifications" "test" { + backup_vault_name = aws_backup_vault.test.name + sns_topic_arn = aws_sns_topic.test.arn + backup_vault_events = ["BACKUP_JOB_STARTED", "RESTORE_JOB_COMPLETED"] +} +`, rName) +} diff --git a/aws/resource_aws_sns_topic_test.go b/aws/resource_aws_sns_topic_test.go index 7c08884c97c..935515d9021 100644 --- a/aws/resource_aws_sns_topic_test.go +++ b/aws/resource_aws_sns_topic_test.go @@ -21,6 +21,7 @@ func init() { F: testSweepSnsTopics, Dependencies: []string{ "aws_autoscaling_group", + "aws_backup_vault_notifications", "aws_budgets_budget", "aws_config_delivery_channel", "aws_dax_cluster", diff --git a/website/docs/r/backup_vault_notifications.html.markdown b/website/docs/r/backup_vault_notifications.html.markdown new file mode 100644 index 00000000000..4db423c8d7c --- /dev/null +++ b/website/docs/r/backup_vault_notifications.html.markdown @@ -0,0 +1,76 @@ +--- +subcategory: "Backup" +layout: "aws" +page_title: "AWS: aws_backup_vault_notifications" +description: |- + Provides an AWS Backup vault notifications resource. +--- + +# Resource: aws_backup_vault_notifications + +Provides an AWS Backup vault notifications resource. + +## Example Usage + +```hcl +resource "aws_sns_topic" "test" { + name = "backup-vault-events" +} + +data "aws_iam_policy_document" "test" { + policy_id = "__default_policy_ID" + + statement { + actions = [ + "SNS:Publish", + ] + + effect = "Allow" + + principals { + type = "Service" + identifiers = ["backup.amazonaws.com"] + } + + resources = [ + aws_sns_topic.test.arn, + ] + + sid = "__default_statement_ID" + } +} + +resource "aws_sns_topic_policy" "test" { + arn = aws_sns_topic.test.arn + policy = data.aws_iam_policy_document.test.json +} + +resource "aws_backup_vault_notifications" "test" { + backup_vault_name = "example_backup_vault" + sns_topic_arn = sns_topic_arn.test.arn + backup_vault_events = ["BACKUP_JOB_STARTED", "RESTORE_JOB_COMPLETED"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `backup_vault_name` - (Required) Name of the backup vault to add notifications for. +* `sns_topic_arn` - (Required) The Amazon Resource Name (ARN) that specifies the topic for a backup vault’s events +* `backup_vault_events` - (Required) An array of events that indicate the status of jobs to back up resources to the backup vault. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The name of the vault. +* `backup_vault_arn` - The ARN of the vault. + +## Import + +Backup vault notifications can be imported using the `name`, e.g. + +``` +$ terraform import aws_backup_vault_notifications.test TestVault +```