diff --git a/CHANGELOG.md b/CHANGELOG.md
index bcf9e9f55..50b5a41c2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,16 @@
-## 0.3.1 (unreleased)
+## 0.3.2 (unreleased)
+
+
+
+## 0.3.1 (October 21, 2014)
+
+IMPROVEMENTS:
+
+ * providers/aws: Support tags for security groups.
+ * providers/google: Add "external\_address" to network attributes [GH-454]
+ * providers/google: External address is used as default connection host. [GH-454]
+ * providers/heroku: Support `locked` and `personal` booleans on organization
+ settings. [GH-406]
BUG FIXES:
@@ -9,10 +21,34 @@ BUG FIXES:
computed so the value is still unknown.
* core: If a resource fails to create and has provisioners, it is
marked as tainted. [GH-434]
+ * core: Set types are validated to be sets. [GH-413]
+ * core: String types are validated properly. [GH-460]
+ * core: Fix crash case when destroying with tainted resources. [GH-412]
+ * core: Don't execute provisioners in some cases on destroy.
+ * core: Inherited provider configurations will be properly interpolated. [GH-418]
+ * core: Refresh works properly if there are outputs that depend on resources
+ that aren't yet created. [GH-483]
* providers/aws: Refresh of launch configs and autoscale groups load
the correct data and don't incorrectly recreate themselves. [GH-425]
* providers/aws: Fix case where ELB would incorrectly plan to modify
listeners (with the same data) in some cases.
+ * providers/aws: Retry destroying internet gateway for some amount of time
+ if there is a dependency violation since it is probably just eventual
+ consistency (public facing resources being destroyed). [GH-447]
+ * providers/aws: Retry deleting security groups for some amount of time
+ if there is a dependency violation since it is probably just eventual
+ consistency. [GH-436]
+ * providers/aws: Retry deleting subnet for some amount of time if there is a
+ dependency violation since probably asynchronous destroy events take
+ place still. [GH-449]
+ * providers/aws: Drain autoscale groups before deleting. [GH-435]
+ * providers/aws: Fix crash case if launch config is manually deleted. [GH-421]
+ * providers/aws: Disassociate EIP before destroying.
+ * providers/aws: ELB treats subnets as a set.
+ * providers/aws: Fix case where in a destroy/create tags weren't reapplied. [GH-464]
+ * providers/aws: Fix incorrect/erroneous apply cases around security group
+ rules. [GH-457]
+ * providers/consul: Fix regression where `key` param changed to `keys. [GH-475]
## 0.3.0 (October 14, 2014)
diff --git a/builtin/providers/aws/resource_aws_autoscaling_group.go b/builtin/providers/aws/resource_aws_autoscaling_group.go
index 1aebfd9a0..54c744989 100644
--- a/builtin/providers/aws/resource_aws_autoscaling_group.go
+++ b/builtin/providers/aws/resource_aws_autoscaling_group.go
@@ -3,8 +3,10 @@ package aws
import (
"fmt"
"log"
+ "time"
"github.com/hashicorp/terraform/helper/hashcode"
+ "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"github.com/mitchellh/goamz/autoscaling"
)
@@ -196,6 +198,22 @@ func resourceAwsAutoscalingGroupDelete(d *schema.ResourceData, meta interface{})
p := meta.(*ResourceProvider)
autoscalingconn := p.autoscalingconn
+ // Read the autoscaling group first. If it doesn't exist, we're done.
+ // We need the group in order to check if there are instances attached.
+ // If so, we need to remove those first.
+ g, err := getAwsAutoscalingGroup(d, meta)
+ if err != nil {
+ return err
+ }
+ if g == nil {
+ return nil
+ }
+ if len(g.Instances) > 0 || g.DesiredCapacity > 0 {
+ if err := resourceAwsAutoscalingGroupDrain(d, meta); err != nil {
+ return err
+ }
+ }
+
log.Printf("[DEBUG] AutoScaling Group destroy: %v", d.Id())
deleteopts := autoscaling.DeleteAutoScalingGroup{Name: d.Id()}
@@ -208,8 +226,7 @@ func resourceAwsAutoscalingGroupDelete(d *schema.ResourceData, meta interface{})
deleteopts.ForceDelete = true
}
- _, err := autoscalingconn.DeleteAutoScalingGroup(&deleteopts)
- if err != nil {
+ if _, err := autoscalingconn.DeleteAutoScalingGroup(&deleteopts); err != nil {
autoscalingerr, ok := err.(*autoscaling.Error)
if ok && autoscalingerr.Code == "InvalidGroup.NotFound" {
return nil
@@ -222,35 +239,14 @@ func resourceAwsAutoscalingGroupDelete(d *schema.ResourceData, meta interface{})
}
func resourceAwsAutoscalingGroupRead(d *schema.ResourceData, meta interface{}) error {
- p := meta.(*ResourceProvider)
- autoscalingconn := p.autoscalingconn
-
- describeOpts := autoscaling.DescribeAutoScalingGroups{
- Names: []string{d.Id()},
- }
-
- log.Printf("[DEBUG] AutoScaling Group describe configuration: %#v", describeOpts)
- describeGroups, err := autoscalingconn.DescribeAutoScalingGroups(&describeOpts)
+ g, err := getAwsAutoscalingGroup(d, meta)
if err != nil {
- autoscalingerr, ok := err.(*autoscaling.Error)
- if ok && autoscalingerr.Code == "InvalidGroup.NotFound" {
- d.SetId("")
- return nil
- }
-
- return fmt.Errorf("Error retrieving AutoScaling groups: %s", err)
+ return err
}
-
- // Verify AWS returned our sg
- if len(describeGroups.AutoScalingGroups) != 1 ||
- describeGroups.AutoScalingGroups[0].Name != d.Id() {
- if err != nil {
- return fmt.Errorf("Unable to find AutoScaling group: %#v", describeGroups.AutoScalingGroups)
- }
+ if g == nil {
+ return nil
}
- g := describeGroups.AutoScalingGroups[0]
-
d.Set("availability_zones", g.AvailabilityZones)
d.Set("default_cooldown", g.DefaultCooldown)
d.Set("desired_capacity", g.DesiredCapacity)
@@ -265,3 +261,76 @@ func resourceAwsAutoscalingGroupRead(d *schema.ResourceData, meta interface{}) e
return nil
}
+
+func getAwsAutoscalingGroup(
+ d *schema.ResourceData,
+ meta interface{}) (*autoscaling.AutoScalingGroup, error) {
+ p := meta.(*ResourceProvider)
+ autoscalingconn := p.autoscalingconn
+
+ describeOpts := autoscaling.DescribeAutoScalingGroups{
+ Names: []string{d.Id()},
+ }
+
+ log.Printf("[DEBUG] AutoScaling Group describe configuration: %#v", describeOpts)
+ describeGroups, err := autoscalingconn.DescribeAutoScalingGroups(&describeOpts)
+ if err != nil {
+ autoscalingerr, ok := err.(*autoscaling.Error)
+ if ok && autoscalingerr.Code == "InvalidGroup.NotFound" {
+ d.SetId("")
+ return nil, nil
+ }
+
+ return nil, fmt.Errorf("Error retrieving AutoScaling groups: %s", err)
+ }
+
+ // Verify AWS returned our sg
+ if len(describeGroups.AutoScalingGroups) != 1 ||
+ describeGroups.AutoScalingGroups[0].Name != d.Id() {
+ if err != nil {
+ return nil, fmt.Errorf(
+ "Unable to find AutoScaling group: %#v",
+ describeGroups.AutoScalingGroups)
+ }
+ }
+
+ return &describeGroups.AutoScalingGroups[0], nil
+}
+
+func resourceAwsAutoscalingGroupDrain(d *schema.ResourceData, meta interface{}) error {
+ p := meta.(*ResourceProvider)
+ autoscalingconn := p.autoscalingconn
+
+ // First, set the capacity to zero so the group will drain
+ log.Printf("[DEBUG] Reducing autoscaling group capacity to zero")
+ opts := autoscaling.UpdateAutoScalingGroup{
+ Name: d.Id(),
+ DesiredCapacity: 0,
+ MinSize: 0,
+ MaxSize: 0,
+ SetDesiredCapacity: true,
+ SetMinSize: true,
+ SetMaxSize: true,
+ }
+ if _, err := autoscalingconn.UpdateAutoScalingGroup(&opts); err != nil {
+ return fmt.Errorf("Error setting capacity to zero to drain: %s", err)
+ }
+
+ // Next, wait for the autoscale group to drain
+ log.Printf("[DEBUG] Waiting for group to have zero instances")
+ return resource.Retry(10*time.Minute, func() error {
+ g, err := getAwsAutoscalingGroup(d, meta)
+ if err != nil {
+ return resource.RetryError{err}
+ }
+ if g == nil {
+ return nil
+ }
+
+ if len(g.Instances) == 0 {
+ return nil
+ }
+
+ return fmt.Errorf("group still has %d instances", len(g.Instances))
+ })
+}
diff --git a/builtin/providers/aws/resource_aws_eip.go b/builtin/providers/aws/resource_aws_eip.go
index d7c9382b4..5a70ebacb 100644
--- a/builtin/providers/aws/resource_aws_eip.go
+++ b/builtin/providers/aws/resource_aws_eip.go
@@ -36,6 +36,11 @@ func resourceAwsEip() *schema.Resource {
Computed: true,
},
+ "association_id": &schema.Schema{
+ Type: schema.TypeString,
+ Computed: true,
+ },
+
"domain": &schema.Schema{
Type: schema.TypeString,
Computed: true,
@@ -127,15 +132,55 @@ func resourceAwsEipUpdate(d *schema.ResourceData, meta interface{}) error {
}
func resourceAwsEipDelete(d *schema.ResourceData, meta interface{}) error {
- stateConf := &resource.StateChangeConf{
- Pending: []string{"pending"},
- Target: "destroyed",
- Refresh: resourceEipDeleteRefreshFunc(d, meta),
- Timeout: 3 * time.Minute,
- MinTimeout: 1 * time.Second,
+ p := meta.(*ResourceProvider)
+ ec2conn := p.ec2conn
+
+ if err := resourceAwsEipRead(d, meta); err != nil {
+ return err
}
- _, err := stateConf.WaitForState()
- return err
+ if d.Id() == "" {
+ // This might happen from the read
+ return nil
+ }
+
+ // If we are attached to an instance, detach first.
+ if d.Get("instance").(string) != "" {
+ log.Printf("[DEBUG] Disassociating EIP: %s", d.Id())
+ var err error
+ switch resourceAwsEipDomain(d) {
+ case "vpc":
+ _, err = ec2conn.DisassociateAddress(d.Get("association_id").(string))
+ case "standard":
+ _, err = ec2conn.DisassociateAddressClassic(d.Get("public_ip").(string))
+ }
+ if err != nil {
+ return err
+ }
+ }
+
+ domain := resourceAwsEipDomain(d)
+ return resource.Retry(3*time.Minute, func() error {
+ var err error
+ switch domain {
+ case "vpc":
+ log.Printf(
+ "[DEBUG] EIP release (destroy) address allocation: %v",
+ d.Id())
+ _, err = ec2conn.ReleaseAddress(d.Id())
+ case "standard":
+ log.Printf("[DEBUG] EIP release (destroy) address: %v", d.Id())
+ _, err = ec2conn.ReleasePublicAddress(d.Id())
+ }
+
+ if err == nil {
+ return nil
+ }
+ if _, ok := err.(*ec2.Error); !ok {
+ return resource.RetryError{err}
+ }
+
+ return err
+ })
}
func resourceAwsEipRead(d *schema.ResourceData, meta interface{}) error {
@@ -178,6 +223,7 @@ func resourceAwsEipRead(d *schema.ResourceData, meta interface{}) error {
address := describeAddresses.Addresses[0]
+ d.Set("association_id", address.AssociationId)
d.Set("instance", address.InstanceId)
d.Set("public_ip", address.PublicIp)
d.Set("private_ip", address.PrivateIpAddress)
@@ -196,38 +242,3 @@ func resourceAwsEipDomain(d *schema.ResourceData) string {
return "standard"
}
-
-func resourceEipDeleteRefreshFunc(
- d *schema.ResourceData,
- meta interface{}) resource.StateRefreshFunc {
- p := meta.(*ResourceProvider)
- ec2conn := p.ec2conn
- domain := resourceAwsEipDomain(d)
-
- return func() (interface{}, string, error) {
- var err error
- if domain == "vpc" {
- log.Printf(
- "[DEBUG] EIP release (destroy) address allocation: %v",
- d.Id())
- _, err = ec2conn.ReleaseAddress(d.Id())
- } else {
- log.Printf("[DEBUG] EIP release (destroy) address: %v", d.Id())
- _, err = ec2conn.ReleasePublicAddress(d.Id())
- }
-
- if err == nil {
- return d, "destroyed", nil
- }
-
- ec2err, ok := err.(*ec2.Error)
- if !ok {
- return d, "error", err
- }
- if ec2err.Code != "InvalidIPAddress.InUse" {
- return d, "error", err
- }
-
- return d, "pending", nil
- }
-}
diff --git a/builtin/providers/aws/resource_aws_elb.go b/builtin/providers/aws/resource_aws_elb.go
index 9fffd321e..353a675d6 100644
--- a/builtin/providers/aws/resource_aws_elb.go
+++ b/builtin/providers/aws/resource_aws_elb.go
@@ -59,11 +59,14 @@ func resourceAwsElb() *schema.Resource {
// TODO: could be not ForceNew
"subnets": &schema.Schema{
- Type: schema.TypeList,
+ Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
ForceNew: true,
Computed: true,
+ Set: func(v interface{}) int {
+ return hashcode.String(v.(string))
+ },
},
// TODO: could be not ForceNew
@@ -200,7 +203,7 @@ func resourceAwsElbCreate(d *schema.ResourceData, meta interface{}) error {
}
if v, ok := d.GetOk("subnets"); ok {
- elbOpts.Subnets = expandStringList(v.([]interface{}))
+ elbOpts.Subnets = expandStringList(v.(*schema.Set).List())
}
log.Printf("[DEBUG] ELB create configuration: %#v", elbOpts)
diff --git a/builtin/providers/aws/resource_aws_instance_test.go b/builtin/providers/aws/resource_aws_instance_test.go
index 48c21eb4e..ff7087732 100644
--- a/builtin/providers/aws/resource_aws_instance_test.go
+++ b/builtin/providers/aws/resource_aws_instance_test.go
@@ -64,8 +64,6 @@ func TestAccAWSInstance_normal(t *testing.T) {
})
}
-
-
func TestAccAWSInstance_sourceDestCheck(t *testing.T) {
var v ec2.Instance
diff --git a/builtin/providers/aws/resource_aws_internet_gateway.go b/builtin/providers/aws/resource_aws_internet_gateway.go
index a13892c60..fc23ebb06 100644
--- a/builtin/providers/aws/resource_aws_internet_gateway.go
+++ b/builtin/providers/aws/resource_aws_internet_gateway.go
@@ -81,14 +81,26 @@ func resource_aws_internet_gateway_destroy(
}
log.Printf("[INFO] Deleting Internet Gateway: %s", s.ID)
- if _, err := ec2conn.DeleteInternetGateway(s.ID); err != nil {
- ec2err, ok := err.(*ec2.Error)
- if ok && ec2err.Code == "InvalidInternetGatewayID.NotFound" {
- return nil
+ return resource.Retry(5*time.Minute, func() error {
+ _, err := ec2conn.DeleteInternetGateway(s.ID)
+ if err != nil {
+ ec2err, ok := err.(*ec2.Error)
+ if !ok {
+ return err
+ }
+
+ switch ec2err.Code {
+ case "InvalidInternetGatewayID.NotFound":
+ return nil
+ case "DependencyViolation":
+ return err // retry
+ default:
+ return resource.RetryError{err}
+ }
}
return fmt.Errorf("Error deleting internet gateway: %s", err)
- }
+ })
// Wait for the internet gateway to actually delete
log.Printf("[DEBUG] Waiting for internet gateway (%s) to delete", s.ID)
diff --git a/builtin/providers/aws/resource_aws_launch_configuration.go b/builtin/providers/aws/resource_aws_launch_configuration.go
index 584bad164..0cb80c395 100644
--- a/builtin/providers/aws/resource_aws_launch_configuration.go
+++ b/builtin/providers/aws/resource_aws_launch_configuration.go
@@ -144,15 +144,16 @@ func resourceAwsLaunchConfigurationRead(d *schema.ResourceData, meta interface{}
if err != nil {
return fmt.Errorf("Error retrieving launch configuration: %s", err)
}
+ if len(describConfs.LaunchConfigurations) == 0 {
+ d.SetId("")
+ return nil
+ }
// Verify AWS returned our launch configuration
- if len(describConfs.LaunchConfigurations) != 1 ||
- describConfs.LaunchConfigurations[0].Name != d.Id() {
- if err != nil {
- return fmt.Errorf(
- "Unable to find launch configuration: %#v",
- describConfs.LaunchConfigurations)
- }
+ if describConfs.LaunchConfigurations[0].Name != d.Id() {
+ return fmt.Errorf(
+ "Unable to find launch configuration: %#v",
+ describConfs.LaunchConfigurations)
}
lc := describConfs.LaunchConfigurations[0]
diff --git a/builtin/providers/aws/resource_aws_security_group.go b/builtin/providers/aws/resource_aws_security_group.go
index 88627a0b8..4be7ca484 100644
--- a/builtin/providers/aws/resource_aws_security_group.go
+++ b/builtin/providers/aws/resource_aws_security_group.go
@@ -66,14 +66,18 @@ func resourceAwsSecurityGroup() *schema.Resource {
},
"security_groups": &schema.Schema{
- Type: schema.TypeList,
+ Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
+ Set: func(v interface{}) int {
+ return hashcode.String(v.(string))
+ },
},
"self": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
+ Default: false,
},
},
},
@@ -84,6 +88,8 @@ func resourceAwsSecurityGroup() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
+
+ "tags": tagsSchema(),
},
}
}
@@ -110,7 +116,7 @@ func resourceAwsSecurityGroupIngressHash(v interface{}) int {
}
}
if v, ok := m["security_groups"]; ok {
- vs := v.([]interface{})
+ vs := v.(*schema.Set).List()
s := make([]string, len(vs))
for i, raw := range vs {
s[i] = raw.(string)
@@ -226,6 +232,12 @@ func resourceAwsSecurityGroupUpdate(d *schema.ResourceData, meta interface{}) er
}
}
+ if err := setTags(ec2conn, d); err != nil {
+ return err
+ } else {
+ d.SetPartial("tags")
+ }
+
return nil
}
@@ -235,15 +247,28 @@ func resourceAwsSecurityGroupDelete(d *schema.ResourceData, meta interface{}) er
log.Printf("[DEBUG] Security Group destroy: %v", d.Id())
- _, err := ec2conn.DeleteSecurityGroup(ec2.SecurityGroup{Id: d.Id()})
- if err != nil {
- ec2err, ok := err.(*ec2.Error)
- if ok && ec2err.Code == "InvalidGroup.NotFound" {
- return nil
- }
- }
+ return resource.Retry(5*time.Minute, func() error {
+ _, err := ec2conn.DeleteSecurityGroup(ec2.SecurityGroup{Id: d.Id()})
+ if err != nil {
+ ec2err, ok := err.(*ec2.Error)
+ if !ok {
+ return err
+ }
- return err
+ switch ec2err.Code {
+ case "InvalidGroup.NotFound":
+ return nil
+ case "DependencyViolation":
+ // If it is a dependency violation, we want to retry
+ return err
+ default:
+ // Any other error, we want to quit the retry loop immediately
+ return resource.RetryError{err}
+ }
+ }
+
+ return nil
+ })
}
func resourceAwsSecurityGroupRead(d *schema.ResourceData, meta interface{}) error {
@@ -262,15 +287,28 @@ func resourceAwsSecurityGroupRead(d *schema.ResourceData, meta interface{}) erro
sg := sgRaw.(*ec2.SecurityGroupInfo)
// Gather our ingress rules
- ingressRules := make([]map[string]interface{}, len(sg.IPPerms))
- for i, perm := range sg.IPPerms {
- n := make(map[string]interface{})
- n["from_port"] = perm.FromPort
- n["protocol"] = perm.Protocol
- n["to_port"] = perm.ToPort
+ ingressMap := make(map[string]map[string]interface{})
+ for _, perm := range sg.IPPerms {
+ k := fmt.Sprintf("%s-%d-%d", perm.Protocol, perm.FromPort, perm.ToPort)
+ m, ok := ingressMap[k]
+ if !ok {
+ m = make(map[string]interface{})
+ ingressMap[k] = m
+ }
+
+ m["from_port"] = perm.FromPort
+ m["to_port"] = perm.ToPort
+ m["protocol"] = perm.Protocol
if len(perm.SourceIPs) > 0 {
- n["cidr_blocks"] = perm.SourceIPs
+ raw, ok := m["cidr_blocks"]
+ if !ok {
+ raw = make([]string, 0, len(perm.SourceIPs))
+ }
+ list := raw.([]string)
+
+ list = append(list, perm.SourceIPs...)
+ m["cidr_blocks"] = list
}
var groups []string
@@ -280,14 +318,24 @@ func resourceAwsSecurityGroupRead(d *schema.ResourceData, meta interface{}) erro
for i, id := range groups {
if id == d.Id() {
groups[i], groups = groups[len(groups)-1], groups[:len(groups)-1]
- n["self"] = true
+ m["self"] = true
}
}
- if len(groups) > 0 {
- n["security_groups"] = groups
- }
- ingressRules[i] = n
+ if len(groups) > 0 {
+ raw, ok := m["security_groups"]
+ if !ok {
+ raw = make([]string, 0, len(groups))
+ }
+ list := raw.([]string)
+
+ list = append(list, groups...)
+ m["security_groups"] = list
+ }
+ }
+ ingressRules := make([]map[string]interface{}, 0, len(ingressMap))
+ for _, m := range ingressMap {
+ ingressRules = append(ingressRules, m)
}
d.Set("description", sg.Description)
@@ -295,6 +343,7 @@ func resourceAwsSecurityGroupRead(d *schema.ResourceData, meta interface{}) erro
d.Set("vpc_id", sg.VpcId)
d.Set("owner_id", sg.OwnerId)
d.Set("ingress", ingressRules)
+ d.Set("tags", tagsToMap(sg.Tags))
return nil
}
diff --git a/builtin/providers/aws/resource_aws_security_group_test.go b/builtin/providers/aws/resource_aws_security_group_test.go
index 0774b71dd..5ff4d4c49 100644
--- a/builtin/providers/aws/resource_aws_security_group_test.go
+++ b/builtin/providers/aws/resource_aws_security_group_test.go
@@ -276,6 +276,34 @@ func testAccCheckAWSSecurityGroupAttributes(group *ec2.SecurityGroupInfo) resour
}
}
+func TestAccAWSSecurityGroup_tags(t *testing.T) {
+ var group ec2.SecurityGroupInfo
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ Providers: testAccProviders,
+ CheckDestroy: testAccCheckAWSSecurityGroupDestroy,
+ Steps: []resource.TestStep{
+ resource.TestStep{
+ Config: testAccAWSSecurityGroupConfigTags,
+ Check: resource.ComposeTestCheckFunc(
+ testAccCheckAWSSecurityGroupExists("aws_security_group.foo", &group),
+ testAccCheckTags(&group.Tags, "foo", "bar"),
+ ),
+ },
+
+ resource.TestStep{
+ Config: testAccAWSSecurityGroupConfigTagsUpdate,
+ Check: resource.ComposeTestCheckFunc(
+ testAccCheckAWSSecurityGroupExists("aws_security_group.foo", &group),
+ testAccCheckTags(&group.Tags, "foo", ""),
+ testAccCheckTags(&group.Tags, "bar", "baz"),
+ ),
+ },
+ },
+ })
+}
+
func testAccCheckAWSSecurityGroupAttributesChanged(group *ec2.SecurityGroupInfo) resource.TestCheckFunc {
return func(s *terraform.State) error {
p := []ec2.IPPerm{
@@ -432,3 +460,19 @@ resource "aws_security_group" "web" {
}
}
`
+
+const testAccAWSSecurityGroupConfigTags = `
+resource "aws_security_group" "foo" {
+ tags {
+ foo = "bar"
+ }
+}
+`
+
+const testAccAWSSecurityGroupConfigTagsUpdate = `
+resource "aws_security_group" "foo" {
+ tags {
+ bar = "baz"
+ }
+}
+`
diff --git a/builtin/providers/aws/resource_aws_subnet.go b/builtin/providers/aws/resource_aws_subnet.go
index 35b6fba3c..249d8bf2c 100644
--- a/builtin/providers/aws/resource_aws_subnet.go
+++ b/builtin/providers/aws/resource_aws_subnet.go
@@ -88,18 +88,26 @@ func resource_aws_subnet_destroy(
ec2conn := p.ec2conn
log.Printf("[INFO] Deleting Subnet: %s", s.ID)
- f := func() error {
+ return resource.Retry(5*time.Minute, func() error {
_, err := ec2conn.DeleteSubnet(s.ID)
- return err
- }
- if err := resource.Retry(10*time.Second, f); err != nil {
- ec2err, ok := err.(*ec2.Error)
- if ok && ec2err.Code == "InvalidSubnetID.NotFound" {
- return nil
+ if err != nil {
+ ec2err, ok := err.(*ec2.Error)
+ if !ok {
+ return err
+ }
+
+ switch ec2err.Code {
+ case "InvalidSubnetID.NotFound":
+ return nil
+ case "DependencyViolation":
+ return err // retry
+ default:
+ return resource.RetryError{err}
+ }
}
return fmt.Errorf("Error deleting subnet: %s", err)
- }
+ })
// Wait for the Subnet to actually delete
log.Printf("[DEBUG] Waiting for subnet (%s) to delete", s.ID)
diff --git a/builtin/providers/aws/structure.go b/builtin/providers/aws/structure.go
index c349991cb..3af176c2a 100644
--- a/builtin/providers/aws/structure.go
+++ b/builtin/providers/aws/structure.go
@@ -3,6 +3,7 @@ package aws
import (
"strings"
+ "github.com/hashicorp/terraform/helper/schema"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/goamz/elb"
)
@@ -48,7 +49,7 @@ func expandIPPerms(id string, configured []interface{}) []ec2.IPPerm {
var groups []string
if raw, ok := m["security_groups"]; ok {
- list := raw.([]interface{})
+ list := raw.(*schema.Set).List()
for _, v := range list {
groups = append(groups, v.(string))
}
diff --git a/builtin/providers/aws/structure_test.go b/builtin/providers/aws/structure_test.go
index 7621535fb..e230cdf91 100644
--- a/builtin/providers/aws/structure_test.go
+++ b/builtin/providers/aws/structure_test.go
@@ -5,6 +5,8 @@ import (
"testing"
"github.com/hashicorp/terraform/flatmap"
+ "github.com/hashicorp/terraform/helper/hashcode"
+ "github.com/hashicorp/terraform/helper/schema"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/goamz/elb"
)
@@ -33,16 +35,20 @@ func testConf() map[string]string {
}
func Test_expandIPPerms(t *testing.T) {
+ hash := func(v interface{}) int {
+ return hashcode.String(v.(string))
+ }
+
expanded := []interface{}{
map[string]interface{}{
"protocol": "icmp",
"from_port": 1,
"to_port": -1,
"cidr_blocks": []interface{}{"0.0.0.0/0"},
- "security_groups": []interface{}{
+ "security_groups": schema.NewSet(hash, []interface{}{
"sg-11111",
"foo/sg-22222",
- },
+ }),
},
map[string]interface{}{
"protocol": "icmp",
@@ -60,13 +66,13 @@ func Test_expandIPPerms(t *testing.T) {
ToPort: -1,
SourceIPs: []string{"0.0.0.0/0"},
SourceGroups: []ec2.UserSecurityGroup{
- ec2.UserSecurityGroup{
- Id: "sg-11111",
- },
ec2.UserSecurityGroup{
OwnerId: "foo",
Id: "sg-22222",
},
+ ec2.UserSecurityGroup{
+ Id: "sg-11111",
+ },
},
},
ec2.IPPerm{
diff --git a/builtin/providers/consul/resource_consul_keys.go b/builtin/providers/consul/resource_consul_keys.go
index 4aef3df5e..45647b8ff 100644
--- a/builtin/providers/consul/resource_consul_keys.go
+++ b/builtin/providers/consul/resource_consul_keys.go
@@ -31,7 +31,7 @@ func resourceConsulKeys() *schema.Resource {
Optional: true,
},
- "keys": &schema.Schema{
+ "key": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
@@ -60,6 +60,7 @@ func resourceConsulKeys() *schema.Resource {
"delete": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
+ Default: false,
},
},
},
@@ -113,19 +114,15 @@ func resourceConsulKeysCreate(d *schema.ResourceData, meta interface{}) error {
vars := make(map[string]string)
// Extract the keys
- keys := d.Get("keys").(*schema.Set).List()
+ keys := d.Get("key").(*schema.Set).List()
for _, raw := range keys {
key, path, sub, err := parse_key(raw)
if err != nil {
return err
}
- if valueRaw, ok := sub["value"]; ok {
- value, ok := valueRaw.(string)
- if !ok {
- return fmt.Errorf("Failed to get value for key '%s'", key)
- }
-
+ value := sub["value"].(string)
+ if value != "" {
log.Printf("[DEBUG] Setting key '%s' to '%v' in %s", path, value, dc)
pair := consulapi.KVPair{Key: path, Value: []byte(value)}
if _, err := kv.Put(&pair, &wOpts); err != nil {
@@ -142,14 +139,13 @@ func resourceConsulKeysCreate(d *schema.ResourceData, meta interface{}) error {
}
value := attribute_value(sub, key, pair)
vars[key] = value
- sub["value"] = value
}
}
// Update the resource
d.SetId("consul")
d.Set("datacenter", dc)
- d.Set("keys", keys)
+ d.Set("key", keys)
d.Set("var", vars)
return nil
}
@@ -178,7 +174,7 @@ func resourceConsulKeysRead(d *schema.ResourceData, meta interface{}) error {
vars := make(map[string]string)
// Extract the keys
- keys := d.Get("keys").(*schema.Set).List()
+ keys := d.Get("key").(*schema.Set).List()
for _, raw := range keys {
key, path, sub, err := parse_key(raw)
if err != nil {
@@ -197,7 +193,7 @@ func resourceConsulKeysRead(d *schema.ResourceData, meta interface{}) error {
}
// Update the resource
- d.Set("keys", keys)
+ d.Set("key", keys)
d.Set("var", vars)
return nil
}
@@ -223,7 +219,7 @@ func resourceConsulKeysDelete(d *schema.ResourceData, meta interface{}) error {
wOpts := consulapi.WriteOptions{Datacenter: dc, Token: token}
// Extract the keys
- keys := d.Get("keys").(*schema.Set).List()
+ keys := d.Get("key").(*schema.Set).List()
for _, raw := range keys {
_, path, sub, err := parse_key(raw)
if err != nil {
diff --git a/builtin/providers/consul/resource_consul_keys_test.go b/builtin/providers/consul/resource_consul_keys_test.go
index 2f52be83b..c0f132486 100644
--- a/builtin/providers/consul/resource_consul_keys_test.go
+++ b/builtin/providers/consul/resource_consul_keys_test.go
@@ -30,7 +30,7 @@ func TestAccConsulKeys(t *testing.T) {
func testAccCheckConsulKeysDestroy(s *terraform.State) error {
kv := testAccProvider.Meta().(*consulapi.Client).KV()
- opts := &consulapi.QueryOptions{Datacenter: "nyc1"}
+ opts := &consulapi.QueryOptions{Datacenter: "nyc3"}
pair, _, err := kv.Get("test/set", opts)
if err != nil {
return err
@@ -44,7 +44,7 @@ func testAccCheckConsulKeysDestroy(s *terraform.State) error {
func testAccCheckConsulKeysExists() resource.TestCheckFunc {
return func(s *terraform.State) error {
kv := testAccProvider.Meta().(*consulapi.Client).KV()
- opts := &consulapi.QueryOptions{Datacenter: "nyc1"}
+ opts := &consulapi.QueryOptions{Datacenter: "nyc3"}
pair, _, err := kv.Get("test/set", opts)
if err != nil {
return err
@@ -78,7 +78,7 @@ func testAccCheckConsulKeysValue(n, attr, val string) resource.TestCheckFunc {
const testAccConsulKeysConfig = `
resource "consul_keys" "app" {
- datacenter = "nyc1"
+ datacenter = "nyc3"
key {
name = "time"
path = "global/time"
diff --git a/builtin/providers/consul/resource_provider_test.go b/builtin/providers/consul/resource_provider_test.go
index 5e2746f98..ad2b29898 100644
--- a/builtin/providers/consul/resource_provider_test.go
+++ b/builtin/providers/consul/resource_provider_test.go
@@ -3,6 +3,7 @@ package consul
import (
"testing"
+ "github.com/armon/consul-api"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
@@ -16,6 +17,13 @@ func init() {
testAccProviders = map[string]terraform.ResourceProvider{
"consul": testAccProvider,
}
+
+ // Use the demo address for the acceptance tests
+ testAccProvider.ConfigureFunc = func(d *schema.ResourceData) (interface{}, error) {
+ conf := consulapi.DefaultConfig()
+ conf.Address = "demo.consul.io:80"
+ return consulapi.NewClient(conf)
+ }
}
func TestResourceProvider(t *testing.T) {
@@ -33,7 +41,7 @@ func TestResourceProvider_Configure(t *testing.T) {
raw := map[string]interface{}{
"address": "demo.consul.io:80",
- "datacenter": "nyc1",
+ "datacenter": "nyc3",
}
rawConfig, err := config.NewRawConfig(raw)
diff --git a/builtin/providers/google/resource_compute_instance.go b/builtin/providers/google/resource_compute_instance.go
index 696d8ca91..f6b0fde7a 100644
--- a/builtin/providers/google/resource_compute_instance.go
+++ b/builtin/providers/google/resource_compute_instance.go
@@ -100,6 +100,11 @@ func resourceComputeInstance() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
+
+ "external_address": &schema.Schema{
+ Type: schema.TypeString,
+ Computed: true,
+ },
},
},
},
@@ -335,12 +340,27 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error
d.Set("can_ip_forward", instance.CanIpForward)
// Set the networks
+ externalIP := ""
for i, iface := range instance.NetworkInterfaces {
prefix := fmt.Sprintf("network.%d", i)
d.Set(prefix+".name", iface.Name)
+
+ // Use the first external IP found for the default connection info.
+ natIP := resourceInstanceNatIP(iface)
+ if externalIP == "" && natIP != "" {
+ externalIP = natIP
+ }
+ d.Set(prefix+".external_address", natIP)
+
d.Set(prefix+".internal_address", iface.NetworkIP)
}
+ // Initialize the connection info
+ d.SetConnInfo(map[string]string{
+ "type": "ssh",
+ "host": externalIP,
+ })
+
// Set the metadata fingerprint if there is one.
if instance.Metadata != nil {
d.Set("metadata_fingerprint", instance.Metadata.Fingerprint)
@@ -506,3 +526,16 @@ func resourceInstanceTags(d *schema.ResourceData) *compute.Tags {
return tags
}
+
+// resourceInstanceNatIP acquires the first NatIP with a "ONE_TO_ONE_NAT" type
+// in the compute.NetworkInterface's AccessConfigs.
+func resourceInstanceNatIP(iface *compute.NetworkInterface) (natIP string) {
+ for _, config := range iface.AccessConfigs {
+ if config.Type == "ONE_TO_ONE_NAT" {
+ natIP = config.NatIP
+ break
+ }
+ }
+
+ return natIP
+}
diff --git a/builtin/providers/heroku/resource_heroku_app.go b/builtin/providers/heroku/resource_heroku_app.go
index 41a42d494..aaa31436c 100644
--- a/builtin/providers/heroku/resource_heroku_app.go
+++ b/builtin/providers/heroku/resource_heroku_app.go
@@ -95,17 +95,40 @@ func resourceHerokuApp() *schema.Resource {
},
"organization": &schema.Schema{
- Type: schema.TypeString,
Description: "Name of Organization to create application in. Leave blank for personal apps.",
+ Type: schema.TypeList,
Optional: true,
ForceNew: true,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "name": &schema.Schema{
+ Type: schema.TypeString,
+ Required: true,
+ },
+
+ "locked": &schema.Schema{
+ Type: schema.TypeBool,
+ Optional: true,
+ },
+
+ "personal": &schema.Schema{
+ Type: schema.TypeBool,
+ Optional: true,
+ },
+ },
+ },
},
},
}
}
func switchHerokuAppCreate(d *schema.ResourceData, meta interface{}) error {
- if _, ok := d.GetOk("organization"); ok {
+ orgCount := d.Get("organization.#").(int)
+ if orgCount > 1 {
+ return fmt.Errorf("Error Creating Heroku App: Only 1 Heroku Organization is permitted")
+ }
+
+ if _, ok := d.GetOk("organization.0.name"); ok {
return resourceHerokuOrgAppCreate(d, meta)
} else {
return resourceHerokuAppCreate(d, meta)
@@ -157,12 +180,26 @@ func resourceHerokuOrgAppCreate(d *schema.ResourceData, meta interface{}) error
client := meta.(*heroku.Service)
// Build up our creation options
opts := heroku.OrganizationAppCreateOpts{}
- if v, ok := d.GetOk("organization"); ok {
+
+ if v := d.Get("organization.0.name"); v != nil {
vs := v.(string)
- log.Printf("[DEBUG] App name: %s", vs)
+ log.Printf("[DEBUG] Organization name: %s", vs)
opts.Organization = &vs
}
- if v, ok := d.GetOk("name"); ok {
+
+ if v := d.Get("organization.0.personal"); v != nil {
+ vs := v.(bool)
+ log.Printf("[DEBUG] Organization Personal: %s", vs)
+ opts.Personal = &vs
+ }
+
+ if v := d.Get("organization.0.locked"); v != nil {
+ vs := v.(bool)
+ log.Printf("[DEBUG] Organization locked: %s", vs)
+ opts.Locked = &vs
+ }
+
+ if v := d.Get("name"); v != nil {
vs := v.(string)
log.Printf("[DEBUG] App name: %s", vs)
opts.Name = &vs
diff --git a/helper/resource/wait.go b/helper/resource/wait.go
index 5fcd59c26..326555053 100644
--- a/helper/resource/wait.go
+++ b/helper/resource/wait.go
@@ -18,14 +18,29 @@ func Retry(timeout time.Duration, f RetryFunc) error {
MinTimeout: 500 * time.Millisecond,
Refresh: func() (interface{}, string, error) {
err = f()
- if err != nil {
- return 42, "error", nil
+ if err == nil {
+ return 42, "success", nil
}
- return 42, "success", nil
+ if rerr, ok := err.(RetryError); ok {
+ err = rerr.Err
+ return nil, "quit", err
+ }
+
+ return 42, "error", nil
},
}
c.WaitForState()
return err
}
+
+// RetryError, if returned, will quit the retry immediately with the
+// Err.
+type RetryError struct {
+ Err error
+}
+
+func (e RetryError) Error() string {
+ return e.Err.Error()
+}
diff --git a/helper/resource/wait_test.go b/helper/resource/wait_test.go
index 34ca6ce18..f79d27656 100644
--- a/helper/resource/wait_test.go
+++ b/helper/resource/wait_test.go
@@ -37,3 +37,26 @@ func TestRetry_timeout(t *testing.T) {
t.Fatal("should error")
}
}
+
+func TestRetry_error(t *testing.T) {
+ t.Parallel()
+
+ expected := fmt.Errorf("nope")
+ f := func() error {
+ return RetryError{expected}
+ }
+
+ errCh := make(chan error)
+ go func() {
+ errCh <- Retry(1*time.Second, f)
+ }()
+
+ select {
+ case err := <-errCh:
+ if err != expected {
+ t.Fatalf("bad: %#v", err)
+ }
+ case <-time.After(5 * time.Second):
+ t.Fatal("timeout")
+ }
+}
diff --git a/helper/schema/resource.go b/helper/schema/resource.go
index 16c49f230..7f5736bc6 100644
--- a/helper/schema/resource.go
+++ b/helper/schema/resource.go
@@ -91,6 +91,12 @@ func (r *Resource) Apply(
if !d.RequiresNew() {
return nil, nil
}
+
+ // Reset the data to be stateless since we just destroyed
+ data, err = schemaMap(r.Schema).Data(nil, d)
+ if err != nil {
+ return nil, err
+ }
}
err = nil
diff --git a/helper/schema/resource_data.go b/helper/schema/resource_data.go
index deb331a05..23be6f290 100644
--- a/helper/schema/resource_data.go
+++ b/helper/schema/resource_data.go
@@ -43,10 +43,11 @@ type getSource byte
const (
getSourceState getSource = 1 << iota
getSourceConfig
- getSourceDiff
getSourceSet
- getSourceExact
- getSourceMax = getSourceSet
+ getSourceExact // Only get from the _exact_ level
+ getSourceDiff // Apply the diff on top our level
+ getSourceLevelMask getSource = getSourceState | getSourceConfig | getSourceSet
+ getSourceMax getSource = getSourceSet
)
// getResult is the internal structure that is generated when a Get
@@ -82,7 +83,7 @@ func (d *ResourceData) Get(key string) interface{} {
// set and the new value is. This is common, for example, for boolean
// fields which have a zero value of false.
func (d *ResourceData) GetChange(key string) (interface{}, interface{}) {
- o, n := d.getChange(key, getSourceConfig, getSourceDiff)
+ o, n := d.getChange(key, getSourceConfig, getSourceConfig|getSourceDiff)
return o.Value, n.Value
}
@@ -93,7 +94,7 @@ func (d *ResourceData) GetChange(key string) (interface{}, interface{}) {
// The first result will not necessarilly be nil if the value doesn't exist.
// The second result should be checked to determine this information.
func (d *ResourceData) GetOk(key string) (interface{}, bool) {
- r := d.getRaw(key, getSourceSet)
+ r := d.getRaw(key, getSourceSet|getSourceDiff)
return r.Value, r.Exists
}
@@ -289,12 +290,21 @@ func (d *ResourceData) getSet(
// entire set must come from set, diff, state, etc. So we go backwards
// and once we get a result, we take it. Or, we never get a result.
var raw getResult
- for listSource := source; listSource > 0; listSource >>= 1 {
- if source&getSourceExact != 0 && listSource != source {
+ sourceLevel := source & getSourceLevelMask
+ sourceFlags := source & ^getSourceLevelMask
+ for listSource := sourceLevel; listSource > 0; listSource >>= 1 {
+ // If we're already asking for an exact source and it doesn't
+ // match, then leave since the original source was the match.
+ if sourceFlags&getSourceExact != 0 && listSource != sourceLevel {
break
}
- raw = d.getList(k, nil, schema, listSource|getSourceExact)
+ // The source we get from is the level we're on, plus the flags
+ // we had, plus the exact flag.
+ getSource := listSource
+ getSource |= sourceFlags
+ getSource |= getSourceExact
+ raw = d.getList(k, nil, schema, getSource)
if raw.Exists {
break
}
@@ -384,11 +394,13 @@ func (d *ResourceData) getMap(
resultSet := false
prefix := k + "."
- exact := source&getSourceExact != 0
- source &^= getSourceExact
+ flags := source & ^getSourceLevelMask
+ level := source & getSourceLevelMask
+ exact := flags&getSourceExact != 0
+ diff := flags&getSourceDiff != 0
- if !exact || source == getSourceState {
- if d.state != nil && source >= getSourceState {
+ if !exact || level == getSourceState {
+ if d.state != nil && level >= getSourceState {
for k, _ := range d.state.Attributes {
if !strings.HasPrefix(k, prefix) {
continue
@@ -401,7 +413,7 @@ func (d *ResourceData) getMap(
}
}
- if d.config != nil && source == getSourceConfig {
+ if d.config != nil && level == getSourceConfig {
// For config, we always set the result to exactly what was requested
if mraw, ok := d.config.Get(k); ok {
result = make(map[string]interface{})
@@ -433,42 +445,47 @@ func (d *ResourceData) getMap(
}
}
- if !exact || source == getSourceDiff {
- if d.diff != nil && source >= getSourceDiff {
- for k, v := range d.diff.Attributes {
- if !strings.HasPrefix(k, prefix) {
- continue
- }
- resultSet = true
+ if d.diff != nil && diff {
+ for k, v := range d.diff.Attributes {
+ if !strings.HasPrefix(k, prefix) {
+ continue
+ }
+ resultSet = true
- single := k[len(prefix):]
+ single := k[len(prefix):]
- if v.NewRemoved {
- delete(result, single)
- } else {
- result[single] = d.getPrimitive(k, nil, elemSchema, source).Value
- }
+ if v.NewRemoved {
+ delete(result, single)
+ } else {
+ result[single] = d.getPrimitive(k, nil, elemSchema, source).Value
}
}
}
- if !exact || source == getSourceSet {
- if d.setMap != nil && source >= getSourceSet {
+ if !exact || level == getSourceSet {
+ if d.setMap != nil && level >= getSourceSet {
cleared := false
- for k, _ := range d.setMap {
- if !strings.HasPrefix(k, prefix) {
- continue
- }
+ if v, ok := d.setMap[k]; ok && v == "" {
+ // We've cleared the map
+ result = make(map[string]interface{})
resultSet = true
+ } else {
+ for k, _ := range d.setMap {
+ if !strings.HasPrefix(k, prefix) {
+ continue
+ }
+ resultSet = true
- if !cleared {
- // We clear the results if they are in the set map
- result = make(map[string]interface{})
- cleared = true
+ if !cleared {
+ // We clear the results if they are in the set map
+ result = make(map[string]interface{})
+ cleared = true
+ }
+
+ single := k[len(prefix):]
+ result[single] = d.getPrimitive(
+ k, nil, elemSchema, source).Value
}
-
- single := k[len(prefix):]
- result[single] = d.getPrimitive(k, nil, elemSchema, source).Value
}
}
}
@@ -577,8 +594,10 @@ func (d *ResourceData) getPrimitive(
var result string
var resultProcessed interface{}
var resultComputed, resultSet bool
- exact := source&getSourceExact != 0
- source &^= getSourceExact
+ flags := source & ^getSourceLevelMask
+ source = source & getSourceLevelMask
+ exact := flags&getSourceExact != 0
+ diff := flags&getSourceDiff != 0
if !exact || source == getSourceState {
if d.state != nil && source >= getSourceState {
@@ -604,28 +623,26 @@ func (d *ResourceData) getPrimitive(
resultComputed = d.config.IsComputed(k)
}
- if !exact || source == getSourceDiff {
- if d.diff != nil && source >= getSourceDiff {
- attrD, ok := d.diff.Attributes[k]
- if ok {
- if !attrD.NewComputed {
- result = attrD.New
- if attrD.NewExtra != nil {
- // If NewExtra != nil, then we have processed data as the New,
- // so we store that but decode the unprocessed data into result
- resultProcessed = result
+ if d.diff != nil && diff {
+ attrD, ok := d.diff.Attributes[k]
+ if ok {
+ if !attrD.NewComputed {
+ result = attrD.New
+ if attrD.NewExtra != nil {
+ // If NewExtra != nil, then we have processed data as the New,
+ // so we store that but decode the unprocessed data into result
+ resultProcessed = result
- err := mapstructure.WeakDecode(attrD.NewExtra, &result)
- if err != nil {
- panic(err)
- }
+ err := mapstructure.WeakDecode(attrD.NewExtra, &result)
+ if err != nil {
+ panic(err)
}
-
- resultSet = true
- } else {
- result = ""
- resultSet = false
}
+
+ resultSet = true
+ } else {
+ result = ""
+ resultSet = false
}
}
}
@@ -773,14 +790,6 @@ func (d *ResourceData) setMapValue(
return fmt.Errorf("%s: full map must be set, no a single element", k)
}
- // Delete any prior map set
- /*
- v := d.getMap(k, nil, schema, getSourceSet)
- for subKey, _ := range v.(map[string]interface{}) {
- delete(d.setMap, fmt.Sprintf("%s.%s", k, subKey))
- }
- */
-
v := reflect.ValueOf(value)
if v.Kind() != reflect.Map {
return fmt.Errorf("%s: must be a map", k)
@@ -794,6 +803,13 @@ func (d *ResourceData) setMapValue(
vs[mk.String()] = mv.Interface()
}
+ if len(vs) == 0 {
+ // The empty string here means the map is removed.
+ d.setMap[k] = ""
+ return nil
+ }
+
+ delete(d.setMap, k)
for subKey, v := range vs {
err := d.set(fmt.Sprintf("%s.%s", k, subKey), nil, elemSchema, v)
if err != nil {
@@ -1101,14 +1117,14 @@ func (d *ResourceData) stateSingle(
func (d *ResourceData) stateSource(prefix string) getSource {
// If we're not doing a partial apply, then get the set level
if !d.partial {
- return getSourceSet
+ return getSourceSet | getSourceDiff
}
// Otherwise, only return getSourceSet if its in the partial map.
// Otherwise we use state level only.
for k, _ := range d.partialMap {
if strings.HasPrefix(prefix, k) {
- return getSourceSet
+ return getSourceSet | getSourceDiff
}
}
diff --git a/helper/schema/resource_data_test.go b/helper/schema/resource_data_test.go
index 01f3616b3..751555964 100644
--- a/helper/schema/resource_data_test.go
+++ b/helper/schema/resource_data_test.go
@@ -527,6 +527,58 @@ func TestResourceDataGet(t *testing.T) {
Value: []interface{}{80},
},
+
+ {
+ Schema: map[string]*Schema{
+ "data": &Schema{
+ Type: TypeSet,
+ Optional: true,
+ Elem: &Resource{
+ Schema: map[string]*Schema{
+ "index": &Schema{
+ Type: TypeInt,
+ Required: true,
+ },
+
+ "value": &Schema{
+ Type: TypeString,
+ Required: true,
+ },
+ },
+ },
+ Set: func(a interface{}) int {
+ m := a.(map[string]interface{})
+ return m["index"].(int)
+ },
+ },
+ },
+
+ State: &terraform.InstanceState{
+ Attributes: map[string]string{
+ "data.#": "1",
+ "data.0.index": "10",
+ "data.0.value": "50",
+ },
+ },
+
+ Diff: &terraform.InstanceDiff{
+ Attributes: map[string]*terraform.ResourceAttrDiff{
+ "data.0.value": &terraform.ResourceAttrDiff{
+ Old: "50",
+ New: "80",
+ },
+ },
+ },
+
+ Key: "data",
+
+ Value: []interface{}{
+ map[string]interface{}{
+ "index": 10,
+ "value": "80",
+ },
+ },
+ },
}
for i, tc := range cases {
@@ -860,6 +912,31 @@ func TestResourceDataHasChange(t *testing.T) {
Change: false,
},
+
+ {
+ Schema: map[string]*Schema{
+ "tags": &Schema{
+ Type: TypeMap,
+ Optional: true,
+ Computed: true,
+ },
+ },
+
+ State: nil,
+
+ Diff: &terraform.InstanceDiff{
+ Attributes: map[string]*terraform.ResourceAttrDiff{
+ "tags.Name": &terraform.ResourceAttrDiff{
+ Old: "foo",
+ New: "foo",
+ },
+ },
+ },
+
+ Key: "tags",
+
+ Change: true,
+ },
}
for i, tc := range cases {
@@ -2141,6 +2218,63 @@ func TestResourceDataState(t *testing.T) {
},
},
},
+
+ // Maps
+ {
+ Schema: map[string]*Schema{
+ "tags": &Schema{
+ Type: TypeMap,
+ Optional: true,
+ Computed: true,
+ },
+ },
+
+ State: nil,
+
+ Diff: &terraform.InstanceDiff{
+ Attributes: map[string]*terraform.ResourceAttrDiff{
+ "tags.Name": &terraform.ResourceAttrDiff{
+ Old: "",
+ New: "foo",
+ },
+ },
+ },
+
+ Result: &terraform.InstanceState{
+ Attributes: map[string]string{
+ "tags.Name": "foo",
+ },
+ },
+ },
+
+ {
+ Schema: map[string]*Schema{
+ "tags": &Schema{
+ Type: TypeMap,
+ Optional: true,
+ Computed: true,
+ },
+ },
+
+ State: nil,
+
+ Diff: &terraform.InstanceDiff{
+ Attributes: map[string]*terraform.ResourceAttrDiff{
+ "tags.Name": &terraform.ResourceAttrDiff{
+ Old: "",
+ New: "foo",
+ },
+ },
+ },
+
+ Set: map[string]interface{}{
+ "tags": map[string]string{},
+ },
+
+ Result: &terraform.InstanceState{
+ Attributes: map[string]string{},
+ },
+ },
}
for i, tc := range cases {
diff --git a/helper/schema/resource_test.go b/helper/schema/resource_test.go
index 32ad34454..845bd264d 100644
--- a/helper/schema/resource_test.go
+++ b/helper/schema/resource_test.go
@@ -95,6 +95,77 @@ func TestResourceApply_destroy(t *testing.T) {
}
}
+func TestResourceApply_destroyCreate(t *testing.T) {
+ r := &Resource{
+ Schema: map[string]*Schema{
+ "foo": &Schema{
+ Type: TypeInt,
+ Optional: true,
+ },
+
+ "tags": &Schema{
+ Type: TypeMap,
+ Optional: true,
+ Computed: true,
+ },
+ },
+ }
+
+ change := false
+ r.Create = func(d *ResourceData, m interface{}) error {
+ change = d.HasChange("tags")
+ d.SetId("foo")
+ return nil
+ }
+ r.Delete = func(d *ResourceData, m interface{}) error {
+ return nil
+ }
+
+ var s *terraform.InstanceState = &terraform.InstanceState{
+ ID: "bar",
+ Attributes: map[string]string{
+ "foo": "bar",
+ "tags.Name": "foo",
+ },
+ }
+
+ d := &terraform.InstanceDiff{
+ Attributes: map[string]*terraform.ResourceAttrDiff{
+ "foo": &terraform.ResourceAttrDiff{
+ New: "42",
+ RequiresNew: true,
+ },
+ "tags.Name": &terraform.ResourceAttrDiff{
+ Old: "foo",
+ New: "foo",
+ RequiresNew: true,
+ },
+ },
+ }
+
+ actual, err := r.Apply(s, d, nil)
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+
+ if !change {
+ t.Fatal("should have change")
+ }
+
+ expected := &terraform.InstanceState{
+ ID: "foo",
+ Attributes: map[string]string{
+ "id": "foo",
+ "foo": "42",
+ "tags.Name": "foo",
+ },
+ }
+
+ if !reflect.DeepEqual(actual, expected) {
+ t.Fatalf("bad: %#v", actual)
+ }
+}
+
func TestResourceApply_destroyPartial(t *testing.T) {
r := &Resource{
Schema: map[string]*Schema{
diff --git a/helper/schema/schema.go b/helper/schema/schema.go
index d074e9512..7cd0fe711 100644
--- a/helper/schema/schema.go
+++ b/helper/schema/schema.go
@@ -205,7 +205,7 @@ func (m schemaMap) Diff(
}
for k, schema := range m {
- err := m.diff(k, schema, result, d)
+ err := m.diff(k, schema, result, d, false)
if err != nil {
return nil, err
}
@@ -225,7 +225,7 @@ func (m schemaMap) Diff(
// Perform the diff again
for k, schema := range m {
- err := m.diff(k, schema, result2, d)
+ err := m.diff(k, schema, result2, d, false)
if err != nil {
return nil, err
}
@@ -346,7 +346,7 @@ func (m schemaMap) Input(
"%s: %s", k, err)
}
- c.Raw[k] = value
+ c.Config[k] = value
}
return c, nil
@@ -427,7 +427,8 @@ func (m schemaMap) diff(
k string,
schema *Schema,
diff *terraform.InstanceDiff,
- d *ResourceData) error {
+ d *ResourceData,
+ all bool) error {
var err error
switch schema.Type {
case TypeBool:
@@ -435,13 +436,13 @@ func (m schemaMap) diff(
case TypeInt:
fallthrough
case TypeString:
- err = m.diffString(k, schema, diff, d)
+ err = m.diffString(k, schema, diff, d, all)
case TypeList:
- err = m.diffList(k, schema, diff, d)
+ err = m.diffList(k, schema, diff, d, all)
case TypeMap:
- err = m.diffMap(k, schema, diff, d)
+ err = m.diffMap(k, schema, diff, d, all)
case TypeSet:
- err = m.diffSet(k, schema, diff, d)
+ err = m.diffSet(k, schema, diff, d, all)
default:
err = fmt.Errorf("%s: unknown type %#v", k, schema.Type)
}
@@ -453,7 +454,8 @@ func (m schemaMap) diffList(
k string,
schema *Schema,
diff *terraform.InstanceDiff,
- d *ResourceData) error {
+ d *ResourceData,
+ all bool) error {
o, n, _, computedList := d.diffChange(k)
nSet := n != nil
@@ -481,7 +483,7 @@ func (m schemaMap) diffList(
// If the new value was set, and the two are equal, then we're done.
// We have to do this check here because sets might be NOT
// reflect.DeepEqual so we need to wait until we get the []interface{}
- if nSet && reflect.DeepEqual(os, vs) {
+ if !all && nSet && reflect.DeepEqual(os, vs) {
return nil
}
@@ -502,7 +504,7 @@ func (m schemaMap) diffList(
// If the counts are not the same, then record that diff
changed := oldLen != newLen
computed := oldLen == 0 && newLen == 0 && schema.Computed
- if changed || computed {
+ if changed || computed || all {
countSchema := &Schema{
Type: TypeInt,
Computed: schema.Computed,
@@ -539,7 +541,7 @@ func (m schemaMap) diffList(
// just diff each.
for i := 0; i < maxLen; i++ {
subK := fmt.Sprintf("%s.%d", k, i)
- err := m.diff(subK, &t2, diff, d)
+ err := m.diff(subK, &t2, diff, d, all)
if err != nil {
return err
}
@@ -549,7 +551,7 @@ func (m schemaMap) diffList(
for i := 0; i < maxLen; i++ {
for k2, schema := range t.Schema {
subK := fmt.Sprintf("%s.%d.%s", k, i, k2)
- err := m.diff(subK, schema, diff, d)
+ err := m.diff(subK, schema, diff, d, all)
if err != nil {
return err
}
@@ -566,8 +568,8 @@ func (m schemaMap) diffMap(
k string,
schema *Schema,
diff *terraform.InstanceDiff,
- d *ResourceData) error {
- //elemSchema := &Schema{Type: TypeString}
+ d *ResourceData,
+ all bool) error {
prefix := k + "."
// First get all the values from the state
@@ -580,12 +582,17 @@ func (m schemaMap) diffMap(
return fmt.Errorf("%s: %s", k, err)
}
+ // If the new map is nil and we're computed, then ignore it.
+ if n == nil && schema.Computed {
+ return nil
+ }
+
// Now we compare, preferring values from the config map
for k, v := range configMap {
old := stateMap[k]
delete(stateMap, k)
- if old == v {
+ if old == v && !all {
continue
}
@@ -608,15 +615,35 @@ func (m schemaMap) diffSet(
k string,
schema *Schema,
diff *terraform.InstanceDiff,
- d *ResourceData) error {
- return m.diffList(k, schema, diff, d)
+ d *ResourceData,
+ all bool) error {
+ if !all {
+ // This is a bit strange, but we expect the entire set to be in the diff,
+ // so we first diff the set normally but with a new diff. Then, if
+ // there IS any change, we just set the change to the entire list.
+ tempD := new(terraform.InstanceDiff)
+ tempD.Attributes = make(map[string]*terraform.ResourceAttrDiff)
+ if err := m.diffList(k, schema, tempD, d, false); err != nil {
+ return err
+ }
+
+ // If we had no changes, then we're done
+ if tempD.Empty() {
+ return nil
+ }
+ }
+
+ // We have changes, so re-run the diff, but set a flag to force
+ // getting all diffs, even if there is no change.
+ return m.diffList(k, schema, diff, d, true)
}
func (m schemaMap) diffString(
k string,
schema *Schema,
diff *terraform.InstanceDiff,
- d *ResourceData) error {
+ d *ResourceData,
+ all bool) error {
var originalN interface{}
var os, ns string
o, n, _, _ := d.diffChange(k)
@@ -641,7 +668,7 @@ func (m schemaMap) diffString(
return fmt.Errorf("%s: %s", k, err)
}
- if os == ns {
+ if os == ns && !all {
// They're the same value. If there old value is not blank or we
// have an ID, then return right away since we're already setup.
if os != "" || d.Id() != "" {
@@ -767,6 +794,44 @@ func (m schemaMap) validateList(
return ws, es
}
+func (m schemaMap) validateMap(
+ k string,
+ raw interface{},
+ schema *Schema,
+ c *terraform.ResourceConfig) ([]string, []error) {
+ // We use reflection to verify the slice because you can't
+ // case to []interface{} unless the slice is exactly that type.
+ rawV := reflect.ValueOf(raw)
+ switch rawV.Kind() {
+ case reflect.Map:
+ case reflect.Slice:
+ default:
+ return nil, []error{fmt.Errorf(
+ "%s: should be a map", k)}
+ }
+
+ // If it is not a slice, it is valid
+ if rawV.Kind() != reflect.Slice {
+ return nil, nil
+ }
+
+ // It is a slice, verify that all the elements are maps
+ raws := make([]interface{}, rawV.Len())
+ for i, _ := range raws {
+ raws[i] = rawV.Index(i).Interface()
+ }
+
+ for _, raw := range raws {
+ v := reflect.ValueOf(raw)
+ if v.Kind() != reflect.Map {
+ return nil, []error{fmt.Errorf(
+ "%s: should be a map", k)}
+ }
+ }
+
+ return nil, nil
+}
+
func (m schemaMap) validateObject(
k string,
schema map[string]*Schema,
@@ -819,14 +884,32 @@ func (m schemaMap) validatePrimitive(
}
switch schema.Type {
+ case TypeSet:
+ fallthrough
case TypeList:
return m.validateList(k, raw, schema, c)
+ case TypeMap:
+ return m.validateMap(k, raw, schema, c)
+ case TypeBool:
+ // Verify that we can parse this as the correct type
+ var n bool
+ if err := mapstructure.WeakDecode(raw, &n); err != nil {
+ return nil, []error{err}
+ }
case TypeInt:
// Verify that we can parse this as an int
var n int
if err := mapstructure.WeakDecode(raw, &n); err != nil {
return nil, []error{err}
}
+ case TypeString:
+ // Verify that we can parse this as a string
+ var n string
+ if err := mapstructure.WeakDecode(raw, &n); err != nil {
+ return nil, []error{err}
+ }
+ default:
+ panic(fmt.Sprintf("Unknown validation type: %s", schema.Type))
}
return nil, nil
diff --git a/helper/schema/schema_test.go b/helper/schema/schema_test.go
index 66830869c..5351f61d7 100644
--- a/helper/schema/schema_test.go
+++ b/helper/schema/schema_test.go
@@ -98,6 +98,38 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
+ // Computed, but set in config
+ {
+ Schema: map[string]*Schema{
+ "availability_zone": &Schema{
+ Type: TypeString,
+ Optional: true,
+ Computed: true,
+ },
+ },
+
+ State: &terraform.InstanceState{
+ Attributes: map[string]string{
+ "availability_zone": "foo",
+ },
+ },
+
+ Config: map[string]interface{}{
+ "availability_zone": "bar",
+ },
+
+ Diff: &terraform.InstanceDiff{
+ Attributes: map[string]*terraform.ResourceAttrDiff{
+ "availability_zone": &terraform.ResourceAttrDiff{
+ Old: "foo",
+ New: "bar",
+ },
+ },
+ },
+
+ Err: false,
+ },
+
// Default
{
Schema: map[string]*Schema{
@@ -342,6 +374,31 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
+ /*
+ * Bool
+ */
+ {
+ Schema: map[string]*Schema{
+ "delete": &Schema{
+ Type: TypeBool,
+ Optional: true,
+ Default: false,
+ },
+ },
+
+ State: &terraform.InstanceState{
+ Attributes: map[string]string{
+ "delete": "false",
+ },
+ },
+
+ Config: nil,
+
+ Diff: nil,
+
+ Err: false,
+ },
+
/*
* List decode
*/
@@ -808,6 +865,14 @@ func TestSchemaMap_Diff(t *testing.T) {
Old: "2",
New: "3",
},
+ "ports.0": &terraform.ResourceAttrDiff{
+ Old: "1",
+ New: "1",
+ },
+ "ports.1": &terraform.ResourceAttrDiff{
+ Old: "2",
+ New: "2",
+ },
"ports.2": &terraform.ResourceAttrDiff{
Old: "",
New: "5",
@@ -1166,6 +1231,67 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false,
},
+ {
+ Schema: map[string]*Schema{
+ "vars": &Schema{
+ Type: TypeMap,
+ Optional: true,
+ Computed: true,
+ },
+ },
+
+ State: &terraform.InstanceState{
+ Attributes: map[string]string{
+ "vars.foo": "bar",
+ },
+ },
+
+ Config: map[string]interface{}{
+ "vars": []interface{}{
+ map[string]interface{}{
+ "bar": "baz",
+ },
+ },
+ },
+
+ Diff: &terraform.InstanceDiff{
+ Attributes: map[string]*terraform.ResourceAttrDiff{
+ "vars.foo": &terraform.ResourceAttrDiff{
+ Old: "bar",
+ New: "",
+ NewRemoved: true,
+ },
+ "vars.bar": &terraform.ResourceAttrDiff{
+ Old: "",
+ New: "baz",
+ },
+ },
+ },
+
+ Err: false,
+ },
+
+ {
+ Schema: map[string]*Schema{
+ "vars": &Schema{
+ Type: TypeMap,
+ Computed: true,
+ },
+ },
+
+ State: &terraform.InstanceState{
+ Attributes: map[string]string{
+ "vars.foo": "bar",
+ },
+ },
+
+ Config: nil,
+
+ Diff: nil,
+
+ Err: false,
+ },
+
{
Schema: map[string]*Schema{
"config_vars": &Schema{
@@ -1413,9 +1539,7 @@ func TestSchemaMap_Input(t *testing.T) {
"availability_zone": "foo",
},
- Result: map[string]interface{}{
- "availability_zone": "bar",
- },
+ Result: map[string]interface{}{},
Err: false,
},
@@ -1494,14 +1618,16 @@ func TestSchemaMap_Input(t *testing.T) {
input := new(terraform.MockUIInput)
input.InputReturnMap = tc.Input
- actual, err := schemaMap(tc.Schema).Input(
- input, terraform.NewResourceConfig(c))
+ rc := terraform.NewResourceConfig(c)
+ rc.Config = make(map[string]interface{})
+
+ actual, err := schemaMap(tc.Schema).Input(input, rc)
if (err != nil) != tc.Err {
t.Fatalf("#%d err: %s", i, err)
}
- if !reflect.DeepEqual(tc.Result, actual.Raw) {
- t.Fatalf("#%d: bad:\n\n%#v", i, actual.Raw)
+ if !reflect.DeepEqual(tc.Result, actual.Config) {
+ t.Fatalf("#%d: bad:\n\n%#v", i, actual.Config)
}
}
}
@@ -1788,6 +1914,25 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true,
},
+ {
+ Schema: map[string]*Schema{
+ "user_data": &Schema{
+ Type: TypeString,
+ Optional: true,
+ },
+ },
+
+ Config: map[string]interface{}{
+ "user_data": []interface{}{
+ map[string]interface{}{
+ "foo": "bar",
+ },
+ },
+ },
+
+ Err: true,
+ },
+
// Bad type, interpolated
{
Schema: map[string]*Schema{
@@ -1970,6 +2115,91 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true,
},
+
+ // Not a set
+ {
+ Schema: map[string]*Schema{
+ "ports": &Schema{
+ Type: TypeSet,
+ Required: true,
+ Elem: &Schema{Type: TypeInt},
+ Set: func(a interface{}) int {
+ return a.(int)
+ },
+ },
+ },
+
+ Config: map[string]interface{}{
+ "ports": "foo",
+ },
+
+ Err: true,
+ },
+
+ // Maps
+ {
+ Schema: map[string]*Schema{
+ "user_data": &Schema{
+ Type: TypeMap,
+ Optional: true,
+ },
+ },
+
+ Config: map[string]interface{}{
+ "user_data": "foo",
+ },
+
+ Err: true,
+ },
+
+ {
+ Schema: map[string]*Schema{
+ "user_data": &Schema{
+ Type: TypeMap,
+ Optional: true,
+ },
+ },
+
+ Config: map[string]interface{}{
+ "user_data": []interface{}{
+ map[string]interface{}{
+ "foo": "bar",
+ },
+ },
+ },
+ },
+
+ {
+ Schema: map[string]*Schema{
+ "user_data": &Schema{
+ Type: TypeMap,
+ Optional: true,
+ },
+ },
+
+ Config: map[string]interface{}{
+ "user_data": map[string]interface{}{
+ "foo": "bar",
+ },
+ },
+ },
+
+ {
+ Schema: map[string]*Schema{
+ "user_data": &Schema{
+ Type: TypeMap,
+ Optional: true,
+ },
+ },
+
+ Config: map[string]interface{}{
+ "user_data": []interface{}{
+ "foo",
+ },
+ },
+
+ Err: true,
+ },
}
for i, tc := range cases {
diff --git a/helper/schema/set.go b/helper/schema/set.go
index 21e329f22..92966ea59 100644
--- a/helper/schema/set.go
+++ b/helper/schema/set.go
@@ -14,6 +14,17 @@ type Set struct {
once sync.Once
}
+// NewSet is a convenience method for creating a new set with the given
+// items.
+func NewSet(f SchemaSetFunc, items []interface{}) *Set {
+ s := &Set{F: f}
+ for _, i := range items {
+ s.Add(i)
+ }
+
+ return s
+}
+
// Add adds an item to the set if it isn't already in the set.
func (s *Set) Add(item interface{}) {
s.add(item)
diff --git a/main.go b/main.go
index 46f143ba5..abd0e18e3 100644
--- a/main.go
+++ b/main.go
@@ -84,6 +84,9 @@ func realMain() int {
func wrappedMain() int {
log.SetOutput(os.Stderr)
+ log.Printf(
+ "[INFO] Terraform version: %s %s %s",
+ Version, VersionPrerelease, GitCommit)
// Load the configuration
config := BuiltinConfig
diff --git a/terraform/context.go b/terraform/context.go
index 3565fbe66..e2d8bbd33 100644
--- a/terraform/context.go
+++ b/terraform/context.go
@@ -531,6 +531,14 @@ func (c *walkContext) Walk() error {
outputs := make(map[string]string)
for _, o := range conf.Outputs {
if err := c.computeVars(o.RawConfig, nil); err != nil {
+ // If we're refreshing, then we ignore output errors. This is
+ // properly not fully the correct behavior, but fixes a range
+ // of issues right now. As we expand test cases to find the
+ // correct behavior, this will likely be removed.
+ if c.Operation == walkRefresh {
+ continue
+ }
+
return err
}
vraw := o.RawConfig.Config()["value"]
@@ -600,6 +608,7 @@ func (c *walkContext) inputWalkFn() depgraph.WalkFunc {
raw = sharedProvider.Config.RawConfig
}
rc := NewResourceConfig(raw)
+ rc.Config = make(map[string]interface{})
// Wrap the input into a namespace
input := &PrefixUIInput{
@@ -617,8 +626,8 @@ func (c *walkContext) inputWalkFn() depgraph.WalkFunc {
return fmt.Errorf(
"Error configuring %s: %s", k, err)
}
- if newc != nil {
- configs[k] = newc.Raw
+ if newc != nil && len(newc.Config) > 0 {
+ configs[k] = newc.Config
}
}
@@ -700,7 +709,7 @@ func (c *walkContext) applyWalkFn() depgraph.WalkFunc {
// We create a new instance if there was no ID
// previously or the diff requires re-creating the
// underlying instance
- createNew := is.ID == "" || diff.RequiresNew()
+ createNew := (is.ID == "" && !diff.Destroy) || diff.RequiresNew()
// With the completed diff, apply!
log.Printf("[DEBUG] %s: Executing Apply", r.Id)
@@ -1034,18 +1043,20 @@ func (c *walkContext) validateWalkFn() depgraph.WalkFunc {
// Interpolate the count and verify it is non-negative
rc := NewResourceConfig(rn.Config.RawCount)
rc.interpolate(c, rn.Resource)
- count, err := rn.Config.Count()
- if err == nil {
- if count < 0 {
- err = fmt.Errorf(
- "%s error: count must be positive", rn.Resource.Id)
+ if !rc.IsComputed(rn.Config.RawCount.Key) {
+ count, err := rn.Config.Count()
+ if err == nil {
+ if count < 0 {
+ err = fmt.Errorf(
+ "%s error: count must be positive", rn.Resource.Id)
+ }
+ }
+ if err != nil {
+ l.Lock()
+ defer l.Unlock()
+ meta.Errs = append(meta.Errs, err)
+ return nil
}
- }
- if err != nil {
- l.Lock()
- defer l.Unlock()
- meta.Errs = append(meta.Errs, err)
- return nil
}
return c.genericWalkResource(rn, walkFn)
@@ -1201,9 +1212,17 @@ func (c *walkContext) genericWalkFn(cb genericWalkFunc) depgraph.WalkFunc {
}
for k, p := range sharedProvider.Providers {
- // Merge the configurations to get what we use to configure with
+ // Interpolate our own configuration before merging
+ if sharedProvider.Config != nil {
+ rc := NewResourceConfig(sharedProvider.Config.RawConfig)
+ rc.interpolate(c, nil)
+ }
+
+ // Merge the configurations to get what we use to configure
+ // with. We don't need to interpolate this because the
+ // lines above verify that all parents are interpolated
+ // properly.
rc := sharedProvider.MergeConfig(false, cs[k])
- rc.interpolate(c, nil)
log.Printf("[INFO] Configuring provider: %s", k)
err := p.Configure(rc)
@@ -1269,6 +1288,20 @@ func (c *walkContext) genericWalkResource(
rc := NewResourceConfig(rn.Config.RawCount)
rc.interpolate(c, rn.Resource)
+ // If we're validating, then we set the count to 1 if it is computed
+ if c.Operation == walkValidate {
+ if key := rn.Config.RawCount.Key; rc.IsComputed(key) {
+ // Preserve the old value so that we reset it properly
+ old := rn.Config.RawCount.Raw[key]
+ defer func() {
+ rn.Config.RawCount.Raw[key] = old
+ }()
+
+ // Set th count to 1 for validation purposes
+ rn.Config.RawCount.Raw[key] = "1"
+ }
+ }
+
// Expand the node to the actual resources
g, err := rn.Expand()
if err != nil {
@@ -1515,7 +1548,7 @@ func (c *walkContext) computeVars(
continue
}
- if c.Operation == walkValidate {
+ if _, ok := vs[n]; !ok && c.Operation == walkValidate {
vs[n] = config.UnknownVariableValue
continue
}
@@ -1578,19 +1611,25 @@ func (c *walkContext) computeResourceVariable(
// Get the relevant module
module := c.Context.state.ModuleByPath(c.Path)
- r, ok := module.Resources[id]
- if !ok {
- if v.Multi && v.Index == 0 {
+ var r *ResourceState
+ if module != nil {
+ var ok bool
+ r, ok = module.Resources[id]
+ if !ok && v.Multi && v.Index == 0 {
r, ok = module.Resources[v.ResourceId()]
}
if !ok {
- return "", fmt.Errorf(
- "Resource '%s' not found for variable '%s'",
- id,
- v.FullKey())
+ r = nil
}
}
+ if r == nil {
+ return "", fmt.Errorf(
+ "Resource '%s' not found for variable '%s'",
+ id,
+ v.FullKey())
+ }
+
if r.Primary == nil {
goto MISSING
}
diff --git a/terraform/context_test.go b/terraform/context_test.go
index dd19c654e..5ad260763 100644
--- a/terraform/context_test.go
+++ b/terraform/context_test.go
@@ -1,6 +1,7 @@
package terraform
import (
+ "bytes"
"fmt"
"os"
"reflect"
@@ -127,6 +128,50 @@ func TestContextValidate_countNegative(t *testing.T) {
}
}
+func TestContextValidate_countVariable(t *testing.T) {
+ p := testProvider("aws")
+ m := testModule(t, "apply-count-variable")
+ c := testContext(t, &ContextOpts{
+ Module: m,
+ Providers: map[string]ResourceProviderFactory{
+ "aws": testProviderFuncFixed(p),
+ },
+ })
+
+ w, e := c.Validate()
+ if len(w) > 0 {
+ t.Fatalf("bad: %#v", w)
+ }
+ if len(e) > 0 {
+ for _, err := range e {
+ t.Errorf("bad: %s", err)
+ }
+ t.FailNow()
+ }
+}
+
+func TestContextValidate_countVariableNoDefault(t *testing.T) {
+ p := testProvider("aws")
+ m := testModule(t, "validate-count-variable")
+ c := testContext(t, &ContextOpts{
+ Module: m,
+ Providers: map[string]ResourceProviderFactory{
+ "aws": testProviderFuncFixed(p),
+ },
+ })
+
+ w, e := c.Validate()
+ if len(w) > 0 {
+ t.Fatalf("bad: %#v", w)
+ }
+ if len(e) > 1 {
+ for _, err := range e {
+ t.Errorf("bad: %s", err)
+ }
+ t.FailNow()
+ }
+}
+
func TestContextValidate_moduleBadResource(t *testing.T) {
m := testModule(t, "validate-module-bad-rc")
p := testProvider("aws")
@@ -602,11 +647,11 @@ func TestContextInput_provider(t *testing.T) {
var actual interface{}
p.InputFn = func(i UIInput, c *ResourceConfig) (*ResourceConfig, error) {
- c.Raw["foo"] = "bar"
+ c.Config["foo"] = "bar"
return c, nil
}
p.ConfigureFn = func(c *ResourceConfig) error {
- actual = c.Raw["foo"]
+ actual = c.Config["foo"]
return nil
}
@@ -648,11 +693,11 @@ func TestContextInput_providerId(t *testing.T) {
return nil, err
}
- c.Raw["foo"] = v
+ c.Config["foo"] = v
return c, nil
}
p.ConfigureFn = func(c *ResourceConfig) error {
- actual = c.Raw["foo"]
+ actual = c.Config["foo"]
return nil
}
@@ -700,11 +745,11 @@ func TestContextInput_providerOnly(t *testing.T) {
var actual interface{}
p.InputFn = func(i UIInput, c *ResourceConfig) (*ResourceConfig, error) {
- c.Raw["foo"] = "bar"
+ c.Config["foo"] = "bar"
return c, nil
}
p.ConfigureFn = func(c *ResourceConfig) error {
- actual = c.Raw["foo"]
+ actual = c.Config["foo"]
return nil
}
@@ -732,6 +777,54 @@ func TestContextInput_providerOnly(t *testing.T) {
}
}
+func TestContextInput_providerVars(t *testing.T) {
+ input := new(MockUIInput)
+ m := testModule(t, "input-provider-with-vars")
+ p := testProvider("aws")
+ p.ApplyFn = testApplyFn
+ p.DiffFn = testDiffFn
+ ctx := testContext(t, &ContextOpts{
+ Module: m,
+ Providers: map[string]ResourceProviderFactory{
+ "aws": testProviderFuncFixed(p),
+ },
+ Variables: map[string]string{
+ "foo": "bar",
+ },
+ UIInput: input,
+ })
+
+ input.InputReturnMap = map[string]string{
+ "var.foo": "bar",
+ }
+
+ var actual interface{}
+ p.InputFn = func(i UIInput, c *ResourceConfig) (*ResourceConfig, error) {
+ c.Config["bar"] = "baz"
+ return c, nil
+ }
+ p.ConfigureFn = func(c *ResourceConfig) error {
+ actual, _ = c.Get("foo")
+ return nil
+ }
+
+ if err := ctx.Input(InputModeStd); err != nil {
+ t.Fatalf("err: %s", err)
+ }
+
+ if _, err := ctx.Plan(nil); err != nil {
+ t.Fatalf("err: %s", err)
+ }
+
+ if _, err := ctx.Apply(); err != nil {
+ t.Fatalf("err: %s", err)
+ }
+
+ if !reflect.DeepEqual(actual, "bar") {
+ t.Fatalf("bad: %#v", actual)
+ }
+}
+
func TestContextInput_varOnly(t *testing.T) {
input := new(MockUIInput)
m := testModule(t, "input-provider-vars")
@@ -1203,6 +1296,35 @@ func TestContextApply_countTainted(t *testing.T) {
t.Fatalf("bad: \n%s", actual)
}
}
+
+func TestContextApply_countVariable(t *testing.T) {
+ m := testModule(t, "apply-count-variable")
+ p := testProvider("aws")
+ p.ApplyFn = testApplyFn
+ p.DiffFn = testDiffFn
+ ctx := testContext(t, &ContextOpts{
+ Module: m,
+ Providers: map[string]ResourceProviderFactory{
+ "aws": testProviderFuncFixed(p),
+ },
+ })
+
+ if _, err := ctx.Plan(nil); err != nil {
+ t.Fatalf("err: %s", err)
+ }
+
+ state, err := ctx.Apply()
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+
+ actual := strings.TrimSpace(state.String())
+ expected := strings.TrimSpace(testTerraformApplyCountVariableStr)
+ if actual != expected {
+ t.Fatalf("bad: \n%s", actual)
+ }
+}
+
func TestContextApply_module(t *testing.T) {
m := testModule(t, "apply-module")
p := testProvider("aws")
@@ -1994,6 +2116,71 @@ func TestContextApply_destroyOrphan(t *testing.T) {
}
}
+func TestContextApply_destroyTaintedProvisioner(t *testing.T) {
+ m := testModule(t, "apply-destroy-provisioner")
+ p := testProvider("aws")
+ pr := testProvisioner()
+ p.ApplyFn = testApplyFn
+ p.DiffFn = testDiffFn
+
+ called := false
+ pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error {
+ called = true
+ return nil
+ }
+
+ s := &State{
+ Modules: []*ModuleState{
+ &ModuleState{
+ Path: rootModulePath,
+ Resources: map[string]*ResourceState{
+ "aws_instance.foo": &ResourceState{
+ Type: "aws_instance",
+ Tainted: []*InstanceState{
+ &InstanceState{
+ ID: "bar",
+ Attributes: map[string]string{
+ "id": "bar",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ ctx := testContext(t, &ContextOpts{
+ Module: m,
+ Providers: map[string]ResourceProviderFactory{
+ "aws": testProviderFuncFixed(p),
+ },
+ Provisioners: map[string]ResourceProvisionerFactory{
+ "shell": testProvisionerFuncFixed(pr),
+ },
+ State: s,
+ })
+
+ if _, err := ctx.Plan(&PlanOpts{Destroy: true}); err != nil {
+ t.Fatalf("err: %s", err)
+ }
+
+ state, err := ctx.Apply()
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+
+ if called {
+ t.Fatal("provisioner should not be called")
+ }
+
+ actual := strings.TrimSpace(state.String())
+ expected := strings.TrimSpace("
- * Based on the lexical grammar at
- * http://golang.org/doc/go_spec.html#Lexical_elements
- *
- * Go uses a minimal style for highlighting so the below does not distinguish
- * strings, keywords, literals, etc. by design.
- * From a discussion with the Go designers:
- * Page Not Found
diff --git a/website/source/assets/fonts/.!50177!league_gothic-webfont.eot b/website/source/assets/fonts/.!50177!league_gothic-webfont.eot
deleted file mode 100755
index e69de29bb..000000000
diff --git a/website/source/assets/fonts/league_gothic-webfont.eot b/website/source/assets/fonts/league_gothic-webfont.eot
deleted file mode 100755
index 598dcbc06..000000000
Binary files a/website/source/assets/fonts/league_gothic-webfont.eot and /dev/null differ
diff --git a/website/source/assets/fonts/league_gothic-webfont.svg b/website/source/assets/fonts/league_gothic-webfont.svg
deleted file mode 100644
index 201cfe1fe..000000000
--- a/website/source/assets/fonts/league_gothic-webfont.svg
+++ /dev/null
@@ -1,230 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/website/source/assets/fonts/league_gothic-webfont.ttf b/website/source/assets/fonts/league_gothic-webfont.ttf
deleted file mode 100644
index 29f896a79..000000000
Binary files a/website/source/assets/fonts/league_gothic-webfont.ttf and /dev/null differ
diff --git a/website/source/assets/fonts/league_gothic-webfont.woff b/website/source/assets/fonts/league_gothic-webfont.woff
deleted file mode 100644
index 71117fb7f..000000000
Binary files a/website/source/assets/fonts/league_gothic-webfont.woff and /dev/null differ
diff --git a/website/source/assets/javascripts/lang-go.js b/website/source/assets/javascripts/lang-go.js
deleted file mode 100644
index f329e29b3..000000000
--- a/website/source/assets/javascripts/lang-go.js
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (C) 2010 Google Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-
-
-/**
- * @fileoverview
- * Registers a language handler for the Go language..
- *
- * On Thursday, July 22, 2010, Mike Samuel <...> wrote:
- * > On Thu, Jul 22, 2010, Rob 'Commander' Pike <...> wrote:
- * >> Personally, I would vote for the subdued style godoc presents at http://golang.org
- * >>
- * >> Not as fancy as some like, but a case can be made it's the official style.
- * >> If people want more colors, I wouldn't fight too hard, in the interest of
- * >> encouragement through familiarity, but even then I would ask to shy away
- * >> from technicolor starbursts.
- * >
- * > Like http://golang.org/pkg/go/scanner/ where comments are blue and all
- * > other content is black? I can do that.
- *
- *
- * @author mikesamuel@gmail.com
- */
-
-PR['registerLangHandler'](
- PR['createSimpleLexer'](
- [
- // Whitespace is made up of spaces, tabs and newline characters.
- [PR['PR_PLAIN'], /^[\t\n\r \xA0]+/, null, '\t\n\r \xA0'],
- // Not escaped as a string. See note on minimalism above.
- [PR['PR_PLAIN'], /^(?:\"(?:[^\"\\]|\\[\s\S])*(?:\"|$)|\'(?:[^\'\\]|\\[\s\S])+(?:\'|$)|`[^`]*(?:`|$))/, null, '"\'']
- ],
- [
- // Block comments are delimited by /* and */.
- // Single-line comments begin with // and extend to the end of a line.
- [PR['PR_COMMENT'], /^(?:\/\/[^\r\n]*|\/\*[\s\S]*?\*\/)/],
- [PR['PR_PLAIN'], /^(?:[^\/\"\'`]|\/(?![\/\*]))+/i]
- ]),
- ['go']);
diff --git a/website/source/assets/javascripts/prettify.js b/website/source/assets/javascripts/prettify.js
deleted file mode 100644
index 7b990496d..000000000
--- a/website/source/assets/javascripts/prettify.js
+++ /dev/null
@@ -1,30 +0,0 @@
-!function(){var q=null;window.PR_SHOULD_USE_CONTINUATION=!0;
-(function(){function S(a){function d(e){var b=e.charCodeAt(0);if(b!==92)return b;var a=e.charAt(1);return(b=r[a])?b:"0"<=a&&a<="7"?parseInt(e.substring(1),8):a==="u"||a==="x"?parseInt(e.substring(2),16):e.charCodeAt(1)}function g(e){if(e<32)return(e<16?"\\x0":"\\x")+e.toString(16);e=String.fromCharCode(e);return e==="\\"||e==="-"||e==="]"||e==="^"?"\\"+e:e}function b(e){var b=e.substring(1,e.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),e=[],a=
-b[0]==="^",c=["["];a&&c.push("^");for(var a=a?1:0,f=b.length;a
diff --git a/website/source/layouts/_meta.erb b/website/source/layouts/_meta.erb index c05381bb6..1f21b8dae 100644 --- a/website/source/layouts/_meta.erb +++ b/website/source/layouts/_meta.erb @@ -1,35 +1,21 @@ -
- - - +
+ + + - + -
+
- -<%= stylesheet_link_tag "application" %> + <%= stylesheet_link_tag "application" %> - - + - - + <%= yield_content :head %> + -<%= yield_content :head %> - - - -