From 85afc7d614dbd7d12f2d3d9cdeaa145e486d6dfa Mon Sep 17 00:00:00 2001 From: stack72 Date: Wed, 11 Nov 2015 20:51:46 +0000 Subject: [PATCH 1/4] Initial creation of the work for AWS RedShift Support Finalising the schema and acceptance tests for the Redshift Security Group's --- builtin/providers/aws/config.go | 6 + builtin/providers/aws/provider.go | 1 + .../resource_aws_redshift_security_group.go | 320 ++++++++++++++++++ ...source_aws_redshift_security_group_test.go | 205 +++++++++++ 4 files changed, 532 insertions(+) create mode 100644 builtin/providers/aws/resource_aws_redshift_security_group.go create mode 100644 builtin/providers/aws/resource_aws_redshift_security_group_test.go diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index e3e2243f1..d98488c1f 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -39,6 +39,7 @@ import ( "github.com/aws/aws-sdk-go/service/lambda" "github.com/aws/aws-sdk-go/service/opsworks" "github.com/aws/aws-sdk-go/service/rds" + "github.com/aws/aws-sdk-go/service/redshift" "github.com/aws/aws-sdk-go/service/route53" "github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/sns" @@ -75,6 +76,7 @@ type AWSClient struct { s3conn *s3.S3 sqsconn *sqs.SQS snsconn *sns.SNS + redshiftconn *redshift.Redshift r53conn *route53.Route53 region string rdsconn *rds.RDS @@ -233,6 +235,10 @@ func (c *Config) Client() (interface{}, error) { log.Println("[INFO] Initializing CodeCommit SDK connection") client.codecommitconn = codecommit.New(usEast1Sess) + + log.Println("[INFO] Initializing Redshift SDK connection") + client.redshiftconn = redshift.New(sess) + } if len(errs) > 0 { diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 6b0c8db2e..45912f6cb 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -170,6 +170,7 @@ func Provider() terraform.ResourceProvider { "aws_proxy_protocol_policy": resourceAwsProxyProtocolPolicy(), "aws_rds_cluster": resourceAwsRDSCluster(), "aws_rds_cluster_instance": resourceAwsRDSClusterInstance(), + "aws_redshift_security_group": resourceAwsRedshiftSecurityGroup(), "aws_route53_delegation_set": resourceAwsRoute53DelegationSet(), "aws_route53_record": resourceAwsRoute53Record(), "aws_route53_zone_association": resourceAwsRoute53ZoneAssociation(), diff --git a/builtin/providers/aws/resource_aws_redshift_security_group.go b/builtin/providers/aws/resource_aws_redshift_security_group.go new file mode 100644 index 000000000..9f2520d15 --- /dev/null +++ b/builtin/providers/aws/resource_aws_redshift_security_group.go @@ -0,0 +1,320 @@ +package aws + +import ( + "bytes" + "fmt" + "log" + "regexp" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/redshift" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsRedshiftSecurityGroup() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsRedshiftSecurityGroupCreate, + Read: resourceAwsRedshiftSecurityGroupRead, + Delete: resourceAwsRedshiftSecurityGroupDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateRedshiftSecurityGroupName, + }, + + "description": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "ingress": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cidr": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "security_group_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "security_group_owner_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + Set: resourceAwsRedshiftSecurityGroupIngressHash, + }, + + "tags": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsRedshiftSecurityGroupCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).redshiftconn + + var err error + var errs []error + + name := d.Get("name").(string) + desc := d.Get("description").(string) + tags := tagsFromMapRedshift(d.Get("tags").(map[string]interface{})) + sgInput := &redshift.CreateClusterSecurityGroupInput{ + ClusterSecurityGroupName: aws.String(name), + Description: aws.String(desc), + Tags: tags, + } + log.Printf("[DEBUG] Redshift security group create: name: %s, description: %s", name, desc) + _, err = conn.CreateClusterSecurityGroup(sgInput) + if err != nil { + return fmt.Errorf("Error creating RedshiftSecurityGroup: %s", err) + } + + d.SetId(d.Get("name").(string)) + + log.Printf("[INFO] Redshift Security Group ID: %s", d.Id()) + sg, err := resourceAwsRedshiftSecurityGroupRetrieve(d, meta) + if err != nil { + return err + } + + ingresses := d.Get("ingress").(*schema.Set) + for _, ing := range ingresses.List() { + err := resourceAwsRedshiftSecurityGroupAuthorizeRule(ing, *sg.ClusterSecurityGroupName, conn) + if err != nil { + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return &multierror.Error{Errors: errs} + } + + log.Println("[INFO] Waiting for Redshift Security Group Ingress Authorizations to be authorized") + stateConf := &resource.StateChangeConf{ + Pending: []string{"authorizing"}, + Target: "authorized", + Refresh: resourceAwsRedshiftSecurityGroupStateRefreshFunc(d, meta), + Timeout: 10 * time.Minute, + } + + _, err = stateConf.WaitForState() + if err != nil { + return err + } + + return resourceAwsRedshiftSecurityGroupRead(d, meta) +} + +func resourceAwsRedshiftSecurityGroupRead(d *schema.ResourceData, meta interface{}) error { + sg, err := resourceAwsRedshiftSecurityGroupRetrieve(d, meta) + if err != nil { + return err + } + + rules := &schema.Set{ + F: resourceAwsRedshiftSecurityGroupIngressHash, + } + + for _, v := range sg.IPRanges { + rule := map[string]interface{}{"cidr": *v.CIDRIP} + rules.Add(rule) + } + + for _, g := range sg.EC2SecurityGroups { + rule := map[string]interface{}{ + "security_group_name": *g.EC2SecurityGroupName, + "security_group_owner_id": *g.EC2SecurityGroupOwnerId, + } + rules.Add(rule) + } + + d.Set("ingress", rules) + d.Set("name", *sg.ClusterSecurityGroupName) + d.Set("description", *sg.Description) + + return nil +} + +func resourceAwsRedshiftSecurityGroupDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).redshiftconn + + log.Printf("[DEBUG] Redshift Security Group destroy: %v", d.Id()) + opts := redshift.DeleteClusterSecurityGroupInput{ + ClusterSecurityGroupName: aws.String(d.Id()), + } + + log.Printf("[DEBUG] Redshift Security Group destroy configuration: %v", opts) + _, err := conn.DeleteClusterSecurityGroup(&opts) + + if err != nil { + newerr, ok := err.(awserr.Error) + if ok && newerr.Code() == "InvalidRedshiftSecurityGroup.NotFound" { + return nil + } + return err + } + + return nil +} + +func resourceAwsRedshiftSecurityGroupRetrieve(d *schema.ResourceData, meta interface{}) (*redshift.ClusterSecurityGroup, error) { + conn := meta.(*AWSClient).redshiftconn + + opts := redshift.DescribeClusterSecurityGroupsInput{ + ClusterSecurityGroupName: aws.String(d.Id()), + } + + log.Printf("[DEBUG] Redshift Security Group describe configuration: %#v", opts) + + resp, err := conn.DescribeClusterSecurityGroups(&opts) + + if err != nil { + return nil, fmt.Errorf("Error retrieving Redshift Security Groups: %s", err) + } + + if len(resp.ClusterSecurityGroups) != 1 || + *resp.ClusterSecurityGroups[0].ClusterSecurityGroupName != d.Id() { + return nil, fmt.Errorf("Unable to find Redshift Security Group: %#v", resp.ClusterSecurityGroups) + } + + return resp.ClusterSecurityGroups[0], nil +} + +func tagsFromMapRedshift(m map[string]interface{}) []*redshift.Tag { + result := make([]*redshift.Tag, 0, len(m)) + for k, v := range m { + result = append(result, &redshift.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + }) + } + + return result +} + +func tagsToMapRedshift(ts []*redshift.Tag) map[string]string { + result := make(map[string]string) + for _, t := range ts { + result[*t.Key] = *t.Value + } + + return result +} + +func validateRedshiftSecurityGroupName(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if value == "default" { + errors = append(errors, fmt.Errorf("the Redshift Security Group name cannot be %q", value)) + } + if !regexp.MustCompile(`^[0-9a-z-]+$`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "only lowercase alphanumeric characters and hyphens allowed in %q: %q", + k, value)) + } + if len(value) > 255 { + errors = append(errors, fmt.Errorf( + "%q cannot be longer than 32 characters: %q", k, value)) + } + return + +} + +func resourceAwsRedshiftSecurityGroupIngressHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + + if v, ok := m["cidr"]; ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + + if v, ok := m["security_group_name"]; ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + + if v, ok := m["security_group_owner_id"]; ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + + return hashcode.String(buf.String()) +} + +func resourceAwsRedshiftSecurityGroupAuthorizeRule(ingress interface{}, redshiftSecurityGroupName string, conn *redshift.Redshift) error { + ing := ingress.(map[string]interface{}) + + opts := redshift.AuthorizeClusterSecurityGroupIngressInput{ + ClusterSecurityGroupName: aws.String(redshiftSecurityGroupName), + } + + if attr, ok := ing["cidr"]; ok && attr != "" { + opts.CIDRIP = aws.String(attr.(string)) + } + + if attr, ok := ing["security_group_name"]; ok && attr != "" { + opts.EC2SecurityGroupName = aws.String(attr.(string)) + } + + if attr, ok := ing["security_group_owner_id"]; ok && attr != "" { + opts.EC2SecurityGroupOwnerId = aws.String(attr.(string)) + } + + log.Printf("[DEBUG] Authorize ingress rule configuration: %#v", opts) + _, err := conn.AuthorizeClusterSecurityGroupIngress(&opts) + + if err != nil { + return fmt.Errorf("Error authorizing security group ingress: %s", err) + } + + return nil +} + +func resourceAwsRedshiftSecurityGroupStateRefreshFunc( + d *schema.ResourceData, meta interface{}) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + v, err := resourceAwsRedshiftSecurityGroupRetrieve(d, meta) + + if err != nil { + log.Printf("Error on retrieving Redshift Security Group when waiting: %s", err) + return nil, "", err + } + + statuses := make([]string, 0, len(v.EC2SecurityGroups)+len(v.IPRanges)) + for _, ec2g := range v.EC2SecurityGroups { + statuses = append(statuses, *ec2g.Status) + } + for _, ips := range v.IPRanges { + statuses = append(statuses, *ips.Status) + } + + for _, stat := range statuses { + // Not done + if stat != "authorized" { + return nil, "authorizing", nil + } + } + + return v, "authorized", nil + } +} diff --git a/builtin/providers/aws/resource_aws_redshift_security_group_test.go b/builtin/providers/aws/resource_aws_redshift_security_group_test.go new file mode 100644 index 000000000..8b6137975 --- /dev/null +++ b/builtin/providers/aws/resource_aws_redshift_security_group_test.go @@ -0,0 +1,205 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/redshift" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSRedshiftSecurityGroup_ingressCidr(t *testing.T) { + var v redshift.ClusterSecurityGroup + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRedshiftSecurityGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSRedshiftSecurityGroupConfig_ingressCidr, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRedshiftSecurityGroupExists("aws_redshift_security_group.bar", &v), + resource.TestCheckResourceAttr( + "aws_redshift_security_group.bar", "name", "redshift-sg-terraform"), + resource.TestCheckResourceAttr( + "aws_redshift_security_group.bar", "description", "this is a description"), + resource.TestCheckResourceAttr( + "aws_redshift_security_group.bar", "ingress.2735652665.cidr", "10.0.0.1/24"), + resource.TestCheckResourceAttr( + "aws_redshift_security_group.bar", "ingress.#", "1"), + ), + }, + }, + }) +} + +func TestAccAWSRedshiftSecurityGroup_ingressSecurityGroup(t *testing.T) { + var v redshift.ClusterSecurityGroup + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRedshiftSecurityGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSRedshiftSecurityGroupConfig_ingressSgId, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRedshiftSecurityGroupExists("aws_redshift_security_group.bar", &v), + resource.TestCheckResourceAttr( + "aws_redshift_security_group.bar", "name", "redshift-sg-terraform"), + resource.TestCheckResourceAttr( + "aws_redshift_security_group.bar", "description", "this is a description"), + resource.TestCheckResourceAttr( + "aws_redshift_security_group.bar", "ingress.#", "1"), + resource.TestCheckResourceAttr( + "aws_redshift_security_group.bar", "ingress.220863.security_group_name", "terraform_redshift_acceptance_test"), + ), + }, + }, + }) +} + +func testAccCheckAWSRedshiftSecurityGroupExists(n string, v *redshift.ClusterSecurityGroup) 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 Security Group ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).redshiftconn + + opts := redshift.DescribeClusterSecurityGroupsInput{ + ClusterSecurityGroupName: aws.String(rs.Primary.ID), + } + + resp, err := conn.DescribeClusterSecurityGroups(&opts) + + if err != nil { + return err + } + + if len(resp.ClusterSecurityGroups) != 1 || + *resp.ClusterSecurityGroups[0].ClusterSecurityGroupName != rs.Primary.ID { + return fmt.Errorf("Redshift Security Group not found") + } + + *v = *resp.ClusterSecurityGroups[0] + + return nil + } +} + +func testAccCheckAWSRedshiftSecurityGroupDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).redshiftconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_redshift_security_group" { + continue + } + + // Try to find the Group + resp, err := conn.DescribeClusterSecurityGroups( + &redshift.DescribeClusterSecurityGroupsInput{ + ClusterSecurityGroupName: aws.String(rs.Primary.ID), + }) + + if err == nil { + if len(resp.ClusterSecurityGroups) != 0 && + *resp.ClusterSecurityGroups[0].ClusterSecurityGroupName == rs.Primary.ID { + return fmt.Errorf("Redshift Security Group still exists") + } + } + + // Verify the error + newerr, ok := err.(awserr.Error) + if !ok { + return err + } + if newerr.Code() != "InvalidRedshiftSecurityGroup.NotFound" { + return err + } + } + + return nil +} + +func TestResourceAWSRedshiftSecurityGroupName_validation(t *testing.T) { + cases := []struct { + Value string + ErrCount int + }{ + { + Value: "default", + ErrCount: 1, + }, + { + Value: "testing123%%", + ErrCount: 1, + }, + { + Value: "TestingSG", + ErrCount: 1, + }, + { + Value: randomString(256), + ErrCount: 1, + }, + } + + for _, tc := range cases { + _, errors := validateRedshiftSecurityGroupName(tc.Value, "aws_redshift_security_group_name") + + if len(errors) != tc.ErrCount { + t.Fatalf("Expected the Redshift Security Group Name to trigger a validation error") + } + } +} + +const testAccAWSRedshiftSecurityGroupConfig_ingressCidr = ` +provider "aws" { + region = "us-east-1" +} + +resource "aws_redshift_security_group" "bar" { + name = "redshift-sg-terraform" + description = "this is a description" + + ingress { + cidr = "10.0.0.1/24" + } +}` + +const testAccAWSRedshiftSecurityGroupConfig_ingressSgId = ` +provider "aws" { + region = "us-east-1" +} + +resource "aws_security_group" "redshift" { + name = "terraform_redshift_acceptance_test" + description = "Used in the redshift acceptance tests" + + ingress { + protocol = "tcp" + from_port = 22 + to_port = 22 + cidr_blocks = ["10.0.0.0/8"] + } +} + +resource "aws_redshift_security_group" "bar" { + name = "redshift-sg-terraform" + description = "this is a description" + + ingress { + security_group_name = "${aws_security_group.redshift.name}" + security_group_owner_id = "${aws_security_group.redshift.owner_id}" + } +}` From 249e7df76c0c996289d93ac54d5f7da2c8fdd18e Mon Sep 17 00:00:00 2001 From: stack72 Date: Wed, 11 Nov 2015 23:37:56 +0000 Subject: [PATCH 2/4] Adding the documentation for the Redshift security groups Creation of the schema, CRUD and acceptance tests for Redshift Parameter Group --- builtin/providers/aws/provider.go | 1 + .../resource_aws_redshift_parameter_group.go | 241 ++++++++++++++++++ ...ource_aws_redshift_parameter_group_test.go | 207 +++++++++++++++ .../resource_aws_redshift_security_group.go | 22 +- builtin/providers/aws/structure.go | 36 +++ builtin/providers/aws/structure_test.go | 61 ++++- builtin/providers/aws/tagsRedshift.go | 27 ++ .../r/redshift_security_group.html.markdown | 50 ++++ website/source/layouts/aws.erb | 11 + 9 files changed, 634 insertions(+), 22 deletions(-) create mode 100644 builtin/providers/aws/resource_aws_redshift_parameter_group.go create mode 100644 builtin/providers/aws/resource_aws_redshift_parameter_group_test.go create mode 100644 builtin/providers/aws/tagsRedshift.go create mode 100644 website/source/docs/providers/aws/r/redshift_security_group.html.markdown diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 45912f6cb..d4d44ee7c 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -171,6 +171,7 @@ func Provider() terraform.ResourceProvider { "aws_rds_cluster": resourceAwsRDSCluster(), "aws_rds_cluster_instance": resourceAwsRDSClusterInstance(), "aws_redshift_security_group": resourceAwsRedshiftSecurityGroup(), + "aws_redshift_parameter_group": resourceAwsRedshiftParameterGroup(), "aws_route53_delegation_set": resourceAwsRoute53DelegationSet(), "aws_route53_record": resourceAwsRoute53Record(), "aws_route53_zone_association": resourceAwsRoute53ZoneAssociation(), diff --git a/builtin/providers/aws/resource_aws_redshift_parameter_group.go b/builtin/providers/aws/resource_aws_redshift_parameter_group.go new file mode 100644 index 000000000..4bebdd314 --- /dev/null +++ b/builtin/providers/aws/resource_aws_redshift_parameter_group.go @@ -0,0 +1,241 @@ +package aws + +import ( + "bytes" + "fmt" + "log" + "regexp" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/redshift" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsRedshiftParameterGroup() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsRedshiftParameterGroupCreate, + Read: resourceAwsRedshiftParameterGroupRead, + Update: resourceAwsRedshiftParameterGroupUpdate, + Delete: resourceAwsRedshiftParameterGroupDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + ForceNew: true, + Required: true, + ValidateFunc: validateRedshiftParamGroupName, + }, + + "family": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "description": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "parameter": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: false, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "value": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + }, + Set: resourceAwsRedshiftParameterHash, + }, + + "tags": tagsSchema(), + }, + } +} + +func resourceAwsRedshiftParameterGroupCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).redshiftconn + + createOpts := redshift.CreateClusterParameterGroupInput{ + ParameterGroupName: aws.String(d.Get("name").(string)), + ParameterGroupFamily: aws.String(d.Get("family").(string)), + Description: aws.String(d.Get("description").(string)), + Tags: tagsFromMapRedshift(d.Get("tags").(map[string]interface{})), + } + + log.Printf("[DEBUG] Create Redshift Parameter Group: %#v", createOpts) + _, err := conn.CreateClusterParameterGroup(&createOpts) + if err != nil { + return fmt.Errorf("Error creating Redshift Parameter Group: %s", err) + } + + d.SetId(*createOpts.ParameterGroupName) + log.Printf("[INFO] Redshift Parameter Group ID: %s", d.Id()) + + return resourceAwsRedshiftParameterGroupUpdate(d, meta) +} + +func resourceAwsRedshiftParameterGroupRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).redshiftconn + + describeOpts := redshift.DescribeClusterParameterGroupsInput{ + ParameterGroupName: aws.String(d.Id()), + } + + describeResp, err := conn.DescribeClusterParameterGroups(&describeOpts) + if err != nil { + return err + } + + if len(describeResp.ParameterGroups) != 1 || + *describeResp.ParameterGroups[0].ParameterGroupName != d.Id() { + return fmt.Errorf("Unable to find Parameter Group: %#v", describeResp.ParameterGroups) + } + + d.Set("name", describeResp.ParameterGroups[0].ParameterGroupName) + d.Set("family", describeResp.ParameterGroups[0].ParameterGroupFamily) + d.Set("description", describeResp.ParameterGroups[0].Description) + d.Set("tags", tagsToMapRedshift(describeResp.ParameterGroups[0].Tags)) + + describeParametersOpts := redshift.DescribeClusterParametersInput{ + ParameterGroupName: aws.String(d.Id()), + Source: aws.String("user"), + } + + describeParametersResp, err := conn.DescribeClusterParameters(&describeParametersOpts) + if err != nil { + return err + } + + d.Set("parameter", flattenRedshiftParameters(describeParametersResp.Parameters)) + return nil +} + +func resourceAwsRedshiftParameterGroupUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).redshiftconn + + d.Partial(true) + + if d.HasChange("parameter") { + o, n := d.GetChange("parameter") + if o == nil { + o = new(schema.Set) + } + if n == nil { + n = new(schema.Set) + } + + os := o.(*schema.Set) + ns := n.(*schema.Set) + + // Expand the "parameter" set to aws-sdk-go compat []redshift.Parameter + parameters, err := expandRedshiftParameters(ns.Difference(os).List()) + if err != nil { + return err + } + + if len(parameters) > 0 { + modifyOpts := redshift.ModifyClusterParameterGroupInput{ + ParameterGroupName: aws.String(d.Get("name").(string)), + Parameters: parameters, + } + + log.Printf("[DEBUG] Modify Redshift Parameter Group: %s", modifyOpts) + _, err = conn.ModifyClusterParameterGroup(&modifyOpts) + if err != nil { + return fmt.Errorf("Error modifying Redshift Parameter Group: %s", err) + } + } + d.SetPartial("parameter") + } + + d.Partial(false) + return resourceAwsRedshiftParameterGroupRead(d, meta) +} + +func resourceAwsRedshiftParameterGroupDelete(d *schema.ResourceData, meta interface{}) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{"pending"}, + Target: "destroyed", + Refresh: resourceAwsRedshiftParameterGroupDeleteRefreshFunc(d, meta), + Timeout: 3 * time.Minute, + MinTimeout: 1 * time.Second, + } + _, err := stateConf.WaitForState() + return err +} + +func resourceAwsRedshiftParameterGroupDeleteRefreshFunc( + d *schema.ResourceData, + meta interface{}) resource.StateRefreshFunc { + conn := meta.(*AWSClient).redshiftconn + + return func() (interface{}, string, error) { + + deleteOpts := redshift.DeleteClusterParameterGroupInput{ + ParameterGroupName: aws.String(d.Id()), + } + + if _, err := conn.DeleteClusterParameterGroup(&deleteOpts); err != nil { + redshiftErr, ok := err.(awserr.Error) + if !ok { + return d, "error", err + } + + if redshiftErr.Code() != "RedshiftParameterGroupNotFoundFault" { + return d, "error", err + } + } + + return d, "destroyed", nil + } +} + +func resourceAwsRedshiftParameterHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["name"].(string))) + // Store the value as a lower case string, to match how we store them in flattenParameters + buf.WriteString(fmt.Sprintf("%s-", strings.ToLower(m["value"].(string)))) + + return hashcode.String(buf.String()) +} + +func validateRedshiftParamGroupName(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if !regexp.MustCompile(`^[0-9a-z-]+$`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "only lowercase alphanumeric characters and hyphens allowed in %q", k)) + } + if !regexp.MustCompile(`^[a-z]`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "first character of %q must be a letter", k)) + } + if regexp.MustCompile(`--`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q cannot contain two consecutive hyphens", k)) + } + if regexp.MustCompile(`-$`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q cannot end with a hyphen", k)) + } + if len(value) > 255 { + errors = append(errors, fmt.Errorf( + "%q cannot be greater than 255 characters", k)) + } + return +} diff --git a/builtin/providers/aws/resource_aws_redshift_parameter_group_test.go b/builtin/providers/aws/resource_aws_redshift_parameter_group_test.go new file mode 100644 index 000000000..e5139d288 --- /dev/null +++ b/builtin/providers/aws/resource_aws_redshift_parameter_group_test.go @@ -0,0 +1,207 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/redshift" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSRedshiftParameterGroup_withParameters(t *testing.T) { + var v redshift.ClusterParameterGroup + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRedshiftParameterGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSRedshiftParameterGroupConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRedshiftParameterGroupExists("aws_redshift_parameter_group.bar", &v), + resource.TestCheckResourceAttr( + "aws_redshift_parameter_group.bar", "name", "parameter-group-test-terraform"), + resource.TestCheckResourceAttr( + "aws_redshift_parameter_group.bar", "family", "redshift-1.0"), + resource.TestCheckResourceAttr( + "aws_redshift_parameter_group.bar", "description", "Test parameter group for terraform"), + resource.TestCheckResourceAttr( + "aws_redshift_parameter_group.bar", "parameter.490804664.name", "require_ssl"), + resource.TestCheckResourceAttr( + "aws_redshift_parameter_group.bar", "parameter.490804664.value", "true"), + resource.TestCheckResourceAttr( + "aws_redshift_parameter_group.bar", "parameter.2036118857.name", "query_group"), + resource.TestCheckResourceAttr( + "aws_redshift_parameter_group.bar", "parameter.2036118857.value", "example"), + resource.TestCheckResourceAttr( + "aws_redshift_parameter_group.bar", "parameter.484080973.name", "enable_user_activity_logging"), + resource.TestCheckResourceAttr( + "aws_redshift_parameter_group.bar", "parameter.484080973.value", "true"), + ), + }, + }, + }) +} + +func TestAccAWSRedshiftParameterGroup_withoutParameters(t *testing.T) { + var v redshift.ClusterParameterGroup + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRedshiftParameterGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSRedshiftParameterGroupOnlyConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRedshiftParameterGroupExists("aws_redshift_parameter_group.bar", &v), + resource.TestCheckResourceAttr( + "aws_redshift_parameter_group.bar", "name", "parameter-group-test-terraform"), + resource.TestCheckResourceAttr( + "aws_redshift_parameter_group.bar", "family", "redshift-1.0"), + resource.TestCheckResourceAttr( + "aws_redshift_parameter_group.bar", "description", "Test parameter group for terraform"), + ), + }, + }, + }) +} + +func TestResourceAWSRedshiftParameterGroupName_validation(t *testing.T) { + cases := []struct { + Value string + ErrCount int + }{ + { + Value: "tEsting123", + ErrCount: 1, + }, + { + Value: "testing123!", + ErrCount: 1, + }, + { + Value: "1testing123", + ErrCount: 1, + }, + { + Value: "testing--123", + ErrCount: 1, + }, + { + Value: "testing123-", + ErrCount: 1, + }, + { + Value: randomString(256), + ErrCount: 1, + }, + } + + for _, tc := range cases { + _, errors := validateRedshiftParamGroupName(tc.Value, "aws_redshift_parameter_group_name") + + if len(errors) != tc.ErrCount { + t.Fatalf("Expected the Redshift Parameter Group Name to trigger a validation error") + } + } +} + +func testAccCheckAWSRedshiftParameterGroupDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).redshiftconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_redshift_parameter_group" { + continue + } + + // Try to find the Group + resp, err := conn.DescribeClusterParameterGroups( + &redshift.DescribeClusterParameterGroupsInput{ + ParameterGroupName: aws.String(rs.Primary.ID), + }) + + if err == nil { + if len(resp.ParameterGroups) != 0 && + *resp.ParameterGroups[0].ParameterGroupName == rs.Primary.ID { + return fmt.Errorf("Redshift Parameter Group still exists") + } + } + + // Verify the error + newerr, ok := err.(awserr.Error) + if !ok { + return err + } + if newerr.Code() != "InvalidRedshiftParameterGroup.NotFound" { + return err + } + } + + return nil +} + +func testAccCheckAWSRedshiftParameterGroupExists(n string, v *redshift.ClusterParameterGroup) 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 Parameter Group ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).redshiftconn + + opts := redshift.DescribeClusterParameterGroupsInput{ + ParameterGroupName: aws.String(rs.Primary.ID), + } + + resp, err := conn.DescribeClusterParameterGroups(&opts) + + if err != nil { + return err + } + + if len(resp.ParameterGroups) != 1 || + *resp.ParameterGroups[0].ParameterGroupName != rs.Primary.ID { + return fmt.Errorf("Redshift Parameter Group not found") + } + + *v = *resp.ParameterGroups[0] + + return nil + } +} + +const testAccAWSRedshiftParameterGroupOnlyConfig = ` +resource "aws_redshift_parameter_group" "bar" { + name = "parameter-group-test-terraform" + family = "redshift-1.0" + description = "Test parameter group for terraform" +}` + +const testAccAWSRedshiftParameterGroupConfig = ` +resource "aws_redshift_parameter_group" "bar" { + name = "parameter-group-test-terraform" + family = "redshift-1.0" + description = "Test parameter group for terraform" + parameter { + name = "require_ssl" + value = "true" + } + parameter { + name = "query_group" + value = "example" + } + parameter{ + name = "enable_user_activity_logging" + value = "true" + } +} +` diff --git a/builtin/providers/aws/resource_aws_redshift_security_group.go b/builtin/providers/aws/resource_aws_redshift_security_group.go index 9f2520d15..0e09eb7c4 100644 --- a/builtin/providers/aws/resource_aws_redshift_security_group.go +++ b/builtin/providers/aws/resource_aws_redshift_security_group.go @@ -154,6 +154,7 @@ func resourceAwsRedshiftSecurityGroupRead(d *schema.ResourceData, meta interface d.Set("ingress", rules) d.Set("name", *sg.ClusterSecurityGroupName) d.Set("description", *sg.Description) + d.Set("tags", tagsToMapRedshift(sg.Tags)) return nil } @@ -203,27 +204,6 @@ func resourceAwsRedshiftSecurityGroupRetrieve(d *schema.ResourceData, meta inter return resp.ClusterSecurityGroups[0], nil } -func tagsFromMapRedshift(m map[string]interface{}) []*redshift.Tag { - result := make([]*redshift.Tag, 0, len(m)) - for k, v := range m { - result = append(result, &redshift.Tag{ - Key: aws.String(k), - Value: aws.String(v.(string)), - }) - } - - return result -} - -func tagsToMapRedshift(ts []*redshift.Tag) map[string]string { - result := make(map[string]string) - for _, t := range ts { - result[*t.Key] = *t.Value - } - - return result -} - func validateRedshiftSecurityGroupName(v interface{}, k string) (ws []string, errors []error) { value := v.(string) if value == "default" { diff --git a/builtin/providers/aws/structure.go b/builtin/providers/aws/structure.go index 748ecc88b..e82d3d961 100644 --- a/builtin/providers/aws/structure.go +++ b/builtin/providers/aws/structure.go @@ -17,6 +17,7 @@ import ( elasticsearch "github.com/aws/aws-sdk-go/service/elasticsearchservice" "github.com/aws/aws-sdk-go/service/elb" "github.com/aws/aws-sdk-go/service/rds" + "github.com/aws/aws-sdk-go/service/redshift" "github.com/aws/aws-sdk-go/service/route53" "github.com/hashicorp/terraform/helper/schema" ) @@ -233,6 +234,29 @@ func expandParameters(configured []interface{}) ([]*rds.Parameter, error) { return parameters, nil } +func expandRedshiftParameters(configured []interface{}) ([]*redshift.Parameter, error) { + var parameters []*redshift.Parameter + + // Loop over our configured parameters and create + // an array of aws-sdk-go compatabile objects + for _, pRaw := range configured { + data := pRaw.(map[string]interface{}) + + if data["name"].(string) == "" { + continue + } + + p := &redshift.Parameter{ + ParameterName: aws.String(data["name"].(string)), + ParameterValue: aws.String(data["value"].(string)), + } + + parameters = append(parameters, p) + } + + return parameters, nil +} + // Takes the result of flatmap.Expand for an array of parameters and // returns Parameter API compatible objects func expandElastiCacheParameters(configured []interface{}) ([]*elasticache.ParameterNameValue, error) { @@ -413,6 +437,18 @@ func flattenParameters(list []*rds.Parameter) []map[string]interface{} { return result } +// Flattens an array of Redshift Parameters into a []map[string]interface{} +func flattenRedshiftParameters(list []*redshift.Parameter) []map[string]interface{} { + result := make([]map[string]interface{}, 0, len(list)) + for _, i := range list { + result = append(result, map[string]interface{}{ + "name": strings.ToLower(*i.ParameterName), + "value": strings.ToLower(*i.ParameterValue), + }) + } + return result +} + // Flattens an array of Parameters into a []map[string]interface{} func flattenElastiCacheParameters(list []*elasticache.Parameter) []map[string]interface{} { result := make([]map[string]interface{}, 0, len(list)) diff --git a/builtin/providers/aws/structure_test.go b/builtin/providers/aws/structure_test.go index 8e41b631f..f74911b50 100644 --- a/builtin/providers/aws/structure_test.go +++ b/builtin/providers/aws/structure_test.go @@ -10,6 +10,7 @@ import ( "github.com/aws/aws-sdk-go/service/elasticache" "github.com/aws/aws-sdk-go/service/elb" "github.com/aws/aws-sdk-go/service/rds" + "github.com/aws/aws-sdk-go/service/redshift" "github.com/aws/aws-sdk-go/service/route53" "github.com/hashicorp/terraform/flatmap" "github.com/hashicorp/terraform/helper/schema" @@ -426,7 +427,36 @@ func TestExpandParameters(t *testing.T) { } } +<<<<<<< HEAD func TestExpandElasticacheParameters(t *testing.T) { +======= +func TestexpandRedshiftParameters(t *testing.T) { + expanded := []interface{}{ + map[string]interface{}{ + "name": "character_set_client", + "value": "utf8", + }, + } + parameters, err := expandRedshiftParameters(expanded) + if err != nil { + t.Fatalf("bad: %#v", err) + } + + expected := &redshift.Parameter{ + ParameterName: aws.String("character_set_client"), + ParameterValue: aws.String("utf8"), + } + + if !reflect.DeepEqual(parameters[0], expected) { + t.Fatalf( + "Got:\n\n%#v\n\nExpected:\n\n%#v\n", + parameters[0], + expected) + } +} + +func TestexpandElasticacheParameters(t *testing.T) { +>>>>>>> Creation of the schema, CRUD and acceptance tests for Redshift Parameter Group expanded := []interface{}{ map[string]interface{}{ "name": "activerehashing", @@ -481,7 +511,36 @@ func TestFlattenParameters(t *testing.T) { } } -func TestFlattenElasticacheParameters(t *testing.T) { +func TestflattenRedshiftParameters(t *testing.T) { + cases := []struct { + Input []*redshift.Parameter + Output []map[string]interface{} + }{ + { + Input: []*redshift.Parameter{ + &redshift.Parameter{ + ParameterName: aws.String("character_set_client"), + ParameterValue: aws.String("utf8"), + }, + }, + Output: []map[string]interface{}{ + map[string]interface{}{ + "name": "character_set_client", + "value": "utf8", + }, + }, + }, + } + + for _, tc := range cases { + output := flattenRedshiftParameters(tc.Input) + if !reflect.DeepEqual(output, tc.Output) { + t.Fatalf("Got:\n\n%#v\n\nExpected:\n\n%#v", output, tc.Output) + } + } +} + +func TestflattenElasticacheParameters(t *testing.T) { cases := []struct { Input []*elasticache.Parameter Output []map[string]interface{} diff --git a/builtin/providers/aws/tagsRedshift.go b/builtin/providers/aws/tagsRedshift.go new file mode 100644 index 000000000..06d6fda23 --- /dev/null +++ b/builtin/providers/aws/tagsRedshift.go @@ -0,0 +1,27 @@ +package aws + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/redshift" +) + +func tagsFromMapRedshift(m map[string]interface{}) []*redshift.Tag { + result := make([]*redshift.Tag, 0, len(m)) + for k, v := range m { + result = append(result, &redshift.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + }) + } + + return result +} + +func tagsToMapRedshift(ts []*redshift.Tag) map[string]string { + result := make(map[string]string) + for _, t := range ts { + result[*t.Key] = *t.Value + } + + return result +} diff --git a/website/source/docs/providers/aws/r/redshift_security_group.html.markdown b/website/source/docs/providers/aws/r/redshift_security_group.html.markdown new file mode 100644 index 000000000..ec8a67ae1 --- /dev/null +++ b/website/source/docs/providers/aws/r/redshift_security_group.html.markdown @@ -0,0 +1,50 @@ +--- +layout: "aws" +page_title: "AWS: aws_redshift_security_group" +sidebar_current: "docs-aws-resource-redshift-security-group" +description: |- + Provides a Redshift security group resource. +--- + +# aws\_redshift\_security\_group + +Creates a new Amazon Redshift security group. You use security groups to control access to non-VPC clusters + +## Example Usage + +``` +resource "aws_redshift_security_group" "default" { + name = "redshift_sg" + description = "Redshift Example security group" + + ingress { + cidr = "10.0.0.0/24" + } + + tags { + Environment = "test" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the Redshift security group. +* `description` - (Required) The description of the Redshift security group. +* `ingress` - (Optional) A list of ingress rules. + +Ingress blocks support the following: + +* `cidr` - The CIDR block to accept +* `security_group_name` - The name of the security group to authorize +* `security_group_owner_id` - The owner Id of the security group provided + by `security_group_name`. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The Redshift security group ID. + diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 5a12b432e..499b2148d 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -433,6 +433,17 @@ + > + Redshift Resources + + + > Route53 Resources From 48091e37c7ea7a73990ca0b1b885bb10045cfb80 Mon Sep 17 00:00:00 2001 From: stack72 Date: Thu, 12 Nov 2015 01:10:57 +0000 Subject: [PATCH 3/4] Adding the documentation for the Redshift Parameter Groups Changed the aws_redshift_security_group and aws_redshift_parameter_group to remove the tags from the schema. Tags are a little bit more complicated than originally though - I will revisit this later Then added the schema, CRUD functionality and basic acceptance tests for aws_redshift_subnet_group Adding an acceptance test for the Update of subnet_ids in AWS Redshift Subnet Group --- builtin/providers/aws/provider.go | 1 + .../resource_aws_redshift_parameter_group.go | 5 +- .../resource_aws_redshift_security_group.go | 9 - .../aws/resource_aws_redshift_subnet_group.go | 186 +++++++++++++++ ...resource_aws_redshift_subnet_group_test.go | 220 ++++++++++++++++++ .../r/redshift_parameter_group.html.markdown | 57 +++++ website/source/layouts/aws.erb | 4 + 7 files changed, 469 insertions(+), 13 deletions(-) create mode 100644 builtin/providers/aws/resource_aws_redshift_subnet_group.go create mode 100644 builtin/providers/aws/resource_aws_redshift_subnet_group_test.go create mode 100644 website/source/docs/providers/aws/r/redshift_parameter_group.html.markdown diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index d4d44ee7c..df1b77378 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -172,6 +172,7 @@ func Provider() terraform.ResourceProvider { "aws_rds_cluster_instance": resourceAwsRDSClusterInstance(), "aws_redshift_security_group": resourceAwsRedshiftSecurityGroup(), "aws_redshift_parameter_group": resourceAwsRedshiftParameterGroup(), + "aws_redshift_subnet_group": resourceAwsRedshiftSubnetGroup(), "aws_route53_delegation_set": resourceAwsRoute53DelegationSet(), "aws_route53_record": resourceAwsRoute53Record(), "aws_route53_zone_association": resourceAwsRoute53ZoneAssociation(), diff --git a/builtin/providers/aws/resource_aws_redshift_parameter_group.go b/builtin/providers/aws/resource_aws_redshift_parameter_group.go index 4bebdd314..54336d439 100644 --- a/builtin/providers/aws/resource_aws_redshift_parameter_group.go +++ b/builtin/providers/aws/resource_aws_redshift_parameter_group.go @@ -61,8 +61,6 @@ func resourceAwsRedshiftParameterGroup() *schema.Resource { }, Set: resourceAwsRedshiftParameterHash, }, - - "tags": tagsSchema(), }, } } @@ -74,7 +72,6 @@ func resourceAwsRedshiftParameterGroupCreate(d *schema.ResourceData, meta interf ParameterGroupName: aws.String(d.Get("name").(string)), ParameterGroupFamily: aws.String(d.Get("family").(string)), Description: aws.String(d.Get("description").(string)), - Tags: tagsFromMapRedshift(d.Get("tags").(map[string]interface{})), } log.Printf("[DEBUG] Create Redshift Parameter Group: %#v", createOpts) @@ -103,13 +100,13 @@ func resourceAwsRedshiftParameterGroupRead(d *schema.ResourceData, meta interfac if len(describeResp.ParameterGroups) != 1 || *describeResp.ParameterGroups[0].ParameterGroupName != d.Id() { + d.SetId("") return fmt.Errorf("Unable to find Parameter Group: %#v", describeResp.ParameterGroups) } d.Set("name", describeResp.ParameterGroups[0].ParameterGroupName) d.Set("family", describeResp.ParameterGroups[0].ParameterGroupFamily) d.Set("description", describeResp.ParameterGroups[0].Description) - d.Set("tags", tagsToMapRedshift(describeResp.ParameterGroups[0].Tags)) describeParametersOpts := redshift.DescribeClusterParametersInput{ ParameterGroupName: aws.String(d.Id()), diff --git a/builtin/providers/aws/resource_aws_redshift_security_group.go b/builtin/providers/aws/resource_aws_redshift_security_group.go index 0e09eb7c4..e21ccc5df 100644 --- a/builtin/providers/aws/resource_aws_redshift_security_group.go +++ b/builtin/providers/aws/resource_aws_redshift_security_group.go @@ -62,12 +62,6 @@ func resourceAwsRedshiftSecurityGroup() *schema.Resource { }, Set: resourceAwsRedshiftSecurityGroupIngressHash, }, - - "tags": &schema.Schema{ - Type: schema.TypeMap, - Optional: true, - ForceNew: true, - }, }, } } @@ -80,11 +74,9 @@ func resourceAwsRedshiftSecurityGroupCreate(d *schema.ResourceData, meta interfa name := d.Get("name").(string) desc := d.Get("description").(string) - tags := tagsFromMapRedshift(d.Get("tags").(map[string]interface{})) sgInput := &redshift.CreateClusterSecurityGroupInput{ ClusterSecurityGroupName: aws.String(name), Description: aws.String(desc), - Tags: tags, } log.Printf("[DEBUG] Redshift security group create: name: %s, description: %s", name, desc) _, err = conn.CreateClusterSecurityGroup(sgInput) @@ -154,7 +146,6 @@ func resourceAwsRedshiftSecurityGroupRead(d *schema.ResourceData, meta interface d.Set("ingress", rules) d.Set("name", *sg.ClusterSecurityGroupName) d.Set("description", *sg.Description) - d.Set("tags", tagsToMapRedshift(sg.Tags)) return nil } diff --git a/builtin/providers/aws/resource_aws_redshift_subnet_group.go b/builtin/providers/aws/resource_aws_redshift_subnet_group.go new file mode 100644 index 000000000..878cd727e --- /dev/null +++ b/builtin/providers/aws/resource_aws_redshift_subnet_group.go @@ -0,0 +1,186 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/redshift" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsRedshiftSubnetGroup() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsRedshiftSubnetGroupCreate, + Read: resourceAwsRedshiftSubnetGroupRead, + Update: resourceAwsRedshiftSubnetGroupUpdate, + Delete: resourceAwsRedshiftSubnetGroupDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + ForceNew: true, + Required: true, + ValidateFunc: validateRedshiftSubnetGroupName, + }, + + "description": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "subnet_ids": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + } +} + +func resourceAwsRedshiftSubnetGroupCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).redshiftconn + + subnetIdsSet := d.Get("subnet_ids").(*schema.Set) + subnetIds := make([]*string, subnetIdsSet.Len()) + for i, subnetId := range subnetIdsSet.List() { + subnetIds[i] = aws.String(subnetId.(string)) + } + + createOpts := redshift.CreateClusterSubnetGroupInput{ + ClusterSubnetGroupName: aws.String(d.Get("name").(string)), + Description: aws.String(d.Get("description").(string)), + SubnetIds: subnetIds, + } + + log.Printf("[DEBUG] Create Redshift Subnet Group: %#v", createOpts) + _, err := conn.CreateClusterSubnetGroup(&createOpts) + if err != nil { + return fmt.Errorf("Error creating Redshift Subnet Group: %s", err) + } + + d.SetId(*createOpts.ClusterSubnetGroupName) + log.Printf("[INFO] Redshift Subnet Group ID: %s", d.Id()) + return resourceAwsRedshiftSubnetGroupRead(d, meta) +} + +func resourceAwsRedshiftSubnetGroupRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).redshiftconn + + describeOpts := redshift.DescribeClusterSubnetGroupsInput{ + ClusterSubnetGroupName: aws.String(d.Id()), + } + + describeResp, err := conn.DescribeClusterSubnetGroups(&describeOpts) + if err != nil { + if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "ClusterSubnetGroupNotFoundFault" { + log.Printf("[INFO] Redshift Subnet Group: %s was not found", d.Id()) + d.SetId("") + return nil + } + return err + } + + if len(describeResp.ClusterSubnetGroups) == 0 { + return fmt.Errorf("Unable to find Redshift Subnet Group: %#v", describeResp.ClusterSubnetGroups) + } + + d.Set("name", d.Id()) + d.Set("description", describeResp.ClusterSubnetGroups[0].Description) + d.Set("subnet_ids", subnetIdsToSlice(describeResp.ClusterSubnetGroups[0].Subnets)) + + return nil +} + +func resourceAwsRedshiftSubnetGroupUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).redshiftconn + if d.HasChange("subnet_ids") { + _, n := d.GetChange("subnet_ids") + if n == nil { + n = new(schema.Set) + } + ns := n.(*schema.Set) + + var sIds []*string + for _, s := range ns.List() { + sIds = append(sIds, aws.String(s.(string))) + } + + _, err := conn.ModifyClusterSubnetGroup(&redshift.ModifyClusterSubnetGroupInput{ + ClusterSubnetGroupName: aws.String(d.Id()), + SubnetIds: sIds, + }) + + if err != nil { + return err + } + } + + return nil +} + +func resourceAwsRedshiftSubnetGroupDelete(d *schema.ResourceData, meta interface{}) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{"pending"}, + Target: "destroyed", + Refresh: resourceAwsRedshiftSubnetGroupDeleteRefreshFunc(d, meta), + Timeout: 3 * time.Minute, + MinTimeout: 1 * time.Second, + } + _, err := stateConf.WaitForState() + return err +} + +func resourceAwsRedshiftSubnetGroupDeleteRefreshFunc(d *schema.ResourceData, meta interface{}) resource.StateRefreshFunc { + conn := meta.(*AWSClient).redshiftconn + + return func() (interface{}, string, error) { + + deleteOpts := redshift.DeleteClusterSubnetGroupInput{ + ClusterSubnetGroupName: aws.String(d.Id()), + } + + if _, err := conn.DeleteClusterSubnetGroup(&deleteOpts); err != nil { + redshiftErr, ok := err.(awserr.Error) + if !ok { + return d, "error", err + } + + if redshiftErr.Code() != "ClusterSubnetGroupNotFoundFault" { + return d, "error", err + } + } + + return d, "destroyed", nil + } +} + +func subnetIdsToSlice(subnetIds []*redshift.Subnet) []string { + subnetsSlice := make([]string, 0, len(subnetIds)) + for _, s := range subnetIds { + subnetsSlice = append(subnetsSlice, *s.SubnetIdentifier) + } + return subnetsSlice +} + +func validateRedshiftSubnetGroupName(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if !regexp.MustCompile(`^[0-9a-z-_]+$`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "only lowercase alphanumeric characters, hyphens, underscores, and periods allowed in %q", k)) + } + if len(value) > 255 { + errors = append(errors, fmt.Errorf( + "%q cannot be longer than 255 characters", k)) + } + if regexp.MustCompile(`(?i)^default$`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q is not allowed as %q", "Default", k)) + } + return +} diff --git a/builtin/providers/aws/resource_aws_redshift_subnet_group_test.go b/builtin/providers/aws/resource_aws_redshift_subnet_group_test.go new file mode 100644 index 000000000..ba69c4f40 --- /dev/null +++ b/builtin/providers/aws/resource_aws_redshift_subnet_group_test.go @@ -0,0 +1,220 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/redshift" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSRedshiftSubnetGroup_basic(t *testing.T) { + var v redshift.ClusterSubnetGroup + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckRedshiftSubnetGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccRedshiftSubnetGroupConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckRedshiftSubnetGroupExists("aws_redshift_subnet_group.foo", &v), + resource.TestCheckResourceAttr( + "aws_redshift_subnet_group.foo", "subnet_ids.#", "2"), + ), + }, + }, + }) +} + +func TestAccAWSRedshiftSubnetGroup_updateSubnetIds(t *testing.T) { + var v redshift.ClusterSubnetGroup + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckRedshiftSubnetGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccRedshiftSubnetGroupConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckRedshiftSubnetGroupExists("aws_redshift_subnet_group.foo", &v), + resource.TestCheckResourceAttr( + "aws_redshift_subnet_group.foo", "subnet_ids.#", "2"), + ), + }, + + resource.TestStep{ + Config: testAccRedshiftSubnetGroupConfig_updateSubnetIds, + Check: resource.ComposeTestCheckFunc( + testAccCheckRedshiftSubnetGroupExists("aws_redshift_subnet_group.foo", &v), + resource.TestCheckResourceAttr( + "aws_redshift_subnet_group.foo", "subnet_ids.#", "3"), + ), + }, + }, + }) +} + +func TestResourceAWSRedshiftSubnetGroupName_validation(t *testing.T) { + cases := []struct { + Value string + ErrCount int + }{ + { + Value: "default", + ErrCount: 1, + }, + { + Value: "testing123%%", + ErrCount: 1, + }, + { + Value: "TestingSG", + ErrCount: 1, + }, + { + Value: randomString(256), + ErrCount: 1, + }, + } + + for _, tc := range cases { + _, errors := validateRedshiftSubnetGroupName(tc.Value, "aws_redshift_subnet_group_name") + + if len(errors) != tc.ErrCount { + t.Fatalf("Expected the Redshift Subnet Group Name to trigger a validation error") + } + } +} + +func testAccCheckRedshiftSubnetGroupDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).redshiftconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_redshift_subnet_group" { + continue + } + + resp, err := conn.DescribeClusterSubnetGroups( + &redshift.DescribeClusterSubnetGroupsInput{ + ClusterSubnetGroupName: aws.String(rs.Primary.ID)}) + if err == nil { + if len(resp.ClusterSubnetGroups) > 0 { + return fmt.Errorf("still exist.") + } + + return nil + } + + redshiftErr, ok := err.(awserr.Error) + if !ok { + return err + } + if redshiftErr.Code() != "ClusterSubnetGroupNotFoundFault" { + return err + } + } + + return nil +} + +func testAccCheckRedshiftSubnetGroupExists(n string, v *redshift.ClusterSubnetGroup) 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 ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).redshiftconn + resp, err := conn.DescribeClusterSubnetGroups( + &redshift.DescribeClusterSubnetGroupsInput{ClusterSubnetGroupName: aws.String(rs.Primary.ID)}) + if err != nil { + return err + } + if len(resp.ClusterSubnetGroups) == 0 { + return fmt.Errorf("ClusterSubnetGroup not found") + } + + *v = *resp.ClusterSubnetGroups[0] + + return nil + } +} + +const testAccRedshiftSubnetGroupConfig = ` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_subnet" "foo" { + cidr_block = "10.1.1.0/24" + availability_zone = "us-west-2a" + vpc_id = "${aws_vpc.foo.id}" + tags { + Name = "tf-dbsubnet-test-1" + } +} + +resource "aws_subnet" "bar" { + cidr_block = "10.1.2.0/24" + availability_zone = "us-west-2b" + vpc_id = "${aws_vpc.foo.id}" + tags { + Name = "tf-dbsubnet-test-2" + } +} + +resource "aws_redshift_subnet_group" "foo" { + name = "foo" + description = "foo description" + subnet_ids = ["${aws_subnet.foo.id}", "${aws_subnet.bar.id}"] +} +` + +const testAccRedshiftSubnetGroupConfig_updateSubnetIds = ` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_subnet" "foo" { + cidr_block = "10.1.1.0/24" + availability_zone = "us-west-2a" + vpc_id = "${aws_vpc.foo.id}" + tags { + Name = "tf-dbsubnet-test-1" + } +} + +resource "aws_subnet" "bar" { + cidr_block = "10.1.2.0/24" + availability_zone = "us-west-2b" + vpc_id = "${aws_vpc.foo.id}" + tags { + Name = "tf-dbsubnet-test-2" + } +} + +resource "aws_subnet" "foobar" { + cidr_block = "10.1.3.0/24" + availability_zone = "us-west-2c" + vpc_id = "${aws_vpc.foo.id}" + tags { + Name = "tf-dbsubnet-test-3" + } +} + +resource "aws_redshift_subnet_group" "foo" { + name = "foo" + description = "foo description" + subnet_ids = ["${aws_subnet.foo.id}", "${aws_subnet.bar.id}", "${aws_subnet.foobar.id}"] +} +` diff --git a/website/source/docs/providers/aws/r/redshift_parameter_group.html.markdown b/website/source/docs/providers/aws/r/redshift_parameter_group.html.markdown new file mode 100644 index 000000000..0974ee6f4 --- /dev/null +++ b/website/source/docs/providers/aws/r/redshift_parameter_group.html.markdown @@ -0,0 +1,57 @@ +--- +layout: "aws" +page_title: "AWS: aws_redshift_parameter_group" +sidebar_current: "docs-aws-resource-redshift-parameter-group" +--- + +# aws\_redshift\_parameter\_group + +Provides an Redshift Cluster parameter group resource. + +## Example Usage + +``` +resource "aws_redshift_parameter_group" "bar" { + name = "parameter-group-test-terraform" + family = "redshift-1.0" + description = "Test parameter group for terraform" + parameter { + name = "require_ssl" + value = "true" + } + parameter { + name = "query_group" + value = "example" + } + parameter{ + name = "enable_user_activity_logging" + value = "true" + } + + tags { + Environment = "test" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the Redshift parameter group. +* `family` - (Required) The family of the Redshift parameter group. +* `description` - (Required) The description of the Redshift parameter group. +* `parameter` - (Optional) A list of Redshift parameters to apply. + +Parameter blocks support the following: + +* `name` - (Required) The name of the Redshift parameter. +* `value` - (Required) The value of the Redshift parameter. + +You can read more about the parameters that Redshift supports in the [documentation](http://docs.aws.amazon.com/redshift/latest/mgmt/working-with-parameter-groups.html) + +## Attributes Reference + +The following attributes are exported: + +* `id` - The Redshift parameter group name. diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 499b2148d..81d2d0f62 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -437,6 +437,10 @@ Redshift Resources