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}" + } +}`