From 2f366fa0453829e49d1e0d284aa6a019a6ecaf4d Mon Sep 17 00:00:00 2001 From: Russell Sherman Date: Wed, 16 May 2018 20:36:45 -0700 Subject: [PATCH 1/4] Adding cluster node IPs --- aws/resource_aws_redshift_cluster.go | 22 +++++++++++++++++++ aws/resource_aws_redshift_cluster_test.go | 22 +++++++++++++++++++ website/docs/r/redshift_cluster.html.markdown | 1 + 3 files changed, 45 insertions(+) diff --git a/aws/resource_aws_redshift_cluster.go b/aws/resource_aws_redshift_cluster.go index ece741ddcf0..12669ac2324 100644 --- a/aws/resource_aws_redshift_cluster.go +++ b/aws/resource_aws_redshift_cluster.go @@ -175,6 +175,14 @@ func resourceAwsRedshiftCluster() *schema.Resource { Default: 1, }, + "cluster_node_ips": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "publicly_accessible": { Type: schema.TypeBool, Optional: true, @@ -401,6 +409,10 @@ func resourceAwsRedshiftClusterCreate(d *schema.ResourceData, meta interface{}) restoreOpts.IamRoles = expandStringSet(v.(*schema.Set)) } + if v := d.Get("vpc_security_group_ids").(*schema.Set); v.Len() > 0 { + restoreOpts.VpcSecurityGroupIds = expandStringList(v.List()) + } + log.Printf("[DEBUG] Redshift Cluster restore cluster options: %s", restoreOpts) resp, err := conn.RestoreFromClusterSnapshot(restoreOpts) @@ -623,6 +635,16 @@ func resourceAwsRedshiftClusterRead(d *schema.ResourceData, meta interface{}) er return fmt.Errorf("Error saving IAM Roles to state for Redshift Cluster (%s): %s", d.Id(), err) } + if rsc.ClusterNodes != nil { + var nip []string + for _, i := range rsc.ClusterNodes { + if i.PrivateIPAddress != nil { + nip = append(nip, *i.PrivateIPAddress) + } + } + d.Set("cluster_node_ips", nip) + } + d.Set("cluster_public_key", rsc.ClusterPublicKey) d.Set("cluster_revision_number", rsc.ClusterRevisionNumber) tags := keyvaluetags.RedshiftKeyValueTags(rsc.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) diff --git a/aws/resource_aws_redshift_cluster_test.go b/aws/resource_aws_redshift_cluster_test.go index 92ed3b7cb86..8983d3376c6 100644 --- a/aws/resource_aws_redshift_cluster_test.go +++ b/aws/resource_aws_redshift_cluster_test.go @@ -175,6 +175,28 @@ func TestAccAWSRedshiftCluster_kmsKey(t *testing.T) { }) } +func TestAccAWSRedshiftCluster_privateIps(t *testing.T) { + var v redshift.Cluster + + ri := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + config := testAccAWSRedshiftClusterConfig_basic(ri) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRedshiftClusterDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRedshiftClusterExists("aws_redshift_cluster.default", &v), + resource.TestCheckResourceAttrSet("aws_redshift_cluster.default", "cluster_node_ips"), + ), + }, + }, + }) +} + func TestAccAWSRedshiftCluster_enhancedVpcRoutingEnabled(t *testing.T) { var v redshift.Cluster diff --git a/website/docs/r/redshift_cluster.html.markdown b/website/docs/r/redshift_cluster.html.markdown index 1a59fe1e32f..ca3ead1d1e2 100644 --- a/website/docs/r/redshift_cluster.html.markdown +++ b/website/docs/r/redshift_cluster.html.markdown @@ -121,6 +121,7 @@ In addition to all arguments above, the following attributes are exported: * `cluster_subnet_group_name` - The name of a cluster subnet group to be associated with this cluster * `cluster_public_key` - The public key for the cluster * `cluster_revision_number` - The specific revision number of the database in the cluster +* `cluster_node_ips` - The IPs associated with the cluster nodes * `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block). ## Import From b9db8a495f7a458f3f0c5633670ba5d984fb11f5 Mon Sep 17 00:00:00 2001 From: Russell Sherman Date: Wed, 16 May 2018 15:19:22 -0700 Subject: [PATCH 2/4] Add output key cluster_node_ips --- aws/resource_aws_redshift_cluster.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/aws/resource_aws_redshift_cluster.go b/aws/resource_aws_redshift_cluster.go index 12669ac2324..e86690b484c 100644 --- a/aws/resource_aws_redshift_cluster.go +++ b/aws/resource_aws_redshift_cluster.go @@ -644,6 +644,13 @@ func resourceAwsRedshiftClusterRead(d *schema.ResourceData, meta interface{}) er } d.Set("cluster_node_ips", nip) } + var nip []string + for _, i := range rsc.ClusterNodes { + nip = append(nip, *i.PrivateIPAddress) + } + if err := d.Set("cluster_node_ips", nip); err != nil { + return fmt.Errorf("[DEBUG] Error saving Cluster Node IPs to state for Redshift Cluster (%s): %s", d.Id(), err) + } d.Set("cluster_public_key", rsc.ClusterPublicKey) d.Set("cluster_revision_number", rsc.ClusterRevisionNumber) From ff740975d24ecb7c5c1eea3581ded55cd41552ce Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 31 Aug 2021 10:59:58 -0400 Subject: [PATCH 3/4] Add CHANGELOG entry. --- .changelog/4563.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/4563.txt diff --git a/.changelog/4563.txt b/.changelog/4563.txt new file mode 100644 index 00000000000..5bc4e932667 --- /dev/null +++ b/.changelog/4563.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_redshift_cluster: Add `cluster_nodes` attribute +``` \ No newline at end of file From 96ba48e8610428808f1721440fafcc22440914e3 Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Tue, 31 Aug 2021 11:17:12 -0400 Subject: [PATCH 4/4] r/aws_redshift_cluster: 'cluster_node_ips' -> 'cluster_nodes'. --- aws/internal/service/redshift/enum.go | 37 ++ .../service/redshift/finder/finder.go | 38 ++ .../service/redshift/waiter/status.go | 25 + .../service/redshift/waiter/waiter.go | 38 ++ aws/resource_aws_redshift_cluster.go | 608 ++++++++---------- aws/resource_aws_redshift_cluster_test.go | 98 +-- website/docs/r/redshift_cluster.html.markdown | 8 +- 7 files changed, 456 insertions(+), 396 deletions(-) create mode 100644 aws/internal/service/redshift/enum.go create mode 100644 aws/internal/service/redshift/finder/finder.go create mode 100644 aws/internal/service/redshift/waiter/status.go create mode 100644 aws/internal/service/redshift/waiter/waiter.go diff --git a/aws/internal/service/redshift/enum.go b/aws/internal/service/redshift/enum.go new file mode 100644 index 00000000000..8e518dcf27a --- /dev/null +++ b/aws/internal/service/redshift/enum.go @@ -0,0 +1,37 @@ +package redshift + +// https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-clusters.html#rs-mgmt-cluster-status. +const ( + ClusterStatusAvailable = "available" + ClusterStatusAvailablePrepForResize = "available, prep-for-resize" + ClusterStatusAvailableResizeCleanup = "available, resize-cleanup" + ClusterStatusCancellingResize = "cancelling-resize" + ClusterStatusCreating = "creating" + ClusterStatusDeleting = "deleting" + ClusterStatusFinalSnapshot = "final-snapshot" + ClusterStatusHardwareFailure = "hardware-failure" + ClusterStatusIncompatibleHsm = "incompatible-hsm" + ClusterStatusIncompatibleNetwork = "incompatible-network" + ClusterStatusIncompatibleParameters = "incompatible-parameters" + ClusterStatusIncompatibleRestore = "incompatible-restore" + ClusterStatusModifying = "modifying" + ClusterStatusPaused = "paused" + ClusterStatusRebooting = "rebooting" + ClusterStatusRenaming = "renaming" + ClusterStatusResizing = "resizing" + ClusterStatusRotatingKeys = "rotating-keys" + ClusterStatusStorageFull = "storage-full" + ClusterStatusUpdatingHsm = "updating-hsm" +) + +const ( + ClusterTypeMultiNode = "multi-node" + ClusterTypeSingleNode = "single-node" +) + +func ClusterType_Values() []string { + return []string{ + ClusterTypeMultiNode, + ClusterTypeSingleNode, + } +} diff --git a/aws/internal/service/redshift/finder/finder.go b/aws/internal/service/redshift/finder/finder.go new file mode 100644 index 00000000000..10cf6fabee6 --- /dev/null +++ b/aws/internal/service/redshift/finder/finder.go @@ -0,0 +1,38 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/redshift" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func ClusterByID(conn *redshift.Redshift, id string) (*redshift.Cluster, error) { + input := &redshift.DescribeClustersInput{ + ClusterIdentifier: aws.String(id), + } + + output, err := conn.DescribeClusters(input) + + if tfawserr.ErrCodeEquals(err, redshift.ErrCodeClusterNotFoundFault) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || len(output.Clusters) == 0 || output.Clusters[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output.Clusters); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + return output.Clusters[0], nil +} diff --git a/aws/internal/service/redshift/waiter/status.go b/aws/internal/service/redshift/waiter/status.go new file mode 100644 index 00000000000..5c2dc0bd96d --- /dev/null +++ b/aws/internal/service/redshift/waiter/status.go @@ -0,0 +1,25 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/redshift" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/redshift/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func ClusterStatus(conn *redshift.Redshift, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.ClusterByID(conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.ClusterStatus), nil + } +} diff --git a/aws/internal/service/redshift/waiter/waiter.go b/aws/internal/service/redshift/waiter/waiter.go new file mode 100644 index 00000000000..6cd56dbc9bf --- /dev/null +++ b/aws/internal/service/redshift/waiter/waiter.go @@ -0,0 +1,38 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/redshift" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfredshift "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/redshift" +) + +const ( + ClusterInvalidClusterStateFaultTimeout = 15 * time.Minute +) + +func ClusterDeleted(conn *redshift.Redshift, id string, timeout time.Duration) (*redshift.Cluster, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + tfredshift.ClusterStatusAvailable, + tfredshift.ClusterStatusCreating, + tfredshift.ClusterStatusDeleting, + tfredshift.ClusterStatusFinalSnapshot, + tfredshift.ClusterStatusRebooting, + tfredshift.ClusterStatusRenaming, + tfredshift.ClusterStatusResizing, + }, + Target: []string{}, + Refresh: ClusterStatus(conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*redshift.Cluster); ok { + return output, err + } + + return nil, err +} diff --git a/aws/resource_aws_redshift_cluster.go b/aws/resource_aws_redshift_cluster.go index e86690b484c..c219ee4c7fa 100644 --- a/aws/resource_aws_redshift_cluster.go +++ b/aws/resource_aws_redshift_cluster.go @@ -10,10 +10,15 @@ import ( "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/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfredshift "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/redshift" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/redshift/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/redshift/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func resourceAwsRedshiftCluster() *schema.Resource { @@ -33,22 +38,27 @@ func resourceAwsRedshiftCluster() *schema.Resource { }, Schema: map[string]*schema.Schema{ + "allow_version_upgrade": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, "arn": { Type: schema.TypeString, Computed: true, }, - - "database_name": { + "automated_snapshot_retention_period": { + Type: schema.TypeInt, + Optional: true, + Default: 1, + ValidateFunc: validation.IntAtMost(35), + }, + "availability_zone": { Type: schema.TypeString, Optional: true, + ForceNew: true, Computed: true, - ValidateFunc: validation.All( - validation.StringLenBetween(1, 64), - validation.StringMatch(regexp.MustCompile(`^[0-9a-z_$]+$`), "must contain only lowercase alphanumeric characters, underscores, and dollar signs"), - validation.StringMatch(regexp.MustCompile(`(?i)^[a-z_]`), "first character must be a letter or underscore"), - ), }, - "cluster_identifier": { Type: schema.TypeString, Required: true, @@ -60,207 +70,211 @@ func resourceAwsRedshiftCluster() *schema.Resource { validation.StringDoesNotMatch(regexp.MustCompile(`-$`), "cannot end with a hyphen"), ), }, - "cluster_type": { - Type: schema.TypeString, - Optional: true, + "cluster_nodes": { + Type: schema.TypeList, Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "node_role": { + Type: schema.TypeString, + Computed: true, + }, + "private_ip_address": { + Type: schema.TypeString, + Computed: true, + }, + "public_ip_address": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, }, - - "node_type": { + "cluster_parameter_group_name": { Type: schema.TypeString, - Required: true, + Optional: true, + Computed: true, }, - - "master_username": { + "cluster_public_key": { Type: schema.TypeString, Optional: true, - ForceNew: true, - ValidateFunc: validation.All( - validation.StringLenBetween(1, 128), - validation.StringMatch(regexp.MustCompile(`^\w+$`), "must contain only alphanumeric characters"), - validation.StringMatch(regexp.MustCompile(`(?i)^[a-z_]`), "first character must be a letter"), - ), - }, - - "master_password": { - Type: schema.TypeString, - Optional: true, - Sensitive: true, - ValidateFunc: validation.All( - validation.StringLenBetween(8, 64), - validation.StringMatch(regexp.MustCompile(`^.*[a-z].*`), "must contain at least one lowercase letter"), - validation.StringMatch(regexp.MustCompile(`^.*[A-Z].*`), "must contain at least one uppercase letter"), - validation.StringMatch(regexp.MustCompile(`^.*[0-9].*`), "must contain at least one number"), - validation.StringMatch(regexp.MustCompile(`^[^\@\/'" ]*$`), "cannot contain [/@\"' ]"), - ), + Computed: true, }, - - "cluster_security_groups": { - Type: schema.TypeSet, + "cluster_revision_number": { + Type: schema.TypeString, Optional: true, Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, }, - - "vpc_security_group_ids": { + "cluster_security_groups": { Type: schema.TypeSet, Optional: true, Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, Set: schema.HashString, }, - "cluster_subnet_group_name": { Type: schema.TypeString, Optional: true, ForceNew: true, Computed: true, }, - - "availability_zone": { + "cluster_type": { Type: schema.TypeString, Optional: true, - ForceNew: true, Computed: true, }, - - "preferred_maintenance_window": { + "cluster_version": { Type: schema.TypeString, Optional: true, - Computed: true, - StateFunc: func(val interface{}) string { - if val == nil { - return "" - } - return strings.ToLower(val.(string)) - }, - ValidateFunc: validateOnceAWeekWindowFormat, + Default: "1.0", }, - - "cluster_parameter_group_name": { + "database_name": { Type: schema.TypeString, Optional: true, Computed: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 64), + validation.StringMatch(regexp.MustCompile(`^[0-9a-z_$]+$`), "must contain only lowercase alphanumeric characters, underscores, and dollar signs"), + validation.StringMatch(regexp.MustCompile(`(?i)^[a-z_]`), "first character must be a letter or underscore"), + ), }, - - "automated_snapshot_retention_period": { - Type: schema.TypeInt, - Optional: true, - Default: 1, - ValidateFunc: validation.IntAtMost(35), - }, - - "port": { - Type: schema.TypeInt, - Optional: true, - Default: 5439, + "dns_name": { + Type: schema.TypeString, + Computed: true, }, - - "cluster_version": { + "elastic_ip": { Type: schema.TypeString, Optional: true, - Default: "1.0", }, - - "allow_version_upgrade": { + "encrypted": { Type: schema.TypeBool, Optional: true, - Default: true, - }, - - "number_of_nodes": { - Type: schema.TypeInt, - Optional: true, - Default: 1, + Default: false, }, - - "cluster_node_ips": { - Type: schema.TypeSet, + "endpoint": { + Type: schema.TypeString, Optional: true, Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, }, - - "publicly_accessible": { + "enhanced_vpc_routing": { Type: schema.TypeBool, Optional: true, - Default: true, + Computed: true, }, - - "encrypted": { - Type: schema.TypeBool, + "final_snapshot_identifier": { + Type: schema.TypeString, Optional: true, - Default: false, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 255), + validation.StringMatch(regexp.MustCompile(`^[0-9A-Za-z-]+$`), "must only contain alphanumeric characters and hyphens"), + validation.StringDoesNotMatch(regexp.MustCompile(`--`), "cannot contain two consecutive hyphens"), + validation.StringDoesNotMatch(regexp.MustCompile(`-$`), "cannot end in a hyphen"), + ), }, - - "enhanced_vpc_routing": { - Type: schema.TypeBool, + "iam_roles": { + Type: schema.TypeSet, Optional: true, Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, }, - "kms_key_id": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validateArn, }, - - "elastic_ip": { - Type: schema.TypeString, - Optional: true, + "logging": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + DiffSuppressFunc: suppressMissingOptionalConfigurationBlock, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "bucket_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "enable": { + Type: schema.TypeBool, + Required: true, + }, + "s3_key_prefix": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, }, - - "final_snapshot_identifier": { + "master_password": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validation.All( + validation.StringLenBetween(8, 64), + validation.StringMatch(regexp.MustCompile(`^.*[a-z].*`), "must contain at least one lowercase letter"), + validation.StringMatch(regexp.MustCompile(`^.*[A-Z].*`), "must contain at least one uppercase letter"), + validation.StringMatch(regexp.MustCompile(`^.*[0-9].*`), "must contain at least one number"), + validation.StringMatch(regexp.MustCompile(`^[^\@\/'" ]*$`), "cannot contain [/@\"' ]"), + ), + }, + "master_username": { Type: schema.TypeString, Optional: true, + ForceNew: true, ValidateFunc: validation.All( - validation.StringLenBetween(1, 255), - validation.StringMatch(regexp.MustCompile(`^[0-9A-Za-z-]+$`), "must only contain alphanumeric characters and hyphens"), - validation.StringDoesNotMatch(regexp.MustCompile(`--`), "cannot contain two consecutive hyphens"), - validation.StringDoesNotMatch(regexp.MustCompile(`-$`), "cannot end in a hyphen"), + validation.StringLenBetween(1, 128), + validation.StringMatch(regexp.MustCompile(`^\w+$`), "must contain only alphanumeric characters"), + validation.StringMatch(regexp.MustCompile(`(?i)^[a-z_]`), "first character must be a letter"), ), }, - - "skip_final_snapshot": { - Type: schema.TypeBool, + "node_type": { + Type: schema.TypeString, + Required: true, + }, + "number_of_nodes": { + Type: schema.TypeInt, Optional: true, - Default: false, + Default: 1, }, - - "endpoint": { + "owner_account": { Type: schema.TypeString, Optional: true, - Computed: true, }, - - "dns_name": { - Type: schema.TypeString, - Computed: true, + "port": { + Type: schema.TypeInt, + Optional: true, + Default: 5439, }, - - "cluster_public_key": { + "preferred_maintenance_window": { Type: schema.TypeString, Optional: true, Computed: true, + StateFunc: func(val interface{}) string { + if val == nil { + return "" + } + return strings.ToLower(val.(string)) + }, + ValidateFunc: validateOnceAWeekWindowFormat, }, - - "cluster_revision_number": { - Type: schema.TypeString, + "publicly_accessible": { + Type: schema.TypeBool, Optional: true, - Computed: true, + Default: true, }, - - "iam_roles": { - Type: schema.TypeSet, + "skip_final_snapshot": { + Type: schema.TypeBool, Optional: true, - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, + Default: false, + }, + "snapshot_cluster_identifier": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, }, - "snapshot_copy": { Type: schema.TypeList, MaxItems: 1, @@ -271,63 +285,30 @@ func resourceAwsRedshiftCluster() *schema.Resource { Type: schema.TypeString, Required: true, }, - "retention_period": { - Type: schema.TypeInt, - Optional: true, - Default: 7, - }, "grant_name": { Type: schema.TypeString, Optional: true, }, - }, - }, - }, - - "logging": { - Type: schema.TypeList, - MaxItems: 1, - Optional: true, - DiffSuppressFunc: suppressMissingOptionalConfigurationBlock, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "enable": { - Type: schema.TypeBool, - Required: true, - }, - - "bucket_name": { - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - - "s3_key_prefix": { - Type: schema.TypeString, + "retention_period": { + Type: schema.TypeInt, Optional: true, - Computed: true, + Default: 7, }, }, }, }, - "snapshot_identifier": { Type: schema.TypeString, Optional: true, ForceNew: true, }, - - "snapshot_cluster_identifier": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - }, - - "owner_account": { - Type: schema.TypeString, + "vpc_security_group_ids": { + Type: schema.TypeSet, Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, }, - "tags": tagsSchema(), "tags_all": tagsSchemaComputed(), }, @@ -409,10 +390,6 @@ func resourceAwsRedshiftClusterCreate(d *schema.ResourceData, meta interface{}) restoreOpts.IamRoles = expandStringSet(v.(*schema.Set)) } - if v := d.Get("vpc_security_group_ids").(*schema.Set); v.Len() > 0 { - restoreOpts.VpcSecurityGroupIds = expandStringList(v.List()) - } - log.Printf("[DEBUG] Redshift Cluster restore cluster options: %s", restoreOpts) resp, err := conn.RestoreFromClusterSnapshot(restoreOpts) @@ -542,118 +519,103 @@ func resourceAwsRedshiftClusterRead(d *schema.ResourceData, meta interface{}) er defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig - log.Printf("[INFO] Reading Redshift Cluster Information: %s", d.Id()) - resp, err := conn.DescribeClusters(&redshift.DescribeClustersInput{ - ClusterIdentifier: aws.String(d.Id()), - }) - - if err != nil { - if isAWSErr(err, redshift.ErrCodeClusterNotFoundFault, "") { - d.SetId("") - log.Printf("[DEBUG] Redshift Cluster (%s) not found", d.Id()) - return nil - } - log.Printf("[DEBUG] Error describing Redshift Cluster (%s)", d.Id()) - return err - } + rsc, err := finder.ClusterByID(conn, d.Id()) - var rsc *redshift.Cluster - for _, c := range resp.Clusters { - if *c.ClusterIdentifier == d.Id() { - rsc = c - } - } - - if rsc == nil { - log.Printf("[WARN] Redshift Cluster (%s) not found", d.Id()) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Redshift Cluster (%s) not found, removing from state", d.Id()) d.SetId("") return nil } - log.Printf("[INFO] Reading Redshift Cluster Logging Status: %s", d.Id()) - loggingStatus, loggingErr := conn.DescribeLoggingStatus(&redshift.DescribeLoggingStatusInput{ + if err != nil { + return fmt.Errorf("error reading Redshift Cluster (%s): %w", d.Id(), err) + } + + loggingStatus, err := conn.DescribeLoggingStatus(&redshift.DescribeLoggingStatusInput{ ClusterIdentifier: aws.String(d.Id()), }) - if loggingErr != nil { - return loggingErr + if err != nil { + return fmt.Errorf("error reading Redshift Cluster (%s) logging status: %w", d.Id(), err) } - d.Set("master_username", rsc.MasterUsername) - d.Set("node_type", rsc.NodeType) d.Set("allow_version_upgrade", rsc.AllowVersionUpgrade) - d.Set("database_name", rsc.DBName) - d.Set("cluster_identifier", rsc.ClusterIdentifier) - d.Set("cluster_version", rsc.ClusterVersion) - - d.Set("cluster_subnet_group_name", rsc.ClusterSubnetGroupName) - d.Set("availability_zone", rsc.AvailabilityZone) - d.Set("encrypted", rsc.Encrypted) - d.Set("enhanced_vpc_routing", rsc.EnhancedVpcRouting) - d.Set("kms_key_id", rsc.KmsKeyId) + arn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Service: "redshift", + Region: meta.(*AWSClient).region, + AccountID: meta.(*AWSClient).accountid, + Resource: fmt.Sprintf("cluster:%s", d.Id()), + }.String() + d.Set("arn", arn) d.Set("automated_snapshot_retention_period", rsc.AutomatedSnapshotRetentionPeriod) - d.Set("preferred_maintenance_window", rsc.PreferredMaintenanceWindow) - if rsc.Endpoint != nil && rsc.Endpoint.Address != nil { - endpoint := *rsc.Endpoint.Address - if rsc.Endpoint.Port != nil { - endpoint = fmt.Sprintf("%s:%d", endpoint, *rsc.Endpoint.Port) - } - d.Set("dns_name", rsc.Endpoint.Address) - d.Set("port", rsc.Endpoint.Port) - d.Set("endpoint", endpoint) + d.Set("availability_zone", rsc.AvailabilityZone) + d.Set("cluster_identifier", rsc.ClusterIdentifier) + if err := d.Set("cluster_nodes", flattenRedshiftClusterNodes(rsc.ClusterNodes)); err != nil { + return fmt.Errorf("error setting cluster_nodes: %w", err) } d.Set("cluster_parameter_group_name", rsc.ClusterParameterGroups[0].ParameterGroupName) + d.Set("cluster_public_key", rsc.ClusterPublicKey) + d.Set("cluster_revision_number", rsc.ClusterRevisionNumber) + d.Set("cluster_subnet_group_name", rsc.ClusterSubnetGroupName) if len(rsc.ClusterNodes) > 1 { - d.Set("cluster_type", "multi-node") + d.Set("cluster_type", tfredshift.ClusterTypeMultiNode) } else { - d.Set("cluster_type", "single-node") + d.Set("cluster_type", tfredshift.ClusterTypeSingleNode) } + d.Set("cluster_version", rsc.ClusterVersion) + d.Set("database_name", rsc.DBName) + d.Set("encrypted", rsc.Encrypted) + d.Set("enhanced_vpc_routing", rsc.EnhancedVpcRouting) + d.Set("kms_key_id", rsc.KmsKeyId) + if err := d.Set("logging", flattenRedshiftLogging(loggingStatus)); err != nil { + return fmt.Errorf("error setting logging: %w", err) + } + d.Set("master_username", rsc.MasterUsername) + d.Set("node_type", rsc.NodeType) d.Set("number_of_nodes", rsc.NumberOfNodes) + d.Set("preferred_maintenance_window", rsc.PreferredMaintenanceWindow) d.Set("publicly_accessible", rsc.PubliclyAccessible) - - var vpcg []string - for _, g := range rsc.VpcSecurityGroups { - vpcg = append(vpcg, *g.VpcSecurityGroupId) - } - if err := d.Set("vpc_security_group_ids", vpcg); err != nil { - return fmt.Errorf("Error saving VPC Security Group IDs to state for Redshift Cluster (%s): %s", d.Id(), err) + if err := d.Set("snapshot_copy", flattenRedshiftSnapshotCopy(rsc.ClusterSnapshotCopyStatus)); err != nil { + return fmt.Errorf("error setting snapshot_copy: %w", err) + } + + d.Set("dns_name", nil) + d.Set("endpoint", nil) + d.Set("port", nil) + if endpoint := rsc.Endpoint; endpoint != nil { + if address := aws.StringValue(endpoint.Address); address != "" { + d.Set("dns_name", address) + if port := aws.Int64Value(endpoint.Port); port != 0 { + d.Set("endpoint", fmt.Sprintf("%s:%d", address, port)) + d.Set("port", port) + } else { + d.Set("endpoint", address) + } + } } - var csg []string - for _, g := range rsc.ClusterSecurityGroups { - csg = append(csg, *g.ClusterSecurityGroupName) - } - if err := d.Set("cluster_security_groups", csg); err != nil { - return fmt.Errorf("Error saving Cluster Security Group Names to state for Redshift Cluster (%s): %s", d.Id(), err) - } + var apiList []*string - var iamRoles []string - for _, i := range rsc.IamRoles { - iamRoles = append(iamRoles, *i.IamRoleArn) - } - if err := d.Set("iam_roles", iamRoles); err != nil { - return fmt.Errorf("Error saving IAM Roles to state for Redshift Cluster (%s): %s", d.Id(), err) + for _, clusterSecurityGroup := range rsc.ClusterSecurityGroups { + apiList = append(apiList, clusterSecurityGroup.ClusterSecurityGroupName) } + d.Set("cluster_security_groups", aws.StringValueSlice(apiList)) - if rsc.ClusterNodes != nil { - var nip []string - for _, i := range rsc.ClusterNodes { - if i.PrivateIPAddress != nil { - nip = append(nip, *i.PrivateIPAddress) - } - } - d.Set("cluster_node_ips", nip) - } - var nip []string - for _, i := range rsc.ClusterNodes { - nip = append(nip, *i.PrivateIPAddress) + apiList = nil + + for _, iamRole := range rsc.IamRoles { + apiList = append(apiList, iamRole.IamRoleArn) } - if err := d.Set("cluster_node_ips", nip); err != nil { - return fmt.Errorf("[DEBUG] Error saving Cluster Node IPs to state for Redshift Cluster (%s): %s", d.Id(), err) + d.Set("iam_roles", aws.StringValueSlice(apiList)) + + apiList = nil + + for _, vpcSecurityGroup := range rsc.VpcSecurityGroups { + apiList = append(apiList, vpcSecurityGroup.VpcSecurityGroupId) } + d.Set("vpc_security_group_ids", aws.StringValueSlice(apiList)) - d.Set("cluster_public_key", rsc.ClusterPublicKey) - d.Set("cluster_revision_number", rsc.ClusterRevisionNumber) tags := keyvaluetags.RedshiftKeyValueTags(rsc.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) //lintignore:AWSR002 @@ -665,22 +627,6 @@ func resourceAwsRedshiftClusterRead(d *schema.ResourceData, meta interface{}) er return fmt.Errorf("error setting tags_all: %w", err) } - d.Set("snapshot_copy", flattenRedshiftSnapshotCopy(rsc.ClusterSnapshotCopyStatus)) - - if err := d.Set("logging", flattenRedshiftLogging(loggingStatus)); err != nil { - return fmt.Errorf("error setting logging: %s", err) - } - - arn := arn.ARN{ - Partition: meta.(*AWSClient).partition, - Service: "redshift", - Region: meta.(*AWSClient).region, - AccountID: meta.(*AWSClient).accountid, - Resource: fmt.Sprintf("cluster:%s", d.Id()), - }.String() - - d.Set("arn", arn) - return nil } @@ -913,67 +859,45 @@ func enableRedshiftSnapshotCopy(id string, scList []interface{}, conn *redshift. func resourceAwsRedshiftClusterDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).redshiftconn - log.Printf("[DEBUG] Destroying Redshift Cluster (%s)", d.Id()) - - deleteOpts := redshift.DeleteClusterInput{ - ClusterIdentifier: aws.String(d.Id()), - } skipFinalSnapshot := d.Get("skip_final_snapshot").(bool) - deleteOpts.SkipFinalClusterSnapshot = aws.Bool(skipFinalSnapshot) + input := &redshift.DeleteClusterInput{ + ClusterIdentifier: aws.String(d.Id()), + SkipFinalClusterSnapshot: aws.Bool(skipFinalSnapshot), + } if !skipFinalSnapshot { - if name, present := d.GetOk("final_snapshot_identifier"); present { - deleteOpts.FinalClusterSnapshotIdentifier = aws.String(name.(string)) + if v, ok := d.GetOk("final_snapshot_identifier"); ok { + input.FinalClusterSnapshotIdentifier = aws.String(v.(string)) } else { return fmt.Errorf("Redshift Cluster Instance FinalSnapshotIdentifier is required when a final snapshot is required") } } - log.Printf("[DEBUG] Deleting Redshift Cluster: %s", deleteOpts) - log.Printf("[DEBUG] schema.TimeoutDelete: %+v", d.Timeout(schema.TimeoutDelete)) - err := deleteAwsRedshiftCluster(&deleteOpts, conn, d.Timeout(schema.TimeoutDelete)) - if err != nil { - return err - } - - log.Printf("[INFO] Redshift Cluster %s successfully deleted", d.Id()) - - return nil -} - -func deleteAwsRedshiftCluster(opts *redshift.DeleteClusterInput, conn *redshift.Redshift, timeout time.Duration) error { - id := *opts.ClusterIdentifier - log.Printf("[INFO] Deleting Redshift Cluster %q", id) - err := resource.Retry(15*time.Minute, func() *resource.RetryError { - _, err := conn.DeleteCluster(opts) - if isAWSErr(err, redshift.ErrCodeInvalidClusterStateFault, "") { - return resource.RetryableError(err) - } + log.Printf("[DEBUG] Deleting Redshift Cluster: %s", d.Id()) + _, err := tfresource.RetryWhenAwsErrCodeEquals( + waiter.ClusterInvalidClusterStateFaultTimeout, + func() (interface{}, error) { + return conn.DeleteCluster(input) + }, + redshift.ErrCodeInvalidClusterStateFault, + ) - if err != nil { - return resource.NonRetryableError(err) - } + if tfawserr.ErrCodeEquals(err, redshift.ErrCodeClusterNotFoundFault) { return nil - }) - if isResourceTimeoutError(err) { - _, err = conn.DeleteCluster(opts) } + if err != nil { - return fmt.Errorf("Error deleting Redshift Cluster (%s): %s", id, err) + return fmt.Errorf("error deleting Redshift Cluster (%s): %w", d.Id(), err) } - stateConf := &resource.StateChangeConf{ - Pending: []string{"available", "creating", "deleting", "rebooting", "resizing", "renaming", "final-snapshot"}, - Target: []string{"destroyed"}, - Refresh: resourceAwsRedshiftClusterStateRefreshFunc(id, conn), - Timeout: timeout, - MinTimeout: 5 * time.Second, - } + _, err = waiter.ClusterDeleted(conn, d.Id(), d.Timeout(schema.TimeoutDelete)) - _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("error waiting for Redshift Cluster (%s) delete: %w", d.Id(), err) + } - return err + return nil } func resourceAwsRedshiftClusterStateRefreshFunc(id string, conn *redshift.Redshift) resource.StateRefreshFunc { @@ -1010,3 +934,43 @@ func resourceAwsRedshiftClusterStateRefreshFunc(id string, conn *redshift.Redshi return rsc, *rsc.ClusterStatus, nil } } + +func flattenRedshiftClusterNode(apiObject *redshift.ClusterNode) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.NodeRole; v != nil { + tfMap["node_role"] = aws.StringValue(v) + } + + if v := apiObject.PrivateIPAddress; v != nil { + tfMap["private_ip_address"] = aws.StringValue(v) + } + + if v := apiObject.PublicIPAddress; v != nil { + tfMap["public_ip_address"] = aws.StringValue(v) + } + + return tfMap +} + +func flattenRedshiftClusterNodes(apiObjects []*redshift.ClusterNode) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenRedshiftClusterNode(apiObject)) + } + + return tfList +} diff --git a/aws/resource_aws_redshift_cluster_test.go b/aws/resource_aws_redshift_cluster_test.go index 8983d3376c6..658d151ca25 100644 --- a/aws/resource_aws_redshift_cluster_test.go +++ b/aws/resource_aws_redshift_cluster_test.go @@ -8,13 +8,14 @@ import ( "testing" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/redshift" multierror "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/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/redshift/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func init() { @@ -86,10 +87,10 @@ func TestAccAWSRedshiftCluster_basic(t *testing.T) { Config: config, Check: resource.ComposeTestCheckFunc( testAccCheckAWSRedshiftClusterExists("aws_redshift_cluster.default", &v), - resource.TestCheckResourceAttr( - "aws_redshift_cluster.default", "cluster_type", "single-node"), - resource.TestCheckResourceAttr( - "aws_redshift_cluster.default", "publicly_accessible", "true"), + resource.TestCheckResourceAttr("aws_redshift_cluster.default", "cluster_nodes.#", "1"), + resource.TestCheckResourceAttrSet("aws_redshift_cluster.default", "cluster_nodes.0.public_ip_address"), + resource.TestCheckResourceAttr("aws_redshift_cluster.default", "cluster_type", "single-node"), + resource.TestCheckResourceAttr("aws_redshift_cluster.default", "publicly_accessible", "true"), resource.TestMatchResourceAttr("aws_redshift_cluster.default", "dns_name", regexp.MustCompile(fmt.Sprintf("^tf-redshift-cluster-%d.*\\.redshift\\..*", ri))), ), }, @@ -175,28 +176,6 @@ func TestAccAWSRedshiftCluster_kmsKey(t *testing.T) { }) } -func TestAccAWSRedshiftCluster_privateIps(t *testing.T) { - var v redshift.Cluster - - ri := rand.New(rand.NewSource(time.Now().UnixNano())).Int() - config := testAccAWSRedshiftClusterConfig_basic(ri) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckAWSRedshiftClusterDestroy, - Steps: []resource.TestStep{ - { - Config: config, - Check: resource.ComposeTestCheckFunc( - testAccCheckAWSRedshiftClusterExists("aws_redshift_cluster.default", &v), - resource.TestCheckResourceAttrSet("aws_redshift_cluster.default", "cluster_node_ips"), - ), - }, - }, - }) -} - func TestAccAWSRedshiftCluster_enhancedVpcRoutingEnabled(t *testing.T) { var v redshift.Cluster @@ -627,34 +606,24 @@ func TestAccAWSRedshiftCluster_changeEncryption2(t *testing.T) { } func testAccCheckAWSRedshiftClusterDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).redshiftconn + for _, rs := range s.RootModule().Resources { if rs.Type != "aws_redshift_cluster" { continue } - // Try to find the Group - conn := testAccProvider.Meta().(*AWSClient).redshiftconn - var err error - resp, err := conn.DescribeClusters( - &redshift.DescribeClustersInput{ - ClusterIdentifier: aws.String(rs.Primary.ID), - }) - - if err == nil { - if len(resp.Clusters) != 0 && - *resp.Clusters[0].ClusterIdentifier == rs.Primary.ID { - return fmt.Errorf("Redshift Cluster %s still exists", rs.Primary.ID) - } + _, err := finder.ClusterByID(conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue } - // Return nil if the cluster is already destroyed - if awsErr, ok := err.(awserr.Error); ok { - if awsErr.Code() == "ClusterNotFound" { - return nil - } + if err != nil { + return err } - return err + return fmt.Errorf("Redshift Cluster %s still exists", rs.Primary.ID) } return nil @@ -681,28 +650,17 @@ func testAccCheckAWSRedshiftClusterSnapshot(rInt int) resource.TestCheckFunc { return fmt.Errorf("error deleting Redshift Cluster Snapshot (%s): %w", snapshot_identifier, err) } - //lastly check that the Cluster is missing - resp, err := conn.DescribeClusters( - &redshift.DescribeClustersInput{ - ClusterIdentifier: aws.String(rs.Primary.ID), - }) + _, err = finder.ClusterByID(conn, rs.Primary.ID) - if err == nil { - if len(resp.Clusters) != 0 && - *resp.Clusters[0].ClusterIdentifier == rs.Primary.ID { - return fmt.Errorf("Redshift Cluster %s still exists", rs.Primary.ID) - } + if tfresource.NotFound(err) { + return nil } - // Return nil if the cluster is already destroyed - if awsErr, ok := err.(awserr.Error); ok { - if awsErr.Code() == "ClusterNotFound" { - return nil - } - + if err != nil { return err } + return fmt.Errorf("Redshift Cluster %s still exists", rs.Primary.ID) } return nil @@ -717,26 +675,20 @@ func testAccCheckAWSRedshiftClusterExists(n string, v *redshift.Cluster) resourc } if rs.Primary.ID == "" { - return fmt.Errorf("No Redshift Cluster Instance ID is set") + return fmt.Errorf("No Redshift Cluster ID is set") } conn := testAccProvider.Meta().(*AWSClient).redshiftconn - resp, err := conn.DescribeClusters(&redshift.DescribeClustersInput{ - ClusterIdentifier: aws.String(rs.Primary.ID), - }) + + output, err := finder.ClusterByID(conn, rs.Primary.ID) if err != nil { return err } - for _, c := range resp.Clusters { - if *c.ClusterIdentifier == rs.Primary.ID { - *v = *c - return nil - } - } + *v = *output - return fmt.Errorf("Redshift Cluster (%s) not found", rs.Primary.ID) + return nil } } diff --git a/website/docs/r/redshift_cluster.html.markdown b/website/docs/r/redshift_cluster.html.markdown index ca3ead1d1e2..40c5ea7ab5a 100644 --- a/website/docs/r/redshift_cluster.html.markdown +++ b/website/docs/r/redshift_cluster.html.markdown @@ -121,9 +121,15 @@ In addition to all arguments above, the following attributes are exported: * `cluster_subnet_group_name` - The name of a cluster subnet group to be associated with this cluster * `cluster_public_key` - The public key for the cluster * `cluster_revision_number` - The specific revision number of the database in the cluster -* `cluster_node_ips` - The IPs associated with the cluster nodes +* `cluster_nodes` - The nodes in the cluster. Cluster node blocks are documented below * `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block). +Cluster nodes (for `cluster_nodes`) support the following attributes: + +* `node_role` - Whether the node is a leader node or a compute node +* `private_ip_address` - The private IP address of a node within a cluster +* `public_ip_address` - The public IP address of a node within a cluster + ## Import Redshift Clusters can be imported using the `cluster_identifier`, e.g.