From 7e0a340baf0226ea63691262c67c3cef62a6d6da Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Thu, 11 Jun 2015 14:57:43 -0700 Subject: [PATCH 01/20] Consider security groups with source security groups when hashing Previously they would conflict you had multiple security group rules with the same ingress or egress ports but different source security groups because only the CIDR blocks were considered (which are empty when using source security groups). Updated to include migrations (from clint@ctshryock.com) Signed-off-by: Clint Shryock --- .../aws/resource_aws_security_group_rule.go | 64 ++++++++-- ...esource_aws_security_group_rule_migrate.go | 118 ++++++++++++++++++ .../resource_aws_security_group_rule_test.go | 44 ++++++- 3 files changed, 212 insertions(+), 14 deletions(-) create mode 100644 builtin/providers/aws/resource_aws_security_group_rule_migrate.go diff --git a/builtin/providers/aws/resource_aws_security_group_rule.go b/builtin/providers/aws/resource_aws_security_group_rule.go index 270b41ed24..45ecd2c6aa 100644 --- a/builtin/providers/aws/resource_aws_security_group_rule.go +++ b/builtin/providers/aws/resource_aws_security_group_rule.go @@ -21,6 +21,9 @@ func resourceAwsSecurityGroupRule() *schema.Resource { Read: resourceAwsSecurityGroupRuleRead, Delete: resourceAwsSecurityGroupRuleDelete, + SchemaVersion: 1, + MigrateState: resourceAwsSecurityGroupRuleMigrateState, + Schema: map[string]*schema.Schema{ "type": &schema.Schema{ Type: schema.TypeString, @@ -48,10 +51,11 @@ func resourceAwsSecurityGroupRule() *schema.Resource { }, "cidr_blocks": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - ForceNew: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + ConflictsWith: []string{"source_security_group_id"}, }, "security_group_id": &schema.Schema{ @@ -61,10 +65,11 @@ func resourceAwsSecurityGroupRule() *schema.Resource { }, "source_security_group_id": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Computed: true, + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + ConflictsWith: []string{"cidr_blocks"}, }, "self": &schema.Schema{ @@ -263,15 +268,32 @@ func findResourceSecurityGroup(conn *ec2.EC2, id string) (*ec2.SecurityGroup, er return resp.SecurityGroups[0], nil } +// byUserIDAndGroup implements sort.Interface for []*ec2.UserIDGroupPairs based on +// UserID and then the GroupID or GroupName field (only one should be set). +type byUserIDAndGroup []*ec2.UserIDGroupPair + +func (b byUserIDAndGroup) Len() int { return len(b) } +func (b byUserIDAndGroup) Swap(i, j int) { b[i], b[j] = b[j], b[i] } +func (b byUserIDAndGroup) Less(i, j int) bool { + if b[i].GroupID != nil && b[j].GroupID != nil { + return *b[i].GroupID < *b[j].GroupID + } + if b[i].GroupName != nil && b[j].GroupName != nil { + return *b[i].GroupName < *b[j].GroupName + } + + panic("mismatched security group rules, may be a terraform bug") +} + func ipPermissionIDHash(ruleType string, ip *ec2.IPPermission) string { var buf bytes.Buffer - // for egress rules, an TCP rule of -1 is automatically added, in which case - // the to and from ports will be nil. We don't record this rule locally. - if ip.IPProtocol != nil && *ip.IPProtocol != "-1" { + if ip.FromPort != nil && *ip.FromPort > 0 { buf.WriteString(fmt.Sprintf("%d-", *ip.FromPort)) - buf.WriteString(fmt.Sprintf("%d-", *ip.ToPort)) - buf.WriteString(fmt.Sprintf("%s-", *ip.IPProtocol)) } + if ip.ToPort != nil && *ip.ToPort > 0 { + buf.WriteString(fmt.Sprintf("%d-", *ip.ToPort)) + } + buf.WriteString(fmt.Sprintf("%s-", *ip.IPProtocol)) buf.WriteString(fmt.Sprintf("%s-", ruleType)) // We need to make sure to sort the strings below so that we always @@ -288,6 +310,22 @@ func ipPermissionIDHash(ruleType string, ip *ec2.IPPermission) string { } } + if len(ip.UserIDGroupPairs) > 0 { + sort.Sort(byUserIDAndGroup(ip.UserIDGroupPairs)) + for _, pair := range ip.UserIDGroupPairs { + if pair.GroupID != nil { + buf.WriteString(fmt.Sprintf("%s-", *pair.GroupID)) + } else { + buf.WriteString("-") + } + if pair.GroupName != nil { + buf.WriteString(fmt.Sprintf("%s-", *pair.GroupName)) + } else { + buf.WriteString("-") + } + } + } + return fmt.Sprintf("sg-%d", hashcode.String(buf.String())) } diff --git a/builtin/providers/aws/resource_aws_security_group_rule_migrate.go b/builtin/providers/aws/resource_aws_security_group_rule_migrate.go new file mode 100644 index 0000000000..012bffb0d2 --- /dev/null +++ b/builtin/providers/aws/resource_aws_security_group_rule_migrate.go @@ -0,0 +1,118 @@ +package aws + +import ( + "fmt" + "log" + "strconv" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/terraform" +) + +func resourceAwsSecurityGroupRuleMigrateState( + v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { + switch v { + case 0: + log.Println("[INFO] Found AWS Security Group State v0; migrating to v1") + return migrateSGRuleStateV0toV1(is, meta) + default: + return is, fmt.Errorf("Unexpected schema version: %d", v) + } + + return is, nil +} + +func migrateSGRuleStateV0toV1(is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { + if is.Empty() { + log.Println("[DEBUG] Empty InstanceState; nothing to migrate.") + return is, nil + } + + conn := meta.(*AWSClient).ec2conn + sg_id := is.Attributes["security_group_id"] + sg, err := findResourceSecurityGroup(conn, sg_id) + if err != nil { + return nil, fmt.Errorf("[WARN] Error finding security group for Security Group migration") + } + + perm, err := migrateExpandIPPerm(is.Attributes, sg) + + if err != nil { + return nil, fmt.Errorf("[WARN] Error making new IP Permission in Security Group migration") + } + + log.Printf("[DEBUG] Attributes before migration: %#v", is.Attributes) + newID := ipPermissionIDHash(is.Attributes["type"], perm) + is.Attributes["id"] = newID + is.ID = newID + log.Printf("[DEBUG] Attributes after migration: %#v, new id: %s", is.Attributes, newID) + return is, nil +} + +func migrateExpandIPPerm(attrs map[string]string, sg *ec2.SecurityGroup) (*ec2.IPPermission, error) { + var perm ec2.IPPermission + tp, err := strconv.Atoi(attrs["to_port"]) + if err != nil { + return nil, fmt.Errorf("Error converting to_port in Security Group migration") + } + + fp, err := strconv.Atoi(attrs["from_port"]) + if err != nil { + return nil, fmt.Errorf("Error converting from_port in Security Group migration") + } + + perm.ToPort = aws.Long(int64(tp)) + perm.FromPort = aws.Long(int64(fp)) + perm.IPProtocol = aws.String(attrs["protocol"]) + + groups := make(map[string]bool) + if attrs["self"] == "true" { + if sg.VPCID != nil && *sg.VPCID != "" { + groups[*sg.GroupID] = true + } else { + groups[*sg.GroupName] = true + } + } + + if attrs["source_security_group_id"] != "" { + groups[attrs["source_security_group_id"]] = true + } + + if len(groups) > 0 { + perm.UserIDGroupPairs = make([]*ec2.UserIDGroupPair, len(groups)) + // build string list of group name/ids + var gl []string + for k, _ := range groups { + gl = append(gl, k) + } + + for i, name := range gl { + perm.UserIDGroupPairs[i] = &ec2.UserIDGroupPair{ + GroupID: aws.String(name), + } + + if sg.VPCID == nil || *sg.VPCID == "" { + perm.UserIDGroupPairs[i].GroupID = nil + perm.UserIDGroupPairs[i].GroupName = aws.String(name) + perm.UserIDGroupPairs[i].UserID = nil + } + } + } + + var cb []string + for k, v := range attrs { + if k != "cidr_blocks.#" && strings.HasPrefix(k, "cidr_blocks") { + cb = append(cb, v) + } + } + if len(cb) > 0 { + perm.IPRanges = make([]*ec2.IPRange, len(cb)) + for i, v := range cb { + perm.IPRanges[i] = &ec2.IPRange{CIDRIP: aws.String(v)} + } + } + + return &perm, nil +} diff --git a/builtin/providers/aws/resource_aws_security_group_rule_test.go b/builtin/providers/aws/resource_aws_security_group_rule_test.go index f322ba44ea..2b99b0c159 100644 --- a/builtin/providers/aws/resource_aws_security_group_rule_test.go +++ b/builtin/providers/aws/resource_aws_security_group_rule_test.go @@ -44,6 +44,46 @@ func TestIpPermissionIDHash(t *testing.T) { }, } + vpc_security_group_source := &ec2.IPPermission{ + IPProtocol: aws.String("tcp"), + FromPort: aws.Long(int64(80)), + ToPort: aws.Long(int64(8000)), + UserIDGroupPairs: []*ec2.UserIDGroupPair{ + &ec2.UserIDGroupPair{ + UserID: aws.String("987654321"), + GroupID: aws.String("sg-12345678"), + }, + &ec2.UserIDGroupPair{ + UserID: aws.String("123456789"), + GroupID: aws.String("sg-987654321"), + }, + &ec2.UserIDGroupPair{ + UserID: aws.String("123456789"), + GroupID: aws.String("sg-12345678"), + }, + }, + } + + security_group_source := &ec2.IPPermission{ + IPProtocol: aws.String("tcp"), + FromPort: aws.Long(int64(80)), + ToPort: aws.Long(int64(8000)), + UserIDGroupPairs: []*ec2.UserIDGroupPair{ + &ec2.UserIDGroupPair{ + UserID: aws.String("987654321"), + GroupName: aws.String("my-security-group"), + }, + &ec2.UserIDGroupPair{ + UserID: aws.String("123456789"), + GroupName: aws.String("my-security-group"), + }, + &ec2.UserIDGroupPair{ + UserID: aws.String("123456789"), + GroupName: aws.String("my-other-security-group"), + }, + }, + } + // hardcoded hashes, to detect future change cases := []struct { Input *ec2.IPPermission @@ -53,12 +93,14 @@ func TestIpPermissionIDHash(t *testing.T) { {simple, "ingress", "sg-82613597"}, {egress, "egress", "sg-363054720"}, {egress_all, "egress", "sg-857124156"}, + {vpc_security_group_source, "egress", "sg-1900174468"}, + {security_group_source, "egress", "sg-1409919872"}, } for _, tc := range cases { actual := ipPermissionIDHash(tc.Type, tc.Input) if actual != tc.Output { - t.Fatalf("input: %s - %#v\noutput: %s", tc.Type, tc.Input, actual) + t.Errorf("input: %s - %#v\noutput: %s", tc.Type, tc.Input, actual) } } } From 8a21bd23eaa97116df4bd29e594ebf49931b97e2 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Tue, 16 Jun 2015 15:09:33 -0500 Subject: [PATCH 02/20] fix existing tests --- .../aws/resource_aws_security_group_rule_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/builtin/providers/aws/resource_aws_security_group_rule_test.go b/builtin/providers/aws/resource_aws_security_group_rule_test.go index 2b99b0c159..f7f5769e9b 100644 --- a/builtin/providers/aws/resource_aws_security_group_rule_test.go +++ b/builtin/providers/aws/resource_aws_security_group_rule_test.go @@ -7,6 +7,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/awsutil" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" @@ -92,15 +93,15 @@ func TestIpPermissionIDHash(t *testing.T) { }{ {simple, "ingress", "sg-82613597"}, {egress, "egress", "sg-363054720"}, - {egress_all, "egress", "sg-857124156"}, - {vpc_security_group_source, "egress", "sg-1900174468"}, - {security_group_source, "egress", "sg-1409919872"}, + {egress_all, "egress", "sg-2766285362"}, + {vpc_security_group_source, "egress", "sg-2661404947"}, + {security_group_source, "egress", "sg-1841245863"}, } for _, tc := range cases { actual := ipPermissionIDHash(tc.Type, tc.Input) if actual != tc.Output { - t.Errorf("input: %s - %#v\noutput: %s", tc.Type, tc.Input, actual) + t.Errorf("input: %s - %s\noutput: %s", tc.Type, awsutil.StringValue(tc.Input), actual) } } } From b25fb8a55d89b2452c7f08cfef58ad0ffacb8eda Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Tue, 16 Jun 2015 15:54:27 -0500 Subject: [PATCH 03/20] remove meta usage, stub test --- ...esource_aws_security_group_rule_migrate.go | 27 ++-------- ...ce_aws_security_group_rule_migrate_test.go | 50 +++++++++++++++++++ 2 files changed, 55 insertions(+), 22 deletions(-) create mode 100644 builtin/providers/aws/resource_aws_security_group_rule_migrate_test.go diff --git a/builtin/providers/aws/resource_aws_security_group_rule_migrate.go b/builtin/providers/aws/resource_aws_security_group_rule_migrate.go index 012bffb0d2..30eb8c09f0 100644 --- a/builtin/providers/aws/resource_aws_security_group_rule_migrate.go +++ b/builtin/providers/aws/resource_aws_security_group_rule_migrate.go @@ -16,7 +16,7 @@ func resourceAwsSecurityGroupRuleMigrateState( switch v { case 0: log.Println("[INFO] Found AWS Security Group State v0; migrating to v1") - return migrateSGRuleStateV0toV1(is, meta) + return migrateSGRuleStateV0toV1(is) default: return is, fmt.Errorf("Unexpected schema version: %d", v) } @@ -24,20 +24,13 @@ func resourceAwsSecurityGroupRuleMigrateState( return is, nil } -func migrateSGRuleStateV0toV1(is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { +func migrateSGRuleStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { if is.Empty() { log.Println("[DEBUG] Empty InstanceState; nothing to migrate.") return is, nil } - conn := meta.(*AWSClient).ec2conn - sg_id := is.Attributes["security_group_id"] - sg, err := findResourceSecurityGroup(conn, sg_id) - if err != nil { - return nil, fmt.Errorf("[WARN] Error finding security group for Security Group migration") - } - - perm, err := migrateExpandIPPerm(is.Attributes, sg) + perm, err := migrateExpandIPPerm(is.Attributes) if err != nil { return nil, fmt.Errorf("[WARN] Error making new IP Permission in Security Group migration") @@ -51,7 +44,7 @@ func migrateSGRuleStateV0toV1(is *terraform.InstanceState, meta interface{}) (*t return is, nil } -func migrateExpandIPPerm(attrs map[string]string, sg *ec2.SecurityGroup) (*ec2.IPPermission, error) { +func migrateExpandIPPerm(attrs map[string]string) (*ec2.IPPermission, error) { var perm ec2.IPPermission tp, err := strconv.Atoi(attrs["to_port"]) if err != nil { @@ -69,11 +62,7 @@ func migrateExpandIPPerm(attrs map[string]string, sg *ec2.SecurityGroup) (*ec2.I groups := make(map[string]bool) if attrs["self"] == "true" { - if sg.VPCID != nil && *sg.VPCID != "" { - groups[*sg.GroupID] = true - } else { - groups[*sg.GroupName] = true - } + groups[attrs["security_group_id"]] = true } if attrs["source_security_group_id"] != "" { @@ -92,12 +81,6 @@ func migrateExpandIPPerm(attrs map[string]string, sg *ec2.SecurityGroup) (*ec2.I perm.UserIDGroupPairs[i] = &ec2.UserIDGroupPair{ GroupID: aws.String(name), } - - if sg.VPCID == nil || *sg.VPCID == "" { - perm.UserIDGroupPairs[i].GroupID = nil - perm.UserIDGroupPairs[i].GroupName = aws.String(name) - perm.UserIDGroupPairs[i].UserID = nil - } } } diff --git a/builtin/providers/aws/resource_aws_security_group_rule_migrate_test.go b/builtin/providers/aws/resource_aws_security_group_rule_migrate_test.go new file mode 100644 index 0000000000..b9ff41b91d --- /dev/null +++ b/builtin/providers/aws/resource_aws_security_group_rule_migrate_test.go @@ -0,0 +1,50 @@ +package aws + +import ( + "testing" + + "github.com/hashicorp/terraform/terraform" +) + +func TestAWSSecurityGroupRuleMigrateState(t *testing.T) { + cases := map[string]struct { + StateVersion int + Attributes map[string]string + Expected string + Meta interface{} + }{ + "v0_1": { + StateVersion: 0, + Attributes: map[string]string{ + // EBS + "self": "true", + }, + Expected: "sg-1234", + }, + "v0_2": { + StateVersion: 0, + Attributes: map[string]string{ + // EBS + "self": "false", + }, + Expected: "sg-1235", + }, + } + + for tn, tc := range cases { + is := &terraform.InstanceState{ + ID: "sg-12333", + Attributes: tc.Attributes, + } + is, err := resourceAwsSecurityGroupRuleMigrateState( + tc.StateVersion, is, tc.Meta) + + if err != nil { + t.Fatalf("bad: %s, err: %#v", tn, err) + } + + if is.ID != tc.Expected { + t.Fatalf("bad sg rule id: %s\n\n expected: %s", is.ID, tc.Expected) + } + } +} From 2d06c81e4b6a4d907809cf2fb0dffea701a1a889 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Tue, 16 Jun 2015 16:15:07 -0500 Subject: [PATCH 04/20] update test --- ...ce_aws_security_group_rule_migrate_test.go | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/builtin/providers/aws/resource_aws_security_group_rule_migrate_test.go b/builtin/providers/aws/resource_aws_security_group_rule_migrate_test.go index b9ff41b91d..960cceaa49 100644 --- a/builtin/providers/aws/resource_aws_security_group_rule_migrate_test.go +++ b/builtin/providers/aws/resource_aws_security_group_rule_migrate_test.go @@ -7,6 +7,11 @@ import ( ) func TestAWSSecurityGroupRuleMigrateState(t *testing.T) { + // "id":"sg-4235098228", "from_port":"0", "source_security_group_id":"sg-11877275"} + + // 2015/06/16 16:04:21 terraform-provider-aws: 2015/06/16 16:04:21 [DEBUG] Attributes after migration: + + // map[string]string{"from_port":"0", "source_security_group_id":"sg-11877275", "id":"sg-3766347571", "security_group_id":"sg-13877277", "cidr_blocks.#":"0", "type":"ingress", "protocol":"-1", "self":"false", "to_port":"0"}, new id: sg-3766347571 cases := map[string]struct { StateVersion int Attributes map[string]string @@ -16,24 +21,31 @@ func TestAWSSecurityGroupRuleMigrateState(t *testing.T) { "v0_1": { StateVersion: 0, Attributes: map[string]string{ - // EBS - "self": "true", + "self": "false", + "to_port": "0", + "security_group_id": "sg-13877277", + "cidr_blocks.#": "0", + "type": "ingress", + "protocol": "-1", + "id": "sg-4235098228", + "from_port": "0", + "source_security_group_id": "sg-11877275", }, - Expected: "sg-1234", - }, - "v0_2": { - StateVersion: 0, - Attributes: map[string]string{ - // EBS - "self": "false", - }, - Expected: "sg-1235", + Expected: "sg-3766347571", }, + // "v0_2": { + // StateVersion: 0, + // Attributes: map[string]string{ + // // EBS + // "self": "false", + // }, + // Expected: "sg-1235", + // }, } for tn, tc := range cases { is := &terraform.InstanceState{ - ID: "sg-12333", + ID: "sg-4235098228", Attributes: tc.Attributes, } is, err := resourceAwsSecurityGroupRuleMigrateState( From 3bf89fb81ec70f8a1fd530a01d25e37a2ce64c28 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Tue, 16 Jun 2015 16:21:46 -0500 Subject: [PATCH 05/20] update tests with another example --- ...ce_aws_security_group_rule_migrate_test.go | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/builtin/providers/aws/resource_aws_security_group_rule_migrate_test.go b/builtin/providers/aws/resource_aws_security_group_rule_migrate_test.go index 960cceaa49..33783828f5 100644 --- a/builtin/providers/aws/resource_aws_security_group_rule_migrate_test.go +++ b/builtin/providers/aws/resource_aws_security_group_rule_migrate_test.go @@ -14,12 +14,14 @@ func TestAWSSecurityGroupRuleMigrateState(t *testing.T) { // map[string]string{"from_port":"0", "source_security_group_id":"sg-11877275", "id":"sg-3766347571", "security_group_id":"sg-13877277", "cidr_blocks.#":"0", "type":"ingress", "protocol":"-1", "self":"false", "to_port":"0"}, new id: sg-3766347571 cases := map[string]struct { StateVersion int + ID string Attributes map[string]string Expected string Meta interface{} }{ "v0_1": { StateVersion: 0, + ID: "sg-4235098228", Attributes: map[string]string{ "self": "false", "to_port": "0", @@ -27,25 +29,33 @@ func TestAWSSecurityGroupRuleMigrateState(t *testing.T) { "cidr_blocks.#": "0", "type": "ingress", "protocol": "-1", - "id": "sg-4235098228", "from_port": "0", "source_security_group_id": "sg-11877275", }, Expected: "sg-3766347571", }, - // "v0_2": { - // StateVersion: 0, - // Attributes: map[string]string{ - // // EBS - // "self": "false", - // }, - // Expected: "sg-1235", - // }, + "v0_2": { + StateVersion: 0, + ID: "sg-1021609891", + Attributes: map[string]string{ + "security_group_id": "sg-0981746d", + "from_port": "0", + "to_port": "0", + "type": "ingress", + "self": "false", + "protocol": "-1", + "cidr_blocks.0": "172.16.1.0/24", + "cidr_blocks.1": "172.16.2.0/24", + "cidr_blocks.2": "172.16.3.0/24", + "cidr_blocks.3": "172.16.4.0/24", + "cidr_blocks.#": "4"}, + Expected: "sg-4100229787", + }, } for tn, tc := range cases { is := &terraform.InstanceState{ - ID: "sg-4235098228", + ID: tc.ID, Attributes: tc.Attributes, } is, err := resourceAwsSecurityGroupRuleMigrateState( From c1cdac1f76c65b4e80bf020d3839df348dcdacf0 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Tue, 16 Jun 2015 16:30:33 -0500 Subject: [PATCH 06/20] clean up old, incompatible test --- .../providers/aws/resource_aws_security_group_rule_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/builtin/providers/aws/resource_aws_security_group_rule_test.go b/builtin/providers/aws/resource_aws_security_group_rule_test.go index f7f5769e9b..595886e184 100644 --- a/builtin/providers/aws/resource_aws_security_group_rule_test.go +++ b/builtin/providers/aws/resource_aws_security_group_rule_test.go @@ -184,9 +184,9 @@ func TestAccAWSSecurityGroupRule_MultiIngress(t *testing.T) { var group ec2.SecurityGroup testMultiRuleCount := func(*terraform.State) error { - if len(group.IPPermissions) != 3 { + if len(group.IPPermissions) != 2 { return fmt.Errorf("Wrong Security Group rule count, expected %d, got %d", - 3, len(group.IPPermissions)) + 2, len(group.IPPermissions)) } var rule *ec2.IPPermission @@ -438,7 +438,6 @@ resource "aws_security_group_rule" "ingress_1" { cidr_blocks = ["10.0.0.0/8"] security_group_id = "${aws_security_group.web.id}" - source_security_group_id = "${aws_security_group.worker.id}" } resource "aws_security_group_rule" "ingress_2" { From 359826be263afb64435ac4a1e2094308704d6d83 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Tue, 16 Jun 2015 16:38:26 -0500 Subject: [PATCH 07/20] clean up some conflicts with --- .../providers/aws/resource_aws_security_group_rule.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/builtin/providers/aws/resource_aws_security_group_rule.go b/builtin/providers/aws/resource_aws_security_group_rule.go index 45ecd2c6aa..b12884e595 100644 --- a/builtin/providers/aws/resource_aws_security_group_rule.go +++ b/builtin/providers/aws/resource_aws_security_group_rule.go @@ -51,11 +51,10 @@ func resourceAwsSecurityGroupRule() *schema.Resource { }, "cidr_blocks": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - ForceNew: true, - Elem: &schema.Schema{Type: schema.TypeString}, - ConflictsWith: []string{"source_security_group_id"}, + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, }, "security_group_id": &schema.Schema{ From 640836ee58d9d3e92a75f7c6e27953eb16e2f911 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Wed, 17 Jun 2015 09:35:50 -0500 Subject: [PATCH 08/20] rename method, update docs --- .../aws/resource_aws_security_group_rule.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/builtin/providers/aws/resource_aws_security_group_rule.go b/builtin/providers/aws/resource_aws_security_group_rule.go index b12884e595..aa634bdf1b 100644 --- a/builtin/providers/aws/resource_aws_security_group_rule.go +++ b/builtin/providers/aws/resource_aws_security_group_rule.go @@ -267,13 +267,13 @@ func findResourceSecurityGroup(conn *ec2.EC2, id string) (*ec2.SecurityGroup, er return resp.SecurityGroups[0], nil } -// byUserIDAndGroup implements sort.Interface for []*ec2.UserIDGroupPairs based on -// UserID and then the GroupID or GroupName field (only one should be set). -type byUserIDAndGroup []*ec2.UserIDGroupPair +// ByGroupPair implements sort.Interface for []*ec2.UserIDGroupPairs based on +// GroupID or GroupName field (only one should be set). +type ByGroupPair []*ec2.UserIDGroupPair -func (b byUserIDAndGroup) Len() int { return len(b) } -func (b byUserIDAndGroup) Swap(i, j int) { b[i], b[j] = b[j], b[i] } -func (b byUserIDAndGroup) Less(i, j int) bool { +func (b ByGroupPair) Len() int { return len(b) } +func (b ByGroupPair) Swap(i, j int) { b[i], b[j] = b[j], b[i] } +func (b ByGroupPair) Less(i, j int) bool { if b[i].GroupID != nil && b[j].GroupID != nil { return *b[i].GroupID < *b[j].GroupID } @@ -310,7 +310,7 @@ func ipPermissionIDHash(ruleType string, ip *ec2.IPPermission) string { } if len(ip.UserIDGroupPairs) > 0 { - sort.Sort(byUserIDAndGroup(ip.UserIDGroupPairs)) + sort.Sort(ByGroupPair(ip.UserIDGroupPairs)) for _, pair := range ip.UserIDGroupPairs { if pair.GroupID != nil { buf.WriteString(fmt.Sprintf("%s-", *pair.GroupID)) From ceeb94e1573443d447155fa2aaf4f9155753d995 Mon Sep 17 00:00:00 2001 From: Alex Pilon Date: Sun, 7 Jun 2015 11:23:32 +0200 Subject: [PATCH 09/20] provider/aws: Add cloudwatch_metric_alarm --- builtin/providers/aws/config.go | 5 + builtin/providers/aws/provider.go | 1 + .../resource_aws_cloudwatch_metric_alarm.go | 287 ++++++++++++++++++ ...source_aws_cloudwatch_metric_alarm_test.go | 95 ++++++ 4 files changed, 388 insertions(+) create mode 100644 builtin/providers/aws/resource_aws_cloudwatch_metric_alarm.go create mode 100644 builtin/providers/aws/resource_aws_cloudwatch_metric_alarm_test.go diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index 658e0ec914..93f7db9190 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -10,6 +10,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ecs" @@ -37,6 +38,7 @@ type Config struct { } type AWSClient struct { + cloudwatchconn *cloudwatch.CloudWatch dynamodbconn *dynamodb.DynamoDB ec2conn *ec2.EC2 ecsconn *ecs.ECS @@ -143,6 +145,9 @@ func (c *Config) Client() (interface{}, error) { log.Println("[INFO] Initializing Lambda Connection") client.lambdaconn = lambda.New(awsConfig) + + log.Println("[INFO] Initializing CloudWatch SDK connection") + client.cloudwatchconn = cloudwatch.New(awsConfig) } if len(errs) > 0 { diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index f28cf69ea0..44f881debe 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -86,6 +86,7 @@ func Provider() terraform.ResourceProvider { "aws_app_cookie_stickiness_policy": resourceAwsAppCookieStickinessPolicy(), "aws_autoscaling_group": resourceAwsAutoscalingGroup(), "aws_autoscaling_notification": resourceAwsAutoscalingNotification(), + "aws_cloudwatch_metric_alarm": resourceAwsCloudWatchMetricAlarm(), "aws_customer_gateway": resourceAwsCustomerGateway(), "aws_db_instance": resourceAwsDbInstance(), "aws_db_parameter_group": resourceAwsDbParameterGroup(), diff --git a/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm.go b/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm.go new file mode 100644 index 0000000000..9f9d60ce8a --- /dev/null +++ b/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm.go @@ -0,0 +1,287 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatch" +) + +func resourceAwsCloudWatchMetricAlarm() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsCloudWatchMetricAlarmCreate, + Read: resourceAwsCloudWatchMetricAlarmRead, + Update: resourceAwsCloudWatchMetricAlarmUpdate, + Delete: resourceAwsCloudWatchMetricAlarmDelete, + + Schema: map[string]*schema.Schema{ + "alarm_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "comparison_operator": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "evaluation_periods": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + "metric_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "namespace": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "period": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + "statistic": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "threshold": &schema.Schema{ + Type: schema.TypeFloat, + Required: true, + }, + "actions_enabled": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "alarm_actions": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + "alarm_description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "dimensions": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + }, + "insufficient_data_actions": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + "ok_actions": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + "unit": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func resourceAwsCloudWatchMetricAlarmCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cloudwatchconn + + params := getAwsCloudWatchPutMetricAlarmInput(d) + + log.Printf("[DEBUG] Creating CloudWatch Metric Alarm: %#v", params) + _, err := conn.PutMetricAlarm(¶ms) + if err != nil { + return fmt.Errorf("Creating metric alarm failed: %s", err) + } + d.SetId(d.Get("alarm_name").(string)) + log.Println("[INFO] CloudWatch Metric Alarm created") + + return resourceAwsCloudWatchMetricAlarmRead(d, meta) +} + +func resourceAwsCloudWatchMetricAlarmRead(d *schema.ResourceData, meta interface{}) error { + a, err := getAwsCloudWatchMetricAlarm(d, meta) + if err != nil { + return err + } + if a == nil { + d.SetId("") + return nil + } + + log.Printf("[DEBUG] Reading CloudWatch Metric Alarm: %s", d.Get("alarm_name")) + + d.Set("actions_enabled", a.ActionsEnabled) + + if err := d.Set("alarm_actions", _strArrPtrToList(a.AlarmActions)); err != nil { + log.Printf("[WARN] Error setting Alarm Actions: %s", err) + } + d.Set("alarm_description", a.AlarmDescription) + d.Set("alarm_name", a.AlarmName) + d.Set("comparison_operator", a.ComparisonOperator) + d.Set("dimensions", a.Dimensions) + d.Set("evaluation_periods", a.EvaluationPeriods) + + if err := d.Set("insufficient_data_actions", _strArrPtrToList(a.InsufficientDataActions)); err != nil { + log.Printf("[WARN] Error setting Insufficient Data Actions: %s", err) + } + d.Set("metric_name", a.MetricName) + d.Set("namespace", a.Namespace) + + if err := d.Set("ok_actions", _strArrPtrToList(a.OKActions)); err != nil { + log.Printf("[WARN] Error setting OK Actions: %s", err) + } + d.Set("period", a.Period) + d.Set("statistic", a.Statistic) + d.Set("threshold", a.Threshold) + d.Set("unit", a.Unit) + + return nil +} + +func resourceAwsCloudWatchMetricAlarmUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cloudwatchconn + params := getAwsCloudWatchPutMetricAlarmInput(d) + + log.Printf("[DEBUG] Updating CloudWatch Metric Alarm: %#v", params) + _, err := conn.PutMetricAlarm(¶ms) + if err != nil { + return fmt.Errorf("Updating metric alarm failed: %s", err) + } + log.Println("[INFO] CloudWatch Metric Alarm updated") + + return resourceAwsCloudWatchMetricAlarmRead(d, meta) +} + +func resourceAwsCloudWatchMetricAlarmDelete(d *schema.ResourceData, meta interface{}) error { + p, err := getAwsCloudWatchMetricAlarm(d, meta) + if err != nil { + return err + } + if p == nil { + log.Printf("[DEBUG] CloudWatch Metric Alarm %s is already gone", d.Id()) + return nil + } + + log.Printf("[INFO] Deleting CloudWatch Metric Alarm: %s", d.Id()) + + conn := meta.(*AWSClient).cloudwatchconn + params := cloudwatch.DeleteAlarmsInput{ + AlarmNames: []*string{aws.String(d.Id())}, + } + + if _, err := conn.DeleteAlarms(¶ms); err != nil { + return fmt.Errorf("Error deleting CloudWatch Metric Alarm: %s", err) + } + log.Println("[INFO] CloudWatch Metric Alarm deleted") + + d.SetId("") + return nil +} + +func getAwsCloudWatchPutMetricAlarmInput(d *schema.ResourceData) cloudwatch.PutMetricAlarmInput { + params := cloudwatch.PutMetricAlarmInput{ + AlarmName: aws.String(d.Get("alarm_name").(string)), + ComparisonOperator: aws.String(d.Get("comparison_operator").(string)), + EvaluationPeriods: aws.Long(int64(d.Get("evaluation_periods").(int))), + MetricName: aws.String(d.Get("metric_name").(string)), + Namespace: aws.String(d.Get("namespace").(string)), + Period: aws.Long(int64(d.Get("period").(int))), + Statistic: aws.String(d.Get("statistic").(string)), + Threshold: aws.Double(d.Get("threshold").(float64)), + } + + if v := d.Get("actions_enabled"); v != nil { + params.ActionsEnabled = aws.Boolean(v.(bool)) + } + + if v, ok := d.GetOk("alarm_description"); ok { + params.AlarmDescription = aws.String(v.(string)) + } + + if v, ok := d.GetOk("unit"); ok { + params.Unit = aws.String(v.(string)) + } + + var alarmActions []*string + if v := d.Get("alarm_actions"); v != nil { + for _, v := range v.(*schema.Set).List() { + str := v.(string) + alarmActions = append(alarmActions, aws.String(str)) + } + params.AlarmActions = alarmActions + } + + var insufficientDataActions []*string + if v := d.Get("insufficient_data_actions"); v != nil { + for _, v := range v.(*schema.Set).List() { + str := v.(string) + insufficientDataActions = append(insufficientDataActions, aws.String(str)) + } + params.InsufficientDataActions = insufficientDataActions + } + + var okActions []*string + if v := d.Get("ok_actions"); v != nil { + for _, v := range v.(*schema.Set).List() { + str := v.(string) + okActions = append(okActions, aws.String(str)) + } + params.OKActions = okActions + } + + a := d.Get("dimensions").(map[string]interface{}) + dimensions := make([]*cloudwatch.Dimension, 0, len(a)) + for k, v := range a { + dimensions = append(dimensions, &cloudwatch.Dimension{ + Name: aws.String(k), + Value: aws.String(v.(string)), + }) + } + params.Dimensions = dimensions + + return params +} + +func getAwsCloudWatchMetricAlarm(d *schema.ResourceData, meta interface{}) (*cloudwatch.MetricAlarm, error) { + conn := meta.(*AWSClient).cloudwatchconn + + params := cloudwatch.DescribeAlarmsInput{ + AlarmNames: []*string{aws.String(d.Id())}, + } + + resp, err := conn.DescribeAlarms(¶ms) + if err != nil { + return nil, nil + } + + // Find it and return it + for idx, ma := range resp.MetricAlarms { + if *ma.AlarmName == d.Id() { + return resp.MetricAlarms[idx], nil + } + } + + return nil, nil +} + +func _strArrPtrToList(strArrPtr []*string) []string { + var result []string + for _, elem := range strArrPtr { + result = append(result, *elem) + } + return result +} diff --git a/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm_test.go b/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm_test.go new file mode 100644 index 0000000000..8d9a53c360 --- /dev/null +++ b/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm_test.go @@ -0,0 +1,95 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatch" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSCloudWatchMetricAlarm_basic(t *testing.T) { + var alarm cloudwatch.MetricAlarm + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudWatchMetricAlarmDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSCloudWatchMetricAlarmConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudWatchMetricAlarmExists("aws_cloudwatch_metric_alarm.foobar", &alarm), + resource.TestCheckResourceAttr("aws_cloudwatch_metric_alarm.foobar", "metric_name", "CPUUtilization"), + resource.TestCheckResourceAttr("aws_cloudwatch_metric_alarm.foobar", "statistic", "Average"), + ), + }, + }, + }) +} + +func testAccCheckCloudWatchMetricAlarmExists(n string, alarm *cloudwatch.MetricAlarm) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := testAccProvider.Meta().(*AWSClient).cloudwatchconn + params := cloudwatch.DescribeAlarmsInput{ + AlarmNames: []*string{aws.String(rs.Primary.ID)}, + } + resp, err := conn.DescribeAlarms(¶ms) + if err != nil { + return err + } + if len(resp.MetricAlarms) == 0 { + return fmt.Errorf("Alarm not found") + } + *alarm = *resp.MetricAlarms[0] + + return nil + } +} + +func testAccCheckAWSCloudWatchMetricAlarmDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).cloudwatchconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_cloudwatch_metric_alarm" { + continue + } + + params := cloudwatch.DescribeAlarmsInput{ + AlarmNames: []*string{aws.String(rs.Primary.ID)}, + } + + resp, err := conn.DescribeAlarms(¶ms) + + if err == nil { + if len(resp.MetricAlarms) != 0 && + *resp.MetricAlarms[0].AlarmName == rs.Primary.ID { + return fmt.Errorf("Alarm Still Exists: %s", rs.Primary.ID) + } + } + } + + return nil +} + +var testAccAWSCloudWatchMetricAlarmConfig = fmt.Sprintf(` +resource "aws_cloudwatch_metric_alarm" "foobar" { + alarm_name = "terraform-test-foobar5" + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = "2" + metric_name = "CPUUtilization" + namespace = "AWS/EC2" + period = "120" + statistic = "Average" + threshold = "80" + alarm_description = "This metric monitor ec2 cpu utilization" + insufficient_data_actions = [] +} +`) From 05f4b9bfd940d717fa3b6646a5d71c6de41541f6 Mon Sep 17 00:00:00 2001 From: Alex Pilon Date: Sun, 7 Jun 2015 11:18:34 +0200 Subject: [PATCH 10/20] provider/aws: Add autoscaling_policy --- builtin/providers/aws/provider.go | 1 + .../aws/resource_aws_autoscaling_policy.go | 181 ++++++++++++++++++ .../resource_aws_autoscaling_policy_test.go | 118 ++++++++++++ 3 files changed, 300 insertions(+) create mode 100644 builtin/providers/aws/resource_aws_autoscaling_policy.go create mode 100644 builtin/providers/aws/resource_aws_autoscaling_policy_test.go diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 44f881debe..0e54e7b501 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -86,6 +86,7 @@ func Provider() terraform.ResourceProvider { "aws_app_cookie_stickiness_policy": resourceAwsAppCookieStickinessPolicy(), "aws_autoscaling_group": resourceAwsAutoscalingGroup(), "aws_autoscaling_notification": resourceAwsAutoscalingNotification(), + "aws_autoscaling_policy": resourceAwsAutoscalingPolicy(), "aws_cloudwatch_metric_alarm": resourceAwsCloudWatchMetricAlarm(), "aws_customer_gateway": resourceAwsCustomerGateway(), "aws_db_instance": resourceAwsDbInstance(), diff --git a/builtin/providers/aws/resource_aws_autoscaling_policy.go b/builtin/providers/aws/resource_aws_autoscaling_policy.go new file mode 100644 index 0000000000..d2f6d2d47b --- /dev/null +++ b/builtin/providers/aws/resource_aws_autoscaling_policy.go @@ -0,0 +1,181 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsAutoscalingPolicy() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAutoscalingPolicyCreate, + Read: resourceAwsAutoscalingPolicyRead, + Update: resourceAwsAutoscalingPolicyUpdate, + Delete: resourceAwsAutoscalingPolicyDelete, + + Schema: map[string]*schema.Schema{ + "arn": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "adjustment_type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "autoscaling_group_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "cooldown": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + "min_adjustment_step": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + "scaling_adjustment": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + }, + } +} + +func resourceAwsAutoscalingPolicyCreate(d *schema.ResourceData, meta interface{}) error { + autoscalingconn := meta.(*AWSClient).autoscalingconn + + params := getAwsAutoscalingPutScalingPolicyInput(d) + + log.Printf("[DEBUG] AutoScaling PutScalingPolicy: %#v", params) + resp, err := autoscalingconn.PutScalingPolicy(¶ms) + if err != nil { + return fmt.Errorf("Error putting scaling policy: %s", err) + } + + d.Set("arn", resp.PolicyARN) + d.SetId(d.Get("name").(string)) + log.Printf("[INFO] AutoScaling Scaling PolicyARN: %s", d.Get("arn").(string)) + + return resourceAwsAutoscalingPolicyRead(d, meta) +} + +func resourceAwsAutoscalingPolicyRead(d *schema.ResourceData, meta interface{}) error { + p, err := getAwsAutoscalingPolicy(d, meta) + if err != nil { + return err + } + if p == nil { + d.SetId("") + return nil + } + + log.Printf("[DEBUG] Read Scaling Policy: ASG: %s, SP: %s, Obj: %#v", d.Get("autoscaling_group_name"), d.Get("name"), p) + + d.Set("adjustment_type", p.AdjustmentType) + d.Set("autoscaling_group_name", p.AutoScalingGroupName) + d.Set("cooldown", p.Cooldown) + d.Set("min_adjustment_step", p.MinAdjustmentStep) + d.Set("arn", p.PolicyARN) + d.Set("name", p.PolicyName) + d.Set("scaling_adjustment", p.ScalingAdjustment) + + return nil +} + +func resourceAwsAutoscalingPolicyUpdate(d *schema.ResourceData, meta interface{}) error { + autoscalingconn := meta.(*AWSClient).autoscalingconn + + params := getAwsAutoscalingPutScalingPolicyInput(d) + + log.Printf("[DEBUG] Autoscaling Update Scaling Policy: %#v", params) + _, err := autoscalingconn.PutScalingPolicy(¶ms) + if err != nil { + return err + } + + return resourceAwsAutoscalingPolicyRead(d, meta) +} + +func resourceAwsAutoscalingPolicyDelete(d *schema.ResourceData, meta interface{}) error { + autoscalingconn := meta.(*AWSClient).autoscalingconn + p, err := getAwsAutoscalingPolicy(d, meta) + if err != nil { + return err + } + if p == nil { + return nil + } + + params := autoscaling.DeletePolicyInput{ + AutoScalingGroupName: aws.String(d.Get("autoscaling_group_name").(string)), + PolicyName: aws.String(d.Get("name").(string)), + } + if _, err := autoscalingconn.DeletePolicy(¶ms); err != nil { + return fmt.Errorf("Autoscaling Scaling Policy: %s ", err) + } + + d.SetId("") + return nil +} + +// PutScalingPolicy seems to require all params to be resent, so create and update can share this common function +func getAwsAutoscalingPutScalingPolicyInput(d *schema.ResourceData) autoscaling.PutScalingPolicyInput { + var params = autoscaling.PutScalingPolicyInput{ + AutoScalingGroupName: aws.String(d.Get("autoscaling_group_name").(string)), + PolicyName: aws.String(d.Get("name").(string)), + } + + if v, ok := d.GetOk("adjustment_type"); ok { + params.AdjustmentType = aws.String(v.(string)) + } + + if v, ok := d.GetOk("cooldown"); ok { + params.Cooldown = aws.Long(int64(v.(int))) + } + + if v, ok := d.GetOk("scaling_adjustment"); ok { + params.ScalingAdjustment = aws.Long(int64(v.(int))) + } + + if v, ok := d.GetOk("min_adjustment_step"); ok { + params.MinAdjustmentStep = aws.Long(int64(v.(int))) + } + + return params +} + +func getAwsAutoscalingPolicy(d *schema.ResourceData, meta interface{}) (*autoscaling.ScalingPolicy, error) { + autoscalingconn := meta.(*AWSClient).autoscalingconn + + params := autoscaling.DescribePoliciesInput{ + AutoScalingGroupName: aws.String(d.Get("autoscaling_group_name").(string)), + PolicyNames: []*string{aws.String(d.Get("name").(string))}, + } + + log.Printf("[DEBUG] AutoScaling Scaling Policy Describe Params: %#v", params) + resp, err := autoscalingconn.DescribePolicies(¶ms) + if err != nil { + return nil, fmt.Errorf("Error retrieving scaling policies: %s", err) + } + + // find scaling policy + name := d.Get("name") + for idx, sp := range resp.ScalingPolicies { + if *sp.PolicyName == name { + return resp.ScalingPolicies[idx], nil + } + } + + // policy not found + return nil, nil +} diff --git a/builtin/providers/aws/resource_aws_autoscaling_policy_test.go b/builtin/providers/aws/resource_aws_autoscaling_policy_test.go new file mode 100644 index 0000000000..2254640b08 --- /dev/null +++ b/builtin/providers/aws/resource_aws_autoscaling_policy_test.go @@ -0,0 +1,118 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSAutoscalingPolicy_basic(t *testing.T) { + var policy autoscaling.ScalingPolicy + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAutoscalingPolicyDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSAutoscalingPolicyConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckScalingPolicyExists("aws_autoscaling_policy.foobar", &policy), + resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar", "adjustment_type", "ChangeInCapacity"), + resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar", "cooldown", "300"), + ), + }, + }, + }) +} + +func testAccCheckScalingPolicyExists(n string, policy *autoscaling.ScalingPolicy) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + rs = rs + return fmt.Errorf("Not found: %s", n) + } + + conn := testAccProvider.Meta().(*AWSClient).autoscalingconn + params := &autoscaling.DescribePoliciesInput{ + AutoScalingGroupName: aws.String(rs.Primary.Attributes["autoscaling_group_name"]), + PolicyNames: []*string{aws.String(rs.Primary.ID)}, + } + resp, err := conn.DescribePolicies(params) + if err != nil { + return err + } + if len(resp.ScalingPolicies) == 0 { + return fmt.Errorf("ScalingPolicy not found") + } + + *policy = *resp.ScalingPolicies[0] + + return nil + } +} + +func testAccCheckAWSAutoscalingPolicyDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).autoscalingconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_autoscaling_group" { + continue + } + + params := autoscaling.DescribePoliciesInput{ + AutoScalingGroupName: aws.String(rs.Primary.Attributes["autoscaling_group_name"]), + PolicyNames: []*string{aws.String(rs.Primary.ID)}, + } + + resp, err := conn.DescribePolicies(¶ms) + + if err == nil { + if len(resp.ScalingPolicies) != 0 && + *resp.ScalingPolicies[0].PolicyName == rs.Primary.ID { + return fmt.Errorf("Scaling Policy Still Exists: %s", rs.Primary.ID) + } + } + } + + return nil +} + +var testAccAWSAutoscalingPolicyConfig = fmt.Sprintf(` +resource "aws_launch_configuration" "foobar" { + name = "terraform-test-foobar5" + image_id = "ami-21f78e11" + instance_type = "t1.micro" +} + +resource "aws_autoscaling_group" "foobar" { + availability_zones = ["us-west-2a"] + name = "terraform-test-foobar5" + max_size = 5 + min_size = 2 + health_check_grace_period = 300 + health_check_type = "ELB" + desired_capacity = 4 + force_delete = true + termination_policies = ["OldestInstance"] + launch_configuration = "${aws_launch_configuration.foobar.name}" + tag { + key = "Foo" + value = "foo-bar" + propagate_at_launch = true + } +} + +resource "aws_autoscaling_policy" "foobar" { + name = "foobar" + scaling_adjustment = 4 + adjustment_type = "ChangeInCapacity" + cooldown = 300 + autoscaling_group_name = "${aws_autoscaling_group.foobar.name}" +} +`) From 14f4e5fe54584fb6e7a2212217a9d5d9d753dd59 Mon Sep 17 00:00:00 2001 From: Alex Pilon Date: Sun, 7 Jun 2015 11:19:57 +0200 Subject: [PATCH 11/20] provider/aws: Add docs for autoscaling_policy + cloudwatch_metric_alarm --- .../resource_aws_autoscaling_policy_test.go | 3 - .../resource_aws_cloudwatch_metric_alarm.go | 1 + .../aws/r/autoscaling_policy.html.markdown | 53 +++++++++++++ .../r/cloudwatch_metric_alarm.html.markdown | 75 +++++++++++++++++++ website/source/layouts/aws.erb | 8 ++ 5 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 website/source/docs/providers/aws/r/autoscaling_policy.html.markdown create mode 100644 website/source/docs/providers/aws/r/cloudwatch_metric_alarm.html.markdown diff --git a/builtin/providers/aws/resource_aws_autoscaling_policy_test.go b/builtin/providers/aws/resource_aws_autoscaling_policy_test.go index 2254640b08..0a7aeff916 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_policy_test.go +++ b/builtin/providers/aws/resource_aws_autoscaling_policy_test.go @@ -51,8 +51,6 @@ func testAccCheckScalingPolicyExists(n string, policy *autoscaling.ScalingPolicy return fmt.Errorf("ScalingPolicy not found") } - *policy = *resp.ScalingPolicies[0] - return nil } } @@ -97,7 +95,6 @@ resource "aws_autoscaling_group" "foobar" { min_size = 2 health_check_grace_period = 300 health_check_type = "ELB" - desired_capacity = 4 force_delete = true termination_policies = ["OldestInstance"] launch_configuration = "${aws_launch_configuration.foobar.name}" diff --git a/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm.go b/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm.go index 9f9d60ce8a..4c8b401400 100644 --- a/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm.go +++ b/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm.go @@ -55,6 +55,7 @@ func resourceAwsCloudWatchMetricAlarm() *schema.Resource { "actions_enabled": &schema.Schema{ Type: schema.TypeBool, Optional: true, + Default: true, }, "alarm_actions": &schema.Schema{ Type: schema.TypeSet, diff --git a/website/source/docs/providers/aws/r/autoscaling_policy.html.markdown b/website/source/docs/providers/aws/r/autoscaling_policy.html.markdown new file mode 100644 index 0000000000..2543c0220d --- /dev/null +++ b/website/source/docs/providers/aws/r/autoscaling_policy.html.markdown @@ -0,0 +1,53 @@ +--- +layout: "aws" +page_title: "AWS: aws_autoscaling_policy" +sidebar_current: "docs-aws-resource-autoscaling-policy" +description: |- + Provides an AutoScaling Scaling Group resource. +--- + +# aws\_autoscaling\_policy + +Provides an AutoScaling Scaling Policy resource. + +~> **NOTE:** You may want to omit `desired_capacity` attribute from attached `aws_autoscaling_group` +when using autoscaling policies. It's good practice to pick either +[manual](http://docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/as-manual-scaling.html) +or [dynamic](http://docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/as-scale-based-on-demand.html) +(policy-based) scaling. + +## Example Usage +``` +resource "aws_autoscaling_policy" "bat" { + name = "foobar3-terraform-test" + scaling_adjustment = 4 + adjustment_type = "ChangeInCapacity" + cooldown = 300 + autoscaling_group_name = "${aws_autoscaling_group.bar.name}" +} + +resource "aws_autoscaling_group" "bar" { + availability_zones = ["us-east-1a"] + name = "foobar3-terraform-test" + max_size = 5 + min_size = 2 + health_check_grace_period = 300 + health_check_type = "ELB" + force_delete = true + launch_configuration = "${aws_launch_configuration.foo.name}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the policy. +* `autoscaling_group_name` - (Required) The name or ARN of the group. +* `adjustment_type` - (Required) Specifies whether the `scaling_adjustment` is an absolute number or a percentage of the current capacity. Valid values are `ChangeInCapacity`, `ExactCapacity`, and `PercentChangeInCapacity`. +* `scaling_adjustment` - (Required) The number of instances by which to scale. `adjustment_type` determines the interpretation of this number (e.g., as an absolute number or as a percentage of the existing Auto Scaling group size). A positive increment adds to the current capacity and a negative value removes from the current capacity. +* `cooldown` - (Optional) The amount of time, in seconds, after a scaling activity completes and before the next scaling activity can start. +* `min_adjustment_step` - (Optional) Used with `adjustment_type` with the value `PercentChangeInCapacity`, the scaling policy changes the `desired_capacity` of the Auto Scaling group by at least the number of instances specified in the value. + +## Attribute Reference +* `arn` - The ARN assigned by AWS to the scaling policy. diff --git a/website/source/docs/providers/aws/r/cloudwatch_metric_alarm.html.markdown b/website/source/docs/providers/aws/r/cloudwatch_metric_alarm.html.markdown new file mode 100644 index 0000000000..e6ede269fc --- /dev/null +++ b/website/source/docs/providers/aws/r/cloudwatch_metric_alarm.html.markdown @@ -0,0 +1,75 @@ +--- +layout: "aws" +page_title: "AWS: cloudwatch_metric_alarm" +sidebar_current: "docs-aws-resource-cloudwatch-metric-alarm" +description: |- + Provides an AutoScaling Scaling Group resource. +--- + +# aws\_cloudwatch\_metric\_alarm + +Provides a CloudWatch Metric Alarm resource. + +## Example Usage +``` +resource "aws_cloudwatch_metric_alarm" "foobar" { + alarm_name = "terraform-test-foobar5" + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = "2" + metric_name = "CPUUtilization" + namespace = "AWS/EC2" + period = "120" + statistic = "Average" + threshold = "80" + alarm_description = "This metric monitor ec2 cpu utilization" + insufficient_data_actions = [] +} +``` + +## Example in Conjuction with Scaling Policies +``` +resource "aws_autoscaling_policy" "bat" { + name = "foobar3-terraform-test" + scaling_adjustment = 4 + adjustment_type = "ChangeInCapacity" + cooldown = 300 + autoscaling_group_name = "${aws_autoscaling_group.bar.name}" +} + +resource "aws_cloudwatch_metric_alarm" "bat" { + alarm_name = "terraform-test-foobar5" + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = "2" + metric_name = "CPUUtilization" + namespace = "AWS/EC2" + period = "120" + statistic = "Average" + threshold = "80" + alarm_description = "This metric monitor ec2 cpu utilization" + alarm_actions = ["${aws_autoscaling_policy.bat.arn}"] +} +``` +## Argument Reference + +See [related part of AWS Docs](http://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_PutMetricAlarm.html) +for details about valid values. + +The following arguments are supported: + +* `alarm_name` - (Required) The descriptive name for the alarm. This name must be unique within the user's AWS account +* `comparison_operator` - (Required) The arithmetic operation to use when comparing the specified Statistic and Threshold. The specified Statistic value is used as the first operand. Either of the following is supported: `GreaterThanOrEqualToThreshold`, `GreaterThanThreshold`, `LessThanThreshold`, `LessThanOrEqualToThreshold`. +* `evaluation_periods` - (Required) The number of periods over which data is compared to the specified threshold. +* `metric_name` - (Required) The name for the alarm's associated metric. + See docs for [supported metrics]([valid metrics](http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html)). +* `namespace` - (Required) The namespace for the alarm's associated metric. +* `period` - (Required) The period in seconds over which the specified `statistic` is applied. +* `statistic` - (Required) The statistic to apply to the alarm's associated metric. + Either of the following is supported: `SampleCount`, `Average`, `Sum`, `Minimum`, `Maximum` +* `threshold` - (Required) The value against which the specified statistic is compared. +* `actions_enabled` - (Optional) Indicates whether or not actions should be executed during any changes to the alarm's state. Defaults to `true`. +* `alarm_actions` - (Optional) The list of actions to execute when this alarm transitions into an ALARM state from any other state. Each action is specified as an Amazon Resource Number (ARN). +* `alarm_description` - (Optional) The description for the alarm. +* `dimensions` - (Optional) The dimensions for the alarm's associated metric. +* `insufficient_data_actions` - (Optional) The list of actions to execute when this alarm transitions into an INSUFFICIENT_DATA state from any other state. Each action is specified as an Amazon Resource Number (ARN). +* `ok_actions` - (Optional) The list of actions to execute when this alarm transitions into an OK state from any other state. Each action is specified as an Amazon Resource Number (ARN). +* `unit` - (Optional) The unit for the alarm's associated metric. diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 7afd00060b..801945dec9 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -21,6 +21,14 @@ aws_autoscaling_notification + > + aws_autoscaling_policy + + + > + aws_cloudwatch_metric_alarm + + > aws_customer_gateway From ad4ff531209a33a6b386490930e2b105b9dcfe95 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Wed, 17 Jun 2015 23:23:21 +0100 Subject: [PATCH 12/20] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d71af9703..7bfe3876a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ FEATURES: * **New provider: `azure`** [GH-2052, GH-2053, GH-2372, GH-2380] * **New resource: `aws_autoscaling_notification`** [GH-2197] + * **New resource: `aws_autoscaling_policy`** [GH-2201] + * **New resource: `aws_cloudwatch_metric_alarm`** [GH-2201] * **New resource: `aws_dynamodb_table`** [GH-2121] * **New resource: `aws_ecs_cluster`** [GH-1803] * **New resource: `aws_ecs_service`** [GH-1803] From 24ee2e5d53f67ec8e7d7b59cbe13f5bec61e698e Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Thu, 18 Jun 2015 08:39:08 -0500 Subject: [PATCH 13/20] remove debugging --- .../aws/resource_aws_security_group_rule_migrate_test.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/builtin/providers/aws/resource_aws_security_group_rule_migrate_test.go b/builtin/providers/aws/resource_aws_security_group_rule_migrate_test.go index 33783828f5..664f050393 100644 --- a/builtin/providers/aws/resource_aws_security_group_rule_migrate_test.go +++ b/builtin/providers/aws/resource_aws_security_group_rule_migrate_test.go @@ -7,11 +7,6 @@ import ( ) func TestAWSSecurityGroupRuleMigrateState(t *testing.T) { - // "id":"sg-4235098228", "from_port":"0", "source_security_group_id":"sg-11877275"} - - // 2015/06/16 16:04:21 terraform-provider-aws: 2015/06/16 16:04:21 [DEBUG] Attributes after migration: - - // map[string]string{"from_port":"0", "source_security_group_id":"sg-11877275", "id":"sg-3766347571", "security_group_id":"sg-13877277", "cidr_blocks.#":"0", "type":"ingress", "protocol":"-1", "self":"false", "to_port":"0"}, new id: sg-3766347571 cases := map[string]struct { StateVersion int ID string From 611741c1088333b23a1e5238bd10846d8ad59ca0 Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Thu, 18 Jun 2015 15:47:54 -0500 Subject: [PATCH 14/20] typo --- builtin/providers/aws/resource_aws_autoscaling_group.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/providers/aws/resource_aws_autoscaling_group.go b/builtin/providers/aws/resource_aws_autoscaling_group.go index 0fc62b4b25..bf9b30c6a2 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group.go @@ -408,7 +408,7 @@ func waitForASGCapacity(d *schema.ResourceData, meta interface{}) error { } wantELB := d.Get("min_elb_capacity").(int) - log.Printf("[DEBUG] Wanting for capacity: %d ASG, %d ELB", wantASG, wantELB) + log.Printf("[DEBUG] Waiting for capacity: %d ASG, %d ELB", wantASG, wantELB) return resource.Retry(waitForASGCapacityTimeout, func() error { g, err := getAwsAutoscalingGroup(d, meta) From 645a5aa55bf7060b86d0e40919803039801688f0 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Fri, 19 Jun 2015 11:23:59 -0500 Subject: [PATCH 15/20] add warning message to explain scenario of conflicting rules --- .../aws/resource_aws_security_group_rule.go | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/builtin/providers/aws/resource_aws_security_group_rule.go b/builtin/providers/aws/resource_aws_security_group_rule.go index aa634bdf1b..6a99fb84d8 100644 --- a/builtin/providers/aws/resource_aws_security_group_rule.go +++ b/builtin/providers/aws/resource_aws_security_group_rule.go @@ -94,6 +94,7 @@ func resourceAwsSecurityGroupRuleCreate(d *schema.ResourceData, meta interface{} ruleType := d.Get("type").(string) + var autherr error switch ruleType { case "ingress": log.Printf("[DEBUG] Authorizing security group %s %s rule: %s", @@ -109,13 +110,7 @@ func resourceAwsSecurityGroupRuleCreate(d *schema.ResourceData, meta interface{} req.GroupName = sg.GroupName } - _, err := conn.AuthorizeSecurityGroupIngress(req) - - if err != nil { - return fmt.Errorf( - "Error authorizing security group %s rules: %s", - "rules", err) - } + _, autherr = conn.AuthorizeSecurityGroupIngress(req) case "egress": log.Printf("[DEBUG] Authorizing security group %s %s rule: %#v", @@ -126,18 +121,28 @@ func resourceAwsSecurityGroupRuleCreate(d *schema.ResourceData, meta interface{} IPPermissions: []*ec2.IPPermission{perm}, } - _, err = conn.AuthorizeSecurityGroupEgress(req) - - if err != nil { - return fmt.Errorf( - "Error authorizing security group %s rules: %s", - "rules", err) - } + _, autherr = conn.AuthorizeSecurityGroupEgress(req) default: return fmt.Errorf("Security Group Rule must be type 'ingress' or type 'egress'") } + if autherr != nil { + if awsErr, ok := autherr.(awserr.Error); ok { + if awsErr.Code() == "InvalidPermission.Duplicate" { + return fmt.Errorf(`[WARN] A duplicate Security Group rule was found. This may be +a side effect of a now-fixed Terraform issue causing two security groups with +identical attributes but different source_security_group_ids to overwrite each +other in the state. See https://github.com/hashicorp/teraform/pull/2376 for more +information and instructions for recovery. Error message: %s`, awsErr.Message()) + } + } + + return fmt.Errorf( + "Error authorizing security group rule type %s: %s", + ruleType, autherr) + } + d.SetId(ipPermissionIDHash(ruleType, perm)) return resourceAwsSecurityGroupRuleRead(d, meta) From 2410824fc9bd5b25a8519a15ba65f41364199b0b Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Fri, 19 Jun 2015 11:39:50 -0500 Subject: [PATCH 16/20] provider/azure: Fix SQL client name to match upstream name was changed in https://github.com/Azure/azure-sdk-for-go/commit/4f4636621e7b3a1cd83c7521d8cef9a56c68fdf7 --- builtin/providers/azure/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/providers/azure/config.go b/builtin/providers/azure/config.go index 52c69d40cb..8e0e378dfd 100644 --- a/builtin/providers/azure/config.go +++ b/builtin/providers/azure/config.go @@ -37,7 +37,7 @@ type Client struct { osImageClient osimage.OSImageClient - sqlClient sql.SqlDatabaseClient + sqlClient sql.SQLDatabaseClient storageServiceClient storageservice.StorageServiceClient From 44eb55f8f66788b9a2e709848b7cdb3f3d81665d Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Fri, 19 Jun 2015 11:50:10 -0500 Subject: [PATCH 17/20] update link to actually work --- builtin/providers/aws/resource_aws_security_group_rule.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/providers/aws/resource_aws_security_group_rule.go b/builtin/providers/aws/resource_aws_security_group_rule.go index 6a99fb84d8..ce8b20498a 100644 --- a/builtin/providers/aws/resource_aws_security_group_rule.go +++ b/builtin/providers/aws/resource_aws_security_group_rule.go @@ -133,7 +133,7 @@ func resourceAwsSecurityGroupRuleCreate(d *schema.ResourceData, meta interface{} return fmt.Errorf(`[WARN] A duplicate Security Group rule was found. This may be a side effect of a now-fixed Terraform issue causing two security groups with identical attributes but different source_security_group_ids to overwrite each -other in the state. See https://github.com/hashicorp/teraform/pull/2376 for more +other in the state. See https://github.com/hashicorp/terraform/pull/2376 for more information and instructions for recovery. Error message: %s`, awsErr.Message()) } } From 9b5c99ba2862d64b83580298e3b657d390376c06 Mon Sep 17 00:00:00 2001 From: aznashwan Date: Fri, 19 Jun 2015 19:52:36 +0300 Subject: [PATCH 18/20] Added affinity group resource. --- builtin/providers/azure/config.go | 5 + builtin/providers/azure/provider.go | 1 + .../azure/resource_azure_affinity_group.go | 168 ++++++++++++++++++ .../resource_azure_affinity_group_test.go | 121 +++++++++++++ .../azure/r/affinity_group.html.markdown | 42 +++++ website/source/layouts/azure.erb | 4 + 6 files changed, 341 insertions(+) create mode 100644 builtin/providers/azure/resource_azure_affinity_group.go create mode 100644 builtin/providers/azure/resource_azure_affinity_group_test.go create mode 100644 website/source/docs/providers/azure/r/affinity_group.html.markdown diff --git a/builtin/providers/azure/config.go b/builtin/providers/azure/config.go index 8e0e378dfd..fff31853d1 100644 --- a/builtin/providers/azure/config.go +++ b/builtin/providers/azure/config.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/Azure/azure-sdk-for-go/management" + "github.com/Azure/azure-sdk-for-go/management/affinitygroup" "github.com/Azure/azure-sdk-for-go/management/hostedservice" "github.com/Azure/azure-sdk-for-go/management/networksecuritygroup" "github.com/Azure/azure-sdk-for-go/management/osimage" @@ -31,6 +32,8 @@ type Config struct { type Client struct { mgmtClient management.Client + affinityGroupClient affinitygroup.AffinityGroupClient + hostedServiceClient hostedservice.HostedServiceClient secGroupClient networksecuritygroup.SecurityGroupClient @@ -107,6 +110,7 @@ func (c *Config) NewClientFromSettingsFile() (*Client, error) { return &Client{ mgmtClient: mc, + affinityGroupClient: affinitygroup.NewClient(mc), hostedServiceClient: hostedservice.NewClient(mc), secGroupClient: networksecuritygroup.NewClient(mc), osImageClient: osimage.NewClient(mc), @@ -130,6 +134,7 @@ func (c *Config) NewClient() (*Client, error) { return &Client{ mgmtClient: mc, + affinityGroupClient: affinitygroup.NewClient(mc), hostedServiceClient: hostedservice.NewClient(mc), secGroupClient: networksecuritygroup.NewClient(mc), osImageClient: osimage.NewClient(mc), diff --git a/builtin/providers/azure/provider.go b/builtin/providers/azure/provider.go index a6be93f5ca..3654884153 100644 --- a/builtin/providers/azure/provider.go +++ b/builtin/providers/azure/provider.go @@ -33,6 +33,7 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "azure_instance": resourceAzureInstance(), + "azure_affinity_group": resourceAzureAffinityGroup(), "azure_data_disk": resourceAzureDataDisk(), "azure_sql_database_server": resourceAzureSqlDatabaseServer(), "azure_sql_database_service": resourceAzureSqlDatabaseService(), diff --git a/builtin/providers/azure/resource_azure_affinity_group.go b/builtin/providers/azure/resource_azure_affinity_group.go new file mode 100644 index 0000000000..5dd5bced42 --- /dev/null +++ b/builtin/providers/azure/resource_azure_affinity_group.go @@ -0,0 +1,168 @@ +package azure + +import ( + "fmt" + "log" + + "github.com/Azure/azure-sdk-for-go/management" + "github.com/Azure/azure-sdk-for-go/management/affinitygroup" + "github.com/hashicorp/terraform/helper/schema" +) + +// resourceAzureAffinityGroup returns the *schema.Resource associated to a +// resource affinity group on Azure. +func resourceAzureAffinityGroup() *schema.Resource { + return &schema.Resource{ + Create: resourceAzureAffinityGroupCreate, + Read: resourceAzureAffinityGroupRead, + Update: resourceAzureAffinityGroupUpdate, + Exists: resourceAzureAffinityGroupExists, + Delete: resourceAzureAffinityGroupDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "location": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "label": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +// resourceAzureAffinityGroupCreate does all the necessary API calls to +// create an affinity group on Azure. +func resourceAzureAffinityGroupCreate(d *schema.ResourceData, meta interface{}) error { + affinityGroupClient := meta.(*Client).affinityGroupClient + + log.Println("[INFO] Begun creating Azure Affinity Group creation request.") + name := d.Get("name").(string) + params := affinitygroup.CreateAffinityGroupParams{ + Name: name, + Label: d.Get("label").(string), + Location: d.Get("location").(string), + } + + if desc, ok := d.GetOk("description"); ok { + params.Description = desc.(string) + } + + log.Println("[INFO] Sending Affinity Group creation request to Azure.") + err := affinityGroupClient.CreateAffinityGroup(params) + if err != nil { + return fmt.Errorf("Error issuing Azure Affinity Group creation: %s", err) + } + + d.SetId(name) + return nil +} + +// resourceAzureAffinityGroupRead does all the necessary API calls to +// read the state of the affinity group off Azure. +func resourceAzureAffinityGroupRead(d *schema.ResourceData, meta interface{}) error { + affinityGroupClient := meta.(*Client).affinityGroupClient + + log.Println("[INFO] Issuing Azure Affinity Group list request.") + affinityGroups, err := affinityGroupClient.ListAffinityGroups() + if err != nil { + return fmt.Errorf("Error obtaining Affinity Group list off Azure: %s", err) + } + + var found bool + name := d.Get("name").(string) + for _, group := range affinityGroups.AffinityGroups { + if group.Name == name { + found = true + d.Set("location", group.Location) + d.Set("label", group.Label) + d.Set("description", group.Description) + break + } + } + + if !found { + // it means the affinity group has been deleted in the meantime, so we + // must stop tracking it: + d.SetId("") + } + + return nil +} + +// resourceAzureAffinityGroupUpdate does all the necessary API calls to +// update the state of the affinity group on Azure. +func resourceAzureAffinityGroupUpdate(d *schema.ResourceData, meta interface{}) error { + affinityGroupClient := meta.(*Client).affinityGroupClient + + name := d.Get("name").(string) + clabel := d.HasChange("label") + cdesc := d.HasChange("description") + if clabel || cdesc { + log.Println("[INFO] Beginning Affinity Group update process.") + params := affinitygroup.UpdateAffinityGroupParams{} + + if clabel { + params.Label = d.Get("label").(string) + } + if cdesc { + params.Description = d.Get("description").(string) + } + + log.Println("[INFO] Sending Affinity Group update request to Azure.") + err := affinityGroupClient.UpdateAffinityGroup(name, params) + if err != nil { + return fmt.Errorf("Error updating Azure Affinity Group parameters: %s", err) + } + } + + return nil +} + +// resourceAzureAffinityGroupExists does all the necessary API calls to +// check for the existence of the affinity group on Azure. +func resourceAzureAffinityGroupExists(d *schema.ResourceData, meta interface{}) (bool, error) { + affinityGroupClient := meta.(*Client).affinityGroupClient + + log.Println("[INFO] Issuing Azure Affinity Group get request.") + name := d.Get("name").(string) + _, err := affinityGroupClient.GetAffinityGroup(name) + if err != nil { + if management.IsResourceNotFoundError(err) { + // it means that the affinity group has been deleted in the + // meantime, so we must untrack it from the schema: + d.SetId("") + return false, nil + } else { + return false, fmt.Errorf("Error getting Affinity Group off Azure: %s", err) + } + } + + return true, nil +} + +// resourceAzureAffinityGroupDelete does all the necessary API calls to +// delete the affinity group off Azure. +func resourceAzureAffinityGroupDelete(d *schema.ResourceData, meta interface{}) error { + affinityGroupClient := meta.(*Client).affinityGroupClient + + log.Println("[INFO] Sending Affinity Group deletion request to Azure.") + name := d.Get("name").(string) + err := affinityGroupClient.DeleteAffinityGroup(name) + if err != nil { + return fmt.Errorf("Error deleting Azure Affinity Group: %s", err) + } + + return nil +} diff --git a/builtin/providers/azure/resource_azure_affinity_group_test.go b/builtin/providers/azure/resource_azure_affinity_group_test.go new file mode 100644 index 0000000000..1cc5211c48 --- /dev/null +++ b/builtin/providers/azure/resource_azure_affinity_group_test.go @@ -0,0 +1,121 @@ +package azure + +import ( + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/management" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAzureAffinityGroupBasic(t *testing.T) { + name := "azure_affinity_group.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureAffinityGroupDestroyed, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureAffinityGroupConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureAffinityGroupExists(name), + resource.TestCheckResourceAttr(name, "name", "terraform-testing-group"), + resource.TestCheckResourceAttr(name, "location", "West US"), + resource.TestCheckResourceAttr(name, "label", "A nice label."), + resource.TestCheckResourceAttr(name, "description", "A nice description."), + ), + }, + }, + }) +} + +func TestAccAzureAffinityGroupUpdate(t *testing.T) { + name := "azure_affinity_group.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureAffinityGroupDestroyed, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureAffinityGroupConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureAffinityGroupExists(name), + resource.TestCheckResourceAttr(name, "name", "terraform-testing-group"), + resource.TestCheckResourceAttr(name, "location", "West US"), + resource.TestCheckResourceAttr(name, "label", "A nice label."), + resource.TestCheckResourceAttr(name, "description", "A nice description."), + ), + }, + resource.TestStep{ + Config: testAccAzureAffinityGroupUpdateConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureAffinityGroupExists(name), + resource.TestCheckResourceAttr(name, "name", "terraform-testing-group"), + resource.TestCheckResourceAttr(name, "location", "West US"), + resource.TestCheckResourceAttr(name, "label", "An even nicer label."), + resource.TestCheckResourceAttr(name, "description", "An even nicer description."), + ), + }, + }, + }) +} + +func testAccCheckAzureAffinityGroupExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Affinity Group resource %q doesn't exist.", name) + } + + if resource.Primary.ID == "" { + return fmt.Errorf("Affinity Group resource %q ID not set.", name) + } + + affinityGroupClient := testAccProvider.Meta().(*Client).affinityGroupClient + _, err := affinityGroupClient.GetAffinityGroup(resource.Primary.ID) + return err + } +} + +func testAccCheckAzureAffinityGroupDestroyed(s *terraform.State) error { + var err error + affinityGroupClient := testAccProvider.Meta().(*Client).affinityGroupClient + + for _, resource := range s.RootModule().Resources { + if resource.Type != "azure_affinity_group" { + continue + } + + if resource.Primary.ID == "" { + return fmt.Errorf("Affinity Group resource ID not set.") + } + + _, err = affinityGroupClient.GetAffinityGroup(resource.Primary.ID) + if !management.IsResourceNotFoundError(err) { + return err + } + } + + return nil +} + +const testAccAzureAffinityGroupConfig = ` +resource "azure_affinity_group" "foo" { + name = "terraform-testing-group" + location = "West US" + label = "A nice label." + description = "A nice description." +} +` + +const testAccAzureAffinityGroupUpdateConfig = ` +resource "azure_affinity_group" "foo" { + name = "terraform-testing-group" + location = "West US" + label = "An even nicer label." + description = "An even nicer description." +} +` diff --git a/website/source/docs/providers/azure/r/affinity_group.html.markdown b/website/source/docs/providers/azure/r/affinity_group.html.markdown new file mode 100644 index 0000000000..906f730707 --- /dev/null +++ b/website/source/docs/providers/azure/r/affinity_group.html.markdown @@ -0,0 +1,42 @@ +--- +layout: "azure" +page_title: "Azure: azure_affinity_group" +sidebar_current: "docs-azure-affinity-group" +description: |- + Creates a new affinity group on Azure. +--- + +# azure\_affinity\_group + +Creates a new affinity group on Azure. + +## Example Usage + +``` +resource "azure_affinity_group" "terraform-main-group" { + name = "terraform-group" + location = "North Europe" + label = "tf-group-01" + description = "Affinity group created by Terraform." +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the affinity group. Must be unique on your + Azure subscription. + +* `location` - (Required) The location where the affinity group should be created. + For a list of all Azure locations, please consult [this link](http://azure.microsoft.com/en-us/regions/). + +* `label` - (Required) A label to be used for tracking purposes. + +* `description` - (Optional) A description for the affinity group. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The affinity group ID. Coincides with the given `name`. diff --git a/website/source/layouts/azure.erb b/website/source/layouts/azure.erb index 4fc096f0e4..9525c45c9b 100644 --- a/website/source/layouts/azure.erb +++ b/website/source/layouts/azure.erb @@ -13,6 +13,10 @@ > Resources