diff --git a/aws/provider.go b/aws/provider.go index 7ae1da3af027..e135d2749ae2 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -640,6 +640,8 @@ func Provider() terraform.ResourceProvider { "aws_redshift_parameter_group": resourceAwsRedshiftParameterGroup(), "aws_redshift_subnet_group": resourceAwsRedshiftSubnetGroup(), "aws_redshift_snapshot_copy_grant": resourceAwsRedshiftSnapshotCopyGrant(), + "aws_redshift_snapshot_schedule": resourceAwsRedshiftSnapshotSchedule(), + "aws_redshift_snapshot_schedule_association": resourceAwsRedshiftSnapshotScheduleAssociation(), "aws_redshift_event_subscription": resourceAwsRedshiftEventSubscription(), "aws_resourcegroups_group": resourceAwsResourceGroupsGroup(), "aws_route53_delegation_set": resourceAwsRoute53DelegationSet(), diff --git a/aws/resource_aws_redshift_snapshot_schedule.go b/aws/resource_aws_redshift_snapshot_schedule.go new file mode 100644 index 000000000000..4d131bcc1614 --- /dev/null +++ b/aws/resource_aws_redshift_snapshot_schedule.go @@ -0,0 +1,234 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/redshift" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsRedshiftSnapshotSchedule() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsRedshiftSnapshotScheduleCreate, + Read: resourceAwsRedshiftSnapshotScheduleRead, + Update: resourceAwsRedshiftSnapshotScheduleUpdate, + Delete: resourceAwsRedshiftSnapshotScheduleDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "identifier": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"identifier_prefix"}, + }, + "identifier_prefix": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "definitions": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "force_destroy": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "tags": tagsSchema(), + }, + } + +} + +func resourceAwsRedshiftSnapshotScheduleCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).redshiftconn + tags := tagsFromMapRedshift(d.Get("tags").(map[string]interface{})) + var identifier string + if v, ok := d.GetOk("identifier"); ok { + identifier = v.(string) + } else { + if v, ok := d.GetOk("identifier_prefix"); ok { + identifier = resource.PrefixedUniqueId(v.(string)) + } else { + identifier = resource.UniqueId() + } + } + createOpts := &redshift.CreateSnapshotScheduleInput{ + ScheduleIdentifier: aws.String(identifier), + ScheduleDefinitions: expandStringSet(d.Get("definitions").(*schema.Set)), + Tags: tags, + } + if attr, ok := d.GetOk("description"); ok { + createOpts.ScheduleDescription = aws.String(attr.(string)) + } + + resp, err := conn.CreateSnapshotSchedule(createOpts) + if err != nil { + return fmt.Errorf("Error creating Redshift Snapshot Schedule: %s", err) + } + + d.SetId(aws.StringValue(resp.ScheduleIdentifier)) + + return resourceAwsRedshiftSnapshotScheduleRead(d, meta) +} + +func resourceAwsRedshiftSnapshotScheduleRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).redshiftconn + + descOpts := &redshift.DescribeSnapshotSchedulesInput{ + ScheduleIdentifier: aws.String(d.Id()), + } + + resp, err := conn.DescribeSnapshotSchedules(descOpts) + if err != nil { + return fmt.Errorf("Error describing Redshift Cluster Snapshot Schedule %s: %s", d.Id(), err) + } + + if resp.SnapshotSchedules == nil || len(resp.SnapshotSchedules) != 1 { + log.Printf("[WARN] Unable to find Redshift Cluster Snapshot Schedule (%s)", d.Id()) + d.SetId("") + return nil + } + snapshotSchedule := resp.SnapshotSchedules[0] + + d.Set("identifier", snapshotSchedule.ScheduleIdentifier) + d.Set("description", snapshotSchedule.ScheduleDescription) + if err := d.Set("definitions", flattenStringList(snapshotSchedule.ScheduleDefinitions)); err != nil { + return fmt.Errorf("Error setting definitions: %s", err) + } + d.Set("tags", tagsToMapRedshift(snapshotSchedule.Tags)) + + arn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Service: "redshift", + Region: meta.(*AWSClient).region, + AccountID: meta.(*AWSClient).accountid, + Resource: fmt.Sprintf("snapshotschedule:%s", d.Id()), + }.String() + + d.Set("arn", arn) + + return nil +} + +func resourceAwsRedshiftSnapshotScheduleUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).redshiftconn + d.Partial(true) + + if tagErr := setTagsRedshift(conn, d); tagErr != nil { + return tagErr + } else { + d.SetPartial("tags") + } + + if d.HasChange("definitions") { + modifyOpts := &redshift.ModifySnapshotScheduleInput{ + ScheduleIdentifier: aws.String(d.Id()), + ScheduleDefinitions: expandStringList(d.Get("definitions").(*schema.Set).List()), + } + _, err := conn.ModifySnapshotSchedule(modifyOpts) + if isAWSErr(err, redshift.ErrCodeSnapshotScheduleNotFoundFault, "") { + log.Printf("[WARN] Redshift Snapshot Schedule (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + if err != nil { + return fmt.Errorf("Error modifying Redshift Snapshot Schedule %s: %s", d.Id(), err) + } + d.SetPartial("definitions") + } + + return resourceAwsRedshiftSnapshotScheduleRead(d, meta) +} + +func resourceAwsRedshiftSnapshotScheduleDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).redshiftconn + + if d.Get("force_destroy").(bool) { + if err := resourceAwsRedshiftSnapshotScheduleDeleteAllAssociatedClusters(conn, d.Id()); err != nil { + return err + } + } + + _, err := conn.DeleteSnapshotSchedule(&redshift.DeleteSnapshotScheduleInput{ + ScheduleIdentifier: aws.String(d.Id()), + }) + if isAWSErr(err, redshift.ErrCodeSnapshotScheduleNotFoundFault, "") { + return nil + } + if err != nil { + return fmt.Errorf("Error deleting Redshift Snapshot Schedule %s: %s", d.Id(), err) + } + + return nil +} + +func resourceAwsRedshiftSnapshotScheduleDeleteAllAssociatedClusters(conn *redshift.Redshift, scheduleIdentifier string) error { + + resp, err := conn.DescribeSnapshotSchedules(&redshift.DescribeSnapshotSchedulesInput{ + ScheduleIdentifier: aws.String(scheduleIdentifier), + }) + if isAWSErr(err, redshift.ErrCodeSnapshotScheduleNotFoundFault, "") { + return nil + } + if err != nil { + return fmt.Errorf("Error describing Redshift Cluster Snapshot Schedule %s: %s", scheduleIdentifier, err) + } + if resp.SnapshotSchedules == nil || len(resp.SnapshotSchedules) != 1 { + log.Printf("[WARN] Unable to find Redshift Cluster Snapshot Schedule (%s)", scheduleIdentifier) + return nil + } + + snapshotSchedule := resp.SnapshotSchedules[0] + + for _, associatedCluster := range snapshotSchedule.AssociatedClusters { + _, err = conn.ModifyClusterSnapshotSchedule(&redshift.ModifyClusterSnapshotScheduleInput{ + ClusterIdentifier: associatedCluster.ClusterIdentifier, + ScheduleIdentifier: aws.String(scheduleIdentifier), + DisassociateSchedule: aws.Bool(true), + }) + + if isAWSErr(err, redshift.ErrCodeClusterNotFoundFault, "") { + log.Printf("[WARN] Redshift Snapshot Cluster (%s) not found, removing from state", aws.StringValue(associatedCluster.ClusterIdentifier)) + continue + } + if isAWSErr(err, redshift.ErrCodeSnapshotScheduleNotFoundFault, "") { + log.Printf("[WARN] Redshift Snapshot Schedule (%s) not found, removing from state", scheduleIdentifier) + continue + } + if err != nil { + return fmt.Errorf("Error disassociate Redshift Cluster (%s) and Snapshot Schedule (%s) Association: %s", aws.StringValue(associatedCluster.ClusterIdentifier), scheduleIdentifier, err) + } + } + + for _, associatedCluster := range snapshotSchedule.AssociatedClusters { + if err := waitForRedshiftSnapshotScheduleAssociationDestroy(conn, 75*time.Minute, aws.StringValue(associatedCluster.ClusterIdentifier), scheduleIdentifier); err != nil { + return err + } + } + + return nil +} diff --git a/aws/resource_aws_redshift_snapshot_schedule_association.go b/aws/resource_aws_redshift_snapshot_schedule_association.go new file mode 100644 index 000000000000..3188a7a40a14 --- /dev/null +++ b/aws/resource_aws_redshift_snapshot_schedule_association.go @@ -0,0 +1,238 @@ +package aws + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/redshift" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsRedshiftSnapshotScheduleAssociation() *schema.Resource { + + return &schema.Resource{ + Create: resourceAwsRedshiftSnapshotScheduleAssociationCreate, + Read: resourceAwsRedshiftSnapshotScheduleAssociationRead, + Delete: resourceAwsRedshiftSnapshotScheduleAssociationDelete, + Importer: &schema.ResourceImporter{ + State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + clusterIdentifier, scheduleIdentifier, err := resourceAwsRedshiftSnapshotScheduleAssociationParseId(d.Id()) + if err != nil { + return nil, fmt.Errorf("Error parse Redshift Cluster Snapshot Schedule Association ID %s: %s", d.Id(), err) + } + + d.Set("cluster_identifier", clusterIdentifier) + d.Set("schedule_identifier", scheduleIdentifier) + d.SetId(fmt.Sprintf("%s/%s", clusterIdentifier, scheduleIdentifier)) + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "cluster_identifier": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "schedule_identifier": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsRedshiftSnapshotScheduleAssociationCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).redshiftconn + clusterIdentifier := d.Get("cluster_identifier").(string) + scheduleIdentifier := d.Get("schedule_identifier").(string) + + _, err := conn.ModifyClusterSnapshotSchedule(&redshift.ModifyClusterSnapshotScheduleInput{ + ClusterIdentifier: aws.String(clusterIdentifier), + ScheduleIdentifier: aws.String(scheduleIdentifier), + DisassociateSchedule: aws.Bool(false), + }) + + if err != nil { + return fmt.Errorf("Error associating Redshift Cluster (%s) and Snapshot Schedule (%s): %s", clusterIdentifier, scheduleIdentifier, err) + } + + if err := waitForRedshiftSnapshotScheduleAssociationActive(conn, 75*time.Minute, clusterIdentifier, scheduleIdentifier); err != nil { + return err + } + + d.SetId(fmt.Sprintf("%s/%s", clusterIdentifier, scheduleIdentifier)) + + return resourceAwsRedshiftSnapshotScheduleAssociationRead(d, meta) +} + +func resourceAwsRedshiftSnapshotScheduleAssociationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).redshiftconn + clusterIdentifier, scheduleIdentifier, err := resourceAwsRedshiftSnapshotScheduleAssociationParseId(d.Id()) + if err != nil { + return fmt.Errorf("Error parse Redshift Cluster Snapshot Schedule Association ID %s: %s", d.Id(), err) + } + + descOpts := &redshift.DescribeSnapshotSchedulesInput{ + ClusterIdentifier: aws.String(clusterIdentifier), + ScheduleIdentifier: aws.String(scheduleIdentifier), + } + + resp, err := conn.DescribeSnapshotSchedules(descOpts) + if err != nil { + return fmt.Errorf("Error describing Redshift Cluster %s Snapshot Schedule %s: %s", clusterIdentifier, clusterIdentifier, err) + } + + if resp.SnapshotSchedules == nil || len(resp.SnapshotSchedules) == 0 { + return fmt.Errorf("Unable to find Redshift Cluster (%s) Snapshot Schedule (%s) Association", clusterIdentifier, scheduleIdentifier) + } + snapshotSchedule := resp.SnapshotSchedules[0] + if snapshotSchedule.AssociatedClusters == nil || aws.Int64Value(snapshotSchedule.AssociatedClusterCount) == 0 { + return fmt.Errorf("Unable to find Redshift Cluster (%s)", clusterIdentifier) + } + + var associatedCluster *redshift.ClusterAssociatedToSchedule + for _, cluster := range snapshotSchedule.AssociatedClusters { + if *cluster.ClusterIdentifier == clusterIdentifier { + associatedCluster = cluster + break + } + } + + if associatedCluster == nil { + return fmt.Errorf("Unable to find Redshift Cluster (%s)", clusterIdentifier) + } + + d.Set("cluster_identifier", associatedCluster.ClusterIdentifier) + d.Set("schedule_identifier", snapshotSchedule.ScheduleIdentifier) + + return nil +} + +func resourceAwsRedshiftSnapshotScheduleAssociationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).redshiftconn + clusterIdentifier, scheduleIdentifier, err := resourceAwsRedshiftSnapshotScheduleAssociationParseId(d.Id()) + if err != nil { + return fmt.Errorf("Error parse Redshift Cluster Snapshot Schedule Association ID %s: %s", d.Id(), err) + } + + _, err = conn.ModifyClusterSnapshotSchedule(&redshift.ModifyClusterSnapshotScheduleInput{ + ClusterIdentifier: aws.String(clusterIdentifier), + ScheduleIdentifier: aws.String(scheduleIdentifier), + DisassociateSchedule: aws.Bool(true), + }) + + if isAWSErr(err, redshift.ErrCodeClusterNotFoundFault, "") { + log.Printf("[WARN] Redshift Snapshot Cluster (%s) not found, removing from state", clusterIdentifier) + d.SetId("") + return nil + } + if isAWSErr(err, redshift.ErrCodeSnapshotScheduleNotFoundFault, "") { + log.Printf("[WARN] Redshift Snapshot Schedule (%s) not found, removing from state", scheduleIdentifier) + d.SetId("") + return nil + } + if err != nil { + return fmt.Errorf("Error disassociate Redshift Cluster (%s) and Snapshot Schedule (%s) Association: %s", clusterIdentifier, scheduleIdentifier, err) + } + + if err := waitForRedshiftSnapshotScheduleAssociationDestroy(conn, 75*time.Minute, clusterIdentifier, scheduleIdentifier); err != nil { + return err + } + + return nil +} + +func resourceAwsRedshiftSnapshotScheduleAssociationParseId(id string) (clusterIdentifier, scheduleIdentifier string, err error) { + parts := strings.SplitN(id, "/", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + err = fmt.Errorf("aws_redshift_snapshot_schedule_association id must be of the form /") + return + } + + clusterIdentifier = parts[0] + scheduleIdentifier = parts[1] + return +} + +func resourceAwsRedshiftSnapshotScheduleAssociationStateRefreshFunc(clusterIdentifier, scheduleIdentifier string, conn *redshift.Redshift) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + log.Printf("[INFO] Reading Redshift Cluster (%s) Snapshot Schedule (%s) Association Information", clusterIdentifier, scheduleIdentifier) + resp, err := conn.DescribeSnapshotSchedules(&redshift.DescribeSnapshotSchedulesInput{ + ClusterIdentifier: aws.String(clusterIdentifier), + ScheduleIdentifier: aws.String(scheduleIdentifier), + }) + if isAWSErr(err, redshift.ErrCodeClusterNotFoundFault, "") { + return 42, "destroyed", nil + } + if isAWSErr(err, redshift.ErrCodeSnapshotScheduleNotFoundFault, "") { + return 42, "destroyed", nil + } + if err != nil { + log.Printf("[WARN] Error on retrieving Redshift Cluster (%s) Snapshot Schedule (%s) Association when waiting: %s", clusterIdentifier, scheduleIdentifier, err) + return nil, "", err + } + + var rcas *redshift.ClusterAssociatedToSchedule + + for _, s := range resp.SnapshotSchedules { + if aws.StringValue(s.ScheduleIdentifier) == scheduleIdentifier { + for _, c := range s.AssociatedClusters { + if aws.StringValue(c.ClusterIdentifier) == clusterIdentifier { + rcas = c + } + } + } + } + + if rcas == nil { + return 42, "destroyed", nil + } + + if rcas.ScheduleAssociationState != nil { + log.Printf("[DEBUG] Redshift Cluster (%s) Snapshot Schedule (%s) Association status: %s", clusterIdentifier, scheduleIdentifier, aws.StringValue(rcas.ScheduleAssociationState)) + } + + return rcas, aws.StringValue(rcas.ScheduleAssociationState), nil + } +} + +func waitForRedshiftSnapshotScheduleAssociationActive(conn *redshift.Redshift, timeout time.Duration, clusterIdentifier, scheduleIdentifier string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{redshift.ScheduleStateModifying}, + Target: []string{redshift.ScheduleStateActive}, + Refresh: resourceAwsRedshiftSnapshotScheduleAssociationStateRefreshFunc(clusterIdentifier, scheduleIdentifier, conn), + Timeout: timeout, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, + } + + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf("Error waiting for Redshift Cluster (%s) and Snapshot Schedule (%s) Association state to be \"ACTIVE\": %s", clusterIdentifier, scheduleIdentifier, err) + } + + return nil +} + +func waitForRedshiftSnapshotScheduleAssociationDestroy(conn *redshift.Redshift, timeout time.Duration, clusterIdentifier, scheduleIdentifier string) error { + + stateConf := &resource.StateChangeConf{ + Pending: []string{redshift.ScheduleStateModifying, redshift.ScheduleStateActive}, + Target: []string{"destroyed"}, + Refresh: resourceAwsRedshiftSnapshotScheduleAssociationStateRefreshFunc(clusterIdentifier, scheduleIdentifier, conn), + Timeout: timeout, + MinTimeout: 10 * time.Second, + } + + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf("Error waiting for Redshift Cluster (%s) and Snapshot Schedule (%s) Association state to be \"destroyed\": %s", clusterIdentifier, scheduleIdentifier, err) + } + + return nil +} diff --git a/aws/resource_aws_redshift_snapshot_schedule_association_test.go b/aws/resource_aws_redshift_snapshot_schedule_association_test.go new file mode 100644 index 000000000000..c65d07fd447d --- /dev/null +++ b/aws/resource_aws_redshift_snapshot_schedule_association_test.go @@ -0,0 +1,119 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/redshift" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSRedshiftSnapshotScheduleAssociation_basic(t *testing.T) { + rInt := acctest.RandInt() + rName := acctest.RandString(8) + resourceName := "aws_redshift_snapshot_schedule_association.default" + snapshotScheduleResourceName := "aws_redshift_snapshot_schedule.default" + clusterResourceName := "aws_redshift_cluster.default" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRedshiftSnapshotScheduleAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRedshiftSnapshotScheduleAssociationConfig(rInt, rName, "rate(12 hours)"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRedshiftSnapshotScheduleAssociationExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "schedule_identifier", snapshotScheduleResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "cluster_identifier", clusterResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAWSRedshiftSnapshotScheduleAssociationDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_redshift_snapshot_schedule_association" { + continue + } + + conn := testAccProvider.Meta().(*AWSClient).redshiftconn + clusterIdentifier, scheduleIdentifier, err := resourceAwsRedshiftSnapshotScheduleAssociationParseId(rs.Primary.ID) + if err != nil { + return err + } + + resp, err := conn.DescribeSnapshotSchedules(&redshift.DescribeSnapshotSchedulesInput{ + ScheduleIdentifier: aws.String(scheduleIdentifier), + ClusterIdentifier: aws.String(clusterIdentifier), + }) + + if err != nil { + return err + } + + if resp != nil && len(resp.SnapshotSchedules) > 0 { + return fmt.Errorf("Redshift Cluster (%s) Snapshot Schedule (%s) Association still exist", clusterIdentifier, scheduleIdentifier) + } + + return err + } + + return nil +} + +func testAccCheckAWSRedshiftSnapshotScheduleAssociationExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Redshift Cluster Snapshot Schedule Association ID is set") + } + + clusterIdentifier, scheduleIdentifier, err := resourceAwsRedshiftSnapshotScheduleAssociationParseId(rs.Primary.ID) + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*AWSClient).redshiftconn + resp, err := conn.DescribeSnapshotSchedules(&redshift.DescribeSnapshotSchedulesInput{ + ScheduleIdentifier: aws.String(scheduleIdentifier), + ClusterIdentifier: aws.String(clusterIdentifier), + }) + + if err != nil { + return err + } + + if len(resp.SnapshotSchedules) != 0 { + return nil + } + + return fmt.Errorf("Redshift Cluster (%s) Snapshot Schedule (%s) Association not found", clusterIdentifier, scheduleIdentifier) + } +} + +func testAccAWSRedshiftSnapshotScheduleAssociationConfig(rInt int, rName, definition string) string { + return fmt.Sprintf(` +%s + +%s + +resource "aws_redshift_snapshot_schedule_association" "default" { + schedule_identifier = "${aws_redshift_snapshot_schedule.default.id}" + cluster_identifier = "${aws_redshift_cluster.default.id}" +} +`, testAccAWSRedshiftClusterConfig_basic(rInt), testAccAWSRedshiftSnapshotScheduleConfig(rName, definition)) +} diff --git a/aws/resource_aws_redshift_snapshot_schedule_test.go b/aws/resource_aws_redshift_snapshot_schedule_test.go new file mode 100644 index 000000000000..15a2bf59b7d7 --- /dev/null +++ b/aws/resource_aws_redshift_snapshot_schedule_test.go @@ -0,0 +1,455 @@ +package aws + +import ( + "fmt" + "log" + "strings" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/redshift" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func init() { + resource.AddTestSweepers("aws_redshift_snapshot_schedule", &resource.Sweeper{ + Name: "aws_redshift_snapshot_schedule", + F: testSweepRedshiftSnapshotSchedules, + }) +} + +func testSweepRedshiftSnapshotSchedules(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + conn := client.(*AWSClient).redshiftconn + + req := &redshift.DescribeSnapshotSchedulesInput{} + + resp, err := conn.DescribeSnapshotSchedules(req) + if err != nil { + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping Redshift Regional Snapshot Schedules sweep for %s: %s", region, err) + return nil + } + return fmt.Errorf("Error describing Redshift Regional Snapshot Schedules: %s", err) + } + + if len(resp.SnapshotSchedules) == 0 { + log.Print("[DEBUG] No AWS Redshift Regional Snapshot Schedules to sweep") + return nil + } + + for _, snapshotSchedules := range resp.SnapshotSchedules { + identifier := aws.StringValue(snapshotSchedules.ScheduleIdentifier) + + hasPrefix := false + prefixes := []string{"tf-snapshot-schedule-"} + + for _, prefix := range prefixes { + if strings.HasPrefix(identifier, prefix) { + hasPrefix = true + break + } + } + + if !hasPrefix { + log.Printf("[INFO] Skipping Delete Redshift Snapshot Schedule: %s", identifier) + continue + } + + _, err := conn.DeleteSnapshotSchedule(&redshift.DeleteSnapshotScheduleInput{ + ScheduleIdentifier: snapshotSchedules.ScheduleIdentifier, + }) + if isAWSErr(err, redshift.ErrCodeSnapshotScheduleNotFoundFault, "") { + return nil + } + if err != nil { + return fmt.Errorf("Error deleting Redshift Snapshot Schedule %s: %s", identifier, err) + } + } + + return nil +} + +func TestAccAWSRedshiftSnapshotSchedule_basic(t *testing.T) { + var v redshift.SnapshotSchedule + + rName := acctest.RandString(8) + resourceName := "aws_redshift_snapshot_schedule.default" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRedshiftSnapshotScheduleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRedshiftSnapshotScheduleConfig(rName, "rate(12 hours)"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRedshiftSnapshotScheduleExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "definitions.#", "1"), + ), + }, + { + Config: testAccAWSRedshiftSnapshotScheduleConfig(rName, "cron(30 12 *)"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRedshiftSnapshotScheduleExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "definitions.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "force_destroy", + }, + }, + }, + }) +} + +func TestAccAWSRedshiftSnapshotSchedule_withMultipleDefinition(t *testing.T) { + var v redshift.SnapshotSchedule + + rName := acctest.RandString(8) + resourceName := "aws_redshift_snapshot_schedule.default" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRedshiftSnapshotScheduleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRedshiftSnapshotScheduleConfigWithMultipleDefinition(rName, "cron(30 12 *)", "cron(15 6 *)"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRedshiftSnapshotScheduleExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "definitions.#", "2"), + ), + }, + { + Config: testAccAWSRedshiftSnapshotScheduleConfigWithMultipleDefinition(rName, "cron(30 8 *)", "cron(15 10 *)"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRedshiftSnapshotScheduleExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "definitions.#", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "force_destroy", + }, + }, + }, + }) + +} + +func TestAccAWSRedshiftSnapshotSchedule_withIdentifierPrefix(t *testing.T) { + var v redshift.SnapshotSchedule + + resourceName := "aws_redshift_snapshot_schedule.default" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRedshiftSnapshotScheduleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRedshiftSnapshotScheduleConfigWithIdentifierPrefix, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRedshiftSnapshotScheduleExists(resourceName, &v), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "identifier_prefix", + "force_destroy", + }, + }, + }, + }) +} + +func TestAccAWSRedshiftSnapshotSchedule_withDescription(t *testing.T) { + var v redshift.SnapshotSchedule + + rName := acctest.RandString(8) + resourceName := "aws_redshift_snapshot_schedule.default" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRedshiftSnapshotScheduleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRedshiftSnapshotScheduleConfigWithDescription(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRedshiftSnapshotScheduleExists(resourceName, &v), + resource.TestCheckResourceAttr( + resourceName, "description", "Test Schedule"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "force_destroy", + }, + }, + }, + }) +} + +func TestAccAWSRedshiftSnapshotSchedule_withTags(t *testing.T) { + var v redshift.SnapshotSchedule + + rName := acctest.RandString(8) + resourceName := "aws_redshift_snapshot_schedule.default" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRedshiftSnapshotScheduleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRedshiftSnapshotScheduleConfigWithTags(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRedshiftSnapshotScheduleExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.foo", "bar"), + resource.TestCheckResourceAttr(resourceName, "tags.fizz", "buzz"), + ), + }, + { + Config: testAccAWSRedshiftSnapshotScheduleConfigWithTagsUpdate(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRedshiftSnapshotScheduleExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.foo", "bar2"), + resource.TestCheckResourceAttr(resourceName, "tags.good", "bad"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "force_destroy", + }, + }, + }, + }) +} + +func TestAccAWSRedshiftSnapshotSchedule_withForceDestroy(t *testing.T) { + var snapshotSchedule redshift.SnapshotSchedule + var cluster redshift.Cluster + rInt := acctest.RandInt() + rName := acctest.RandString(8) + resourceName := "aws_redshift_snapshot_schedule.default" + clusterResourceName := "aws_redshift_cluster.default" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRedshiftSnapshotScheduleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRedshiftSnapshotScheduleConfigWithForceDestroy(rInt, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRedshiftSnapshotScheduleExists(resourceName, &snapshotSchedule), + testAccCheckAWSRedshiftClusterExists(clusterResourceName, &cluster), + testAccCheckAWSRedshiftSnapshotScheduleCreateSnapshotScheduleAssociation(&cluster, &snapshotSchedule), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "force_destroy", + }, + }, + }, + }) +} + +func testAccCheckAWSRedshiftSnapshotScheduleDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_redshift_snapshot_schedule" { + continue + } + + conn := testAccProvider.Meta().(*AWSClient).redshiftconn + resp, err := conn.DescribeSnapshotSchedules(&redshift.DescribeSnapshotSchedulesInput{ + ScheduleIdentifier: aws.String(rs.Primary.ID), + }) + + if err == nil { + if len(resp.SnapshotSchedules) != 0 { + for _, s := range resp.SnapshotSchedules { + if *s.ScheduleIdentifier == rs.Primary.ID { + return fmt.Errorf("Redshift Cluster Snapshot Schedule %s still exists", rs.Primary.ID) + } + } + } + } + + return err + } + + return nil +} + +func testAccCheckAWSRedshiftSnapshotScheduleExists(n string, v *redshift.SnapshotSchedule) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Redshift Cluster Snapshot Schedule ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).redshiftconn + resp, err := conn.DescribeSnapshotSchedules(&redshift.DescribeSnapshotSchedulesInput{ + ScheduleIdentifier: aws.String(rs.Primary.ID), + }) + + if err != nil { + return err + } + + for _, s := range resp.SnapshotSchedules { + if *s.ScheduleIdentifier == rs.Primary.ID { + *v = *s + return nil + } + } + + return fmt.Errorf("Redshift Snapshot Schedule (%s) not found", rs.Primary.ID) + } +} + +func testAccCheckAWSRedshiftSnapshotScheduleCreateSnapshotScheduleAssociation(cluster *redshift.Cluster, snapshotSchedule *redshift.SnapshotSchedule) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).redshiftconn + + if _, err := conn.ModifyClusterSnapshotSchedule(&redshift.ModifyClusterSnapshotScheduleInput{ + ClusterIdentifier: cluster.ClusterIdentifier, + ScheduleIdentifier: snapshotSchedule.ScheduleIdentifier, + DisassociateSchedule: aws.Bool(false), + }); err != nil { + return fmt.Errorf("Error associate Redshift Cluster and Snapshot Schedule: %s", err) + } + + if err := waitForRedshiftSnapshotScheduleAssociationActive(conn, 75*time.Minute, aws.StringValue(cluster.ClusterIdentifier), aws.StringValue(snapshotSchedule.ScheduleIdentifier)); err != nil { + return err + } + + return nil + } +} + +const testAccAWSRedshiftSnapshotScheduleConfigWithIdentifierPrefix = ` +resource "aws_redshift_snapshot_schedule" "default" { + identifier_prefix = "tf-snapshot-schedule-" + definitions = [ + "rate(12 hours)", + ] +} +` + +func testAccAWSRedshiftSnapshotScheduleConfig(rName, definition string) string { + return fmt.Sprintf(` +resource "aws_redshift_snapshot_schedule" "default" { + identifier = "tf-snapshot-schedule-%[1]s" + definitions = [ + "%[2]s", + ] +} + `, rName, definition) +} + +func testAccAWSRedshiftSnapshotScheduleConfigWithMultipleDefinition(rName, definition1, definition2 string) string { + return fmt.Sprintf(` +resource "aws_redshift_snapshot_schedule" "default" { + identifier = "tf-snapshot-schedule-%[1]s" + definitions = [ + "%[2]s", + "%[3]s", + ] +} + `, rName, definition1, definition2) +} + +func testAccAWSRedshiftSnapshotScheduleConfigWithDescription(rName string) string { + return fmt.Sprintf(` +resource "aws_redshift_snapshot_schedule" "default" { + identifier = "tf-snapshot-schedule-%[1]s" + description = "Test Schedule" + definitions = [ + "rate(12 hours)", + ] +} + `, rName) +} + +func testAccAWSRedshiftSnapshotScheduleConfigWithTags(rName string) string { + return fmt.Sprintf(` +resource "aws_redshift_snapshot_schedule" "default" { + identifier = "tf-snapshot-schedule-%[1]s" + definitions = [ + "rate(12 hours)", + ] + + tags = { + foo = "bar" + fizz = "buzz" + } +} + `, rName) +} + +func testAccAWSRedshiftSnapshotScheduleConfigWithTagsUpdate(rName string) string { + return fmt.Sprintf(` +resource "aws_redshift_snapshot_schedule" "default" { + identifier = "tf-snapshot-schedule-%[1]s" + definitions = [ + "rate(12 hours)", + ] + + tags = { + foo = "bar2" + good = "bad" + } +} + `, rName) +} + +func testAccAWSRedshiftSnapshotScheduleConfigWithForceDestroy(rInt int, rName string) string { + return fmt.Sprintf(` +%s + +resource "aws_redshift_snapshot_schedule" "default" { + identifier = "tf-snapshot-schedule-%[2]s" + description = "Test Schedule" + definitions = [ + "rate(12 hours)", + ] + force_destroy = true +} +`, testAccAWSRedshiftClusterConfig_basic(rInt), rName) +} diff --git a/website/aws.erb b/website/aws.erb index 9ff75a137926..2dc27c8b6240 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -2360,6 +2360,12 @@
  • aws_redshift_snapshot_copy_grant
  • +
  • + aws_redshift_snapshot_schedule_association +
  • +
  • + aws_redshift_snapshot_schedule +
  • aws_redshift_subnet_group
  • diff --git a/website/docs/r/redshift_snapshot_schedule.html.markdown b/website/docs/r/redshift_snapshot_schedule.html.markdown new file mode 100644 index 000000000000..852173fa207f --- /dev/null +++ b/website/docs/r/redshift_snapshot_schedule.html.markdown @@ -0,0 +1,40 @@ +--- +layout: "aws" +page_title: "AWS: aws_redshift_snapshot_schedule" +sidebar_current: "docs-aws-resource-redshift-snapshot-schedule" +description: |- + Provides an Redshift Snapshot Schedule resource. +--- + +# Resource: aws_redshift_snapshot_schedule + +## Example Usage + +```hcl +resource "aws_redshift_snapshot_schedule" "default" { + identifier = "tf-redshift-snapshot-schedule" + definitions = [ + "rate(12 hours)", + ] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `identifier` - (Optional, Forces new resource) The snapshot schedule identifier. If omitted, Terraform will assign a random, unique identifier. +* `identifier_prefix` - (Optional, Forces new resource) Creates a unique +identifier beginning with the specified prefix. Conflicts with `identifier`. +* `description` - (Optional) The description of the snapshot schedule. +* `definitions` - (Optional) The definition of the snapshot schedule. The definition is made up of schedule expressions, for example `cron(30 12 *)` or `rate(12 hours)`. +* `force_destroy` - (Optional) Whether to destroy all associated clusters with this snapshot schedule on deletion. Must be enabled and applied before attempting deletion. +* `tags` - (Optional) A mapping of tags to assign to the resource. + +## Import + +Redshift Snapshot Schedule can be imported using the `identifier`, e.g. + +``` +$ terraform import aws_redshift_snapshot_schedule.default tf-redshift-snapshot-schedule +``` diff --git a/website/docs/r/redshift_snapshot_schedule_association.html.markdown b/website/docs/r/redshift_snapshot_schedule_association.html.markdown new file mode 100644 index 000000000000..8652cbc19de6 --- /dev/null +++ b/website/docs/r/redshift_snapshot_schedule_association.html.markdown @@ -0,0 +1,49 @@ +--- +layout: "aws" +page_title: "AWS: aws_redshift_snapshot_schedule_association" +sidebar_current: "docs-aws-resource-redshift-snapshot-schedule-association" +description: |- + Provides an Association Redshift Cluster and Snapshot Schedule resource. +--- + +# Resource: aws_redshift_snapshot_schedule_association + +## Example Usage + +```hcl +resource "aws_redshift_cluster" "default" { + cluster_identifier = "tf-redshift-cluster" + database_name = "mydb" + master_username = "foo" + master_password = "Mustbe8characters" + node_type = "dc1.large" + cluster_type = "single-node" +} + +resource "aws_redshift_snapshot_schedule" "default" { + identifier = "tf-redshift-snapshot-schedule" + definitions = [ + "rate(12 hours)", + ] +} + +resource "aws_redshift_snapshot_schedule_association" "default" { + cluster_identifier = "${aws_redshift_cluster.default.id}" + schedule_identifier = "${aws_redshift_snapshot_schedule.default.id}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `cluster_identifier` - (Required, Forces new resource) The cluster identifier. +* `schedule_identifier` - (Required, Forces new resource) The snapshot schedule identifier. + +## Import + +Redshift Snapshot Schedule Association can be imported using the `/`, e.g. + +``` +$ terraform import aws_redshift_snapshot_schedule_association.default tf-redshift-cluster/tf-redshift-snapshot-schedule +``` \ No newline at end of file