merging from hashicorp master

This commit is contained in:
Peter Beams 2015-03-06 15:53:31 +00:00
commit e59d4fc976
73 changed files with 2383 additions and 895 deletions

View File

@ -10,6 +10,7 @@ install: make updatedeps
script: script:
- go test ./... - go test ./...
- make vet
#- go test -race ./... #- go test -race ./...
branches: branches:

View File

@ -1,5 +1,12 @@
## 0.4.0 (unreleased) ## 0.4.0 (unreleased)
BACKWARDS INCOMPATIBILITIES:
* Commands `terraform push` and `terraform pull` are now nested under
the `remote` command: `terraform remote push` and `terraform remote pull`.
The old `remote` functionality is now at `terraform remote config`. This
consolidates all remote state management under one command.
FEATURES: FEATURES:
* **New provider: `dme` (DNSMadeEasy)** [GH-855] * **New provider: `dme` (DNSMadeEasy)** [GH-855]
@ -16,6 +23,8 @@ FEATURES:
IMPROVEMENTS: IMPROVEMENTS:
* **New config function: `format`** - Format a string using `sprintf`
format. [GH-1096]
* **New config function: `replace`** - Search and replace string values. * **New config function: `replace`** - Search and replace string values.
Search can be a regular expression. See documentation for more Search can be a regular expression. See documentation for more
info. [GH-1029] info. [GH-1029]
@ -39,9 +48,15 @@ BUG FIXES:
"resource.0" would ignore the latter completely. [GH-1086] "resource.0" would ignore the latter completely. [GH-1086]
* providers/aws: manually deleted VPC removes it from the state * providers/aws: manually deleted VPC removes it from the state
* providers/aws: `source_dest_check` regression fixed (now works). [GH-1020] * providers/aws: `source_dest_check` regression fixed (now works). [GH-1020]
* providers/aws: Longer wait times for DB instances
* providers/digitalocean: Waits until droplet is ready to be destroyed [GH-1057] * providers/digitalocean: Waits until droplet is ready to be destroyed [GH-1057]
* providers/digitalocean: More lenient about 404's while waiting [GH-1062] * providers/digitalocean: More lenient about 404's while waiting [GH-1062]
* providers/aws: Longer wait times for DB instances * providers/google: Network data in state was not being stored. [GH-1095]
PLUGIN CHANGES:
* New `helper/schema` fields for resources: `Deprecated` and `Removed` allow
plugins to generate warning or error messages when a given attribute is used.
## 0.3.7 (February 19, 2015) ## 0.3.7 (February 19, 2015)

View File

@ -11,12 +11,13 @@ import (
"github.com/mitchellh/goamz/ec2" "github.com/mitchellh/goamz/ec2"
awsGo "github.com/hashicorp/aws-sdk-go/aws" awsGo "github.com/hashicorp/aws-sdk-go/aws"
awsec2 "github.com/hashicorp/aws-sdk-go/gen/ec2"
"github.com/hashicorp/aws-sdk-go/gen/autoscaling" "github.com/hashicorp/aws-sdk-go/gen/autoscaling"
"github.com/hashicorp/aws-sdk-go/gen/elb" "github.com/hashicorp/aws-sdk-go/gen/elb"
"github.com/hashicorp/aws-sdk-go/gen/rds" "github.com/hashicorp/aws-sdk-go/gen/rds"
"github.com/hashicorp/aws-sdk-go/gen/route53" "github.com/hashicorp/aws-sdk-go/gen/route53"
"github.com/hashicorp/aws-sdk-go/gen/s3" "github.com/hashicorp/aws-sdk-go/gen/s3"
awsEC2 "github.com/hashicorp/aws-sdk-go/gen/ec2"
) )
type Config struct { type Config struct {
@ -28,7 +29,7 @@ type Config struct {
type AWSClient struct { type AWSClient struct {
ec2conn *ec2.EC2 ec2conn *ec2.EC2
ec2conn2 *awsec2.EC2 awsEC2conn *awsEC2.EC2
elbconn *elb.ELB elbconn *elb.ELB
autoscalingconn *autoscaling.AutoScaling autoscalingconn *autoscaling.AutoScaling
s3conn *s3.S3 s3conn *s3.S3
@ -80,6 +81,8 @@ func (c *Config) Client() (interface{}, error) {
// See http://docs.aws.amazon.com/general/latest/gr/sigv4_changes.html // See http://docs.aws.amazon.com/general/latest/gr/sigv4_changes.html
log.Println("[INFO] Initializing Route53 connection") log.Println("[INFO] Initializing Route53 connection")
client.r53conn = route53.New(creds, "us-east-1", nil) client.r53conn = route53.New(creds, "us-east-1", nil)
log.Println("[INFO] Initializing AWS-GO EC2 Connection")
client.awsEC2conn = awsEC2.New(creds, c.Region, nil)
} }
if len(errs) > 0 { if len(errs) > 0 {

View File

@ -324,7 +324,7 @@ func resourceAwsDbInstanceRead(d *schema.ResourceData, meta interface{}) error {
d.Set("parameter_group_name", *v.DBParameterGroups[0].DBParameterGroupName) d.Set("parameter_group_name", *v.DBParameterGroups[0].DBParameterGroupName)
} }
d.Set("address", *v.Endpoint.Port) d.Set("address", *v.Endpoint.Address)
d.Set("endpoint", fmt.Sprintf("%s:%d", *v.Endpoint.Address, *v.Endpoint.Port)) d.Set("endpoint", fmt.Sprintf("%s:%d", *v.Endpoint.Address, *v.Endpoint.Port))
d.Set("status", *v.DBInstanceStatus) d.Set("status", *v.DBInstanceStatus)
d.Set("storage_encrypted", *v.StorageEncrypted) d.Set("storage_encrypted", *v.StorageEncrypted)

View File

@ -6,9 +6,10 @@ import (
"strings" "strings"
"time" "time"
"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/ec2"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"github.com/mitchellh/goamz/ec2"
) )
func resourceAwsEip() *schema.Resource { func resourceAwsEip() *schema.Resource {
@ -59,7 +60,7 @@ func resourceAwsEip() *schema.Resource {
} }
func resourceAwsEipCreate(d *schema.ResourceData, meta interface{}) error { func resourceAwsEipCreate(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn ec2conn := meta.(*AWSClient).awsEC2conn
// By default, we're not in a VPC // By default, we're not in a VPC
domainOpt := "" domainOpt := ""
@ -67,12 +68,12 @@ func resourceAwsEipCreate(d *schema.ResourceData, meta interface{}) error {
domainOpt = "vpc" domainOpt = "vpc"
} }
allocOpts := ec2.AllocateAddress{ allocOpts := &ec2.AllocateAddressRequest{
Domain: domainOpt, Domain: aws.String(domainOpt),
} }
log.Printf("[DEBUG] EIP create configuration: %#v", allocOpts) log.Printf("[DEBUG] EIP create configuration: %#v", allocOpts)
allocResp, err := ec2conn.AllocateAddress(&allocOpts) allocResp, err := ec2conn.AllocateAddress(allocOpts)
if err != nil { if err != nil {
return fmt.Errorf("Error creating EIP: %s", err) return fmt.Errorf("Error creating EIP: %s", err)
} }
@ -86,17 +87,17 @@ func resourceAwsEipCreate(d *schema.ResourceData, meta interface{}) error {
// it defaults to using the public IP // it defaults to using the public IP
log.Printf("[DEBUG] EIP Allocate: %#v", allocResp) log.Printf("[DEBUG] EIP Allocate: %#v", allocResp)
if d.Get("domain").(string) == "vpc" { if d.Get("domain").(string) == "vpc" {
d.SetId(allocResp.AllocationId) d.SetId(*allocResp.AllocationID)
} else { } else {
d.SetId(allocResp.PublicIp) d.SetId(*allocResp.PublicIP)
} }
log.Printf("[INFO] EIP ID: %s (domain: %v)", d.Id(), allocResp.Domain) log.Printf("[INFO] EIP ID: %s (domain: %v)", d.Id(), *allocResp.Domain)
return resourceAwsEipUpdate(d, meta) return resourceAwsEipUpdate(d, meta)
} }
func resourceAwsEipRead(d *schema.ResourceData, meta interface{}) error { func resourceAwsEipRead(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn ec2conn := meta.(*AWSClient).awsEC2conn
domain := resourceAwsEipDomain(d) domain := resourceAwsEipDomain(d)
id := d.Id() id := d.Id()
@ -113,9 +114,13 @@ func resourceAwsEipRead(d *schema.ResourceData, meta interface{}) error {
"[DEBUG] EIP describe configuration: %#v, %#v (domain: %s)", "[DEBUG] EIP describe configuration: %#v, %#v (domain: %s)",
assocIds, publicIps, domain) assocIds, publicIps, domain)
describeAddresses, err := ec2conn.Addresses(publicIps, assocIds, nil) req := &ec2.DescribeAddressesRequest{
AllocationIDs: assocIds,
PublicIPs: publicIps,
}
describeAddresses, err := ec2conn.DescribeAddresses(req)
if err != nil { if err != nil {
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidAllocationID.NotFound" { if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidAllocationID.NotFound" {
d.SetId("") d.SetId("")
return nil return nil
} }
@ -125,8 +130,8 @@ func resourceAwsEipRead(d *schema.ResourceData, meta interface{}) error {
// Verify AWS returned our EIP // Verify AWS returned our EIP
if len(describeAddresses.Addresses) != 1 || if len(describeAddresses.Addresses) != 1 ||
describeAddresses.Addresses[0].AllocationId != id || *describeAddresses.Addresses[0].AllocationID != id ||
describeAddresses.Addresses[0].PublicIp != id { *describeAddresses.Addresses[0].PublicIP != id {
if err != nil { if err != nil {
return fmt.Errorf("Unable to find EIP: %#v", describeAddresses.Addresses) return fmt.Errorf("Unable to find EIP: %#v", describeAddresses.Addresses)
} }
@ -134,16 +139,16 @@ func resourceAwsEipRead(d *schema.ResourceData, meta interface{}) error {
address := describeAddresses.Addresses[0] address := describeAddresses.Addresses[0]
d.Set("association_id", address.AssociationId) d.Set("association_id", address.AssociationID)
d.Set("instance", address.InstanceId) d.Set("instance", address.InstanceID)
d.Set("public_ip", address.PublicIp) d.Set("private_ip", address.PrivateIPAddress)
d.Set("private_ip", address.PrivateIpAddress) d.Set("public_ip", address.PublicIP)
return nil return nil
} }
func resourceAwsEipUpdate(d *schema.ResourceData, meta interface{}) error { func resourceAwsEipUpdate(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn ec2conn := meta.(*AWSClient).awsEC2conn
domain := resourceAwsEipDomain(d) domain := resourceAwsEipDomain(d)
@ -151,22 +156,22 @@ func resourceAwsEipUpdate(d *schema.ResourceData, meta interface{}) error {
if v, ok := d.GetOk("instance"); ok { if v, ok := d.GetOk("instance"); ok {
instanceId := v.(string) instanceId := v.(string)
assocOpts := ec2.AssociateAddress{ assocOpts := &ec2.AssociateAddressRequest{
InstanceId: instanceId, InstanceID: aws.String(instanceId),
PublicIp: d.Id(), PublicIP: aws.String(d.Id()),
} }
// more unique ID conditionals // more unique ID conditionals
if domain == "vpc" { if domain == "vpc" {
assocOpts = ec2.AssociateAddress{ assocOpts = &ec2.AssociateAddressRequest{
InstanceId: instanceId, InstanceID: aws.String(instanceId),
AllocationId: d.Id(), AllocationID: aws.String(d.Id()),
PublicIp: "", PublicIP: aws.String(""),
} }
} }
log.Printf("[DEBUG] EIP associate configuration: %#v (domain: %v)", assocOpts, domain) log.Printf("[DEBUG] EIP associate configuration: %#v (domain: %v)", assocOpts, domain)
_, err := ec2conn.AssociateAddress(&assocOpts) _, err := ec2conn.AssociateAddress(assocOpts)
if err != nil { if err != nil {
return fmt.Errorf("Failure associating instances: %s", err) return fmt.Errorf("Failure associating instances: %s", err)
} }
@ -176,7 +181,7 @@ func resourceAwsEipUpdate(d *schema.ResourceData, meta interface{}) error {
} }
func resourceAwsEipDelete(d *schema.ResourceData, meta interface{}) error { func resourceAwsEipDelete(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn ec2conn := meta.(*AWSClient).awsEC2conn
if err := resourceAwsEipRead(d, meta); err != nil { if err := resourceAwsEipRead(d, meta); err != nil {
return err return err
@ -192,9 +197,13 @@ func resourceAwsEipDelete(d *schema.ResourceData, meta interface{}) error {
var err error var err error
switch resourceAwsEipDomain(d) { switch resourceAwsEipDomain(d) {
case "vpc": case "vpc":
_, err = ec2conn.DisassociateAddress(d.Get("association_id").(string)) err = ec2conn.DisassociateAddress(&ec2.DisassociateAddressRequest{
AssociationID: aws.String(d.Get("association_id").(string)),
})
case "standard": case "standard":
_, err = ec2conn.DisassociateAddressClassic(d.Get("public_ip").(string)) err = ec2conn.DisassociateAddress(&ec2.DisassociateAddressRequest{
PublicIP: aws.String(d.Get("public_ip").(string)),
})
} }
if err != nil { if err != nil {
return err return err
@ -209,16 +218,20 @@ func resourceAwsEipDelete(d *schema.ResourceData, meta interface{}) error {
log.Printf( log.Printf(
"[DEBUG] EIP release (destroy) address allocation: %v", "[DEBUG] EIP release (destroy) address allocation: %v",
d.Id()) d.Id())
_, err = ec2conn.ReleaseAddress(d.Id()) err = ec2conn.ReleaseAddress(&ec2.ReleaseAddressRequest{
AllocationID: aws.String(d.Id()),
})
case "standard": case "standard":
log.Printf("[DEBUG] EIP release (destroy) address: %v", d.Id()) log.Printf("[DEBUG] EIP release (destroy) address: %v", d.Id())
_, err = ec2conn.ReleasePublicAddress(d.Id()) err = ec2conn.ReleaseAddress(&ec2.ReleaseAddressRequest{
PublicIP: aws.String(d.Id()),
})
} }
if err == nil { if err == nil {
return nil return nil
} }
if _, ok := err.(*ec2.Error); !ok { if _, ok := err.(aws.APIError); !ok {
return resource.RetryError{Err: err} return resource.RetryError{Err: err}
} }

View File

@ -5,9 +5,10 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/ec2"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/goamz/ec2"
) )
func TestAccAWSEIP_normal(t *testing.T) { func TestAccAWSEIP_normal(t *testing.T) {
@ -57,24 +58,28 @@ func TestAccAWSEIP_instance(t *testing.T) {
} }
func testAccCheckAWSEIPDestroy(s *terraform.State) error { func testAccCheckAWSEIPDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn conn := testAccProvider.Meta().(*AWSClient).awsEC2conn
for _, rs := range s.RootModule().Resources { for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_eip" { if rs.Type != "aws_eip" {
continue continue
} }
describe, err := conn.Addresses([]string{rs.Primary.ID}, []string{}, nil) req := &ec2.DescribeAddressesRequest{
AllocationIDs: []string{},
PublicIPs: []string{rs.Primary.ID},
}
describe, err := conn.DescribeAddresses(req)
if err == nil { if err == nil {
if len(describe.Addresses) != 0 && if len(describe.Addresses) != 0 &&
describe.Addresses[0].PublicIp == rs.Primary.ID { *describe.Addresses[0].PublicIP == rs.Primary.ID {
return fmt.Errorf("EIP still exists") return fmt.Errorf("EIP still exists")
} }
} }
// Verify the error // Verify the error
providerErr, ok := err.(*ec2.Error) providerErr, ok := err.(aws.APIError)
if !ok { if !ok {
return err return err
} }
@ -89,7 +94,7 @@ func testAccCheckAWSEIPDestroy(s *terraform.State) error {
func testAccCheckAWSEIPAttributes(conf *ec2.Address) resource.TestCheckFunc { func testAccCheckAWSEIPAttributes(conf *ec2.Address) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
if conf.PublicIp == "" { if *conf.PublicIP == "" {
return fmt.Errorf("empty public_ip") return fmt.Errorf("empty public_ip")
} }
@ -108,28 +113,36 @@ func testAccCheckAWSEIPExists(n string, res *ec2.Address) resource.TestCheckFunc
return fmt.Errorf("No EIP ID is set") return fmt.Errorf("No EIP ID is set")
} }
conn := testAccProvider.Meta().(*AWSClient).ec2conn conn := testAccProvider.Meta().(*AWSClient).awsEC2conn
if strings.Contains(rs.Primary.ID, "eipalloc") { if strings.Contains(rs.Primary.ID, "eipalloc") {
describe, err := conn.Addresses([]string{}, []string{rs.Primary.ID}, nil) req := &ec2.DescribeAddressesRequest{
AllocationIDs: []string{rs.Primary.ID},
PublicIPs: []string{},
}
describe, err := conn.DescribeAddresses(req)
if err != nil { if err != nil {
return err return err
} }
if len(describe.Addresses) != 1 || if len(describe.Addresses) != 1 ||
describe.Addresses[0].AllocationId != rs.Primary.ID { *describe.Addresses[0].AllocationID != rs.Primary.ID {
return fmt.Errorf("EIP not found") return fmt.Errorf("EIP not found")
} }
*res = describe.Addresses[0] *res = describe.Addresses[0]
} else { } else {
describe, err := conn.Addresses([]string{rs.Primary.ID}, []string{}, nil) req := &ec2.DescribeAddressesRequest{
AllocationIDs: []string{},
PublicIPs: []string{rs.Primary.ID},
}
describe, err := conn.DescribeAddresses(req)
if err != nil { if err != nil {
return err return err
} }
if len(describe.Addresses) != 1 || if len(describe.Addresses) != 1 ||
describe.Addresses[0].PublicIp != rs.Primary.ID { *describe.Addresses[0].PublicIP != rs.Primary.ID {
return fmt.Errorf("EIP not found") return fmt.Errorf("EIP not found")
} }
*res = describe.Addresses[0] *res = describe.Addresses[0]

View File

@ -186,6 +186,13 @@ func resourceAwsInstance() *schema.Resource {
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
"iops": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},
}, },
}, },
Set: resourceAwsInstanceBlockDevicesHash, Set: resourceAwsInstanceBlockDevicesHash,
@ -231,6 +238,13 @@ func resourceAwsInstance() *schema.Resource {
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
"iops": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},
}, },
}, },
}, },
@ -313,6 +327,9 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
if v, ok := bd["encrypted"].(bool); ok { if v, ok := bd["encrypted"].(bool); ok {
runOpts.BlockDevices[i].Encrypted = v runOpts.BlockDevices[i].Encrypted = v
} }
if v, ok := bd["iops"].(int); ok {
runOpts.BlockDevices[i].IOPS = int64(v)
}
} }
} }
@ -477,6 +494,7 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
blockDevice["snapshot_id"] = vol.SnapshotId blockDevice["snapshot_id"] = vol.SnapshotId
blockDevice["encrypted"] = vol.Encrypted blockDevice["encrypted"] = vol.Encrypted
blockDevice["iops"] = vol.IOPS
nonRootBlockDevices = append(nonRootBlockDevices, blockDevice) nonRootBlockDevices = append(nonRootBlockDevices, blockDevice)
} }
d.Set("block_device", nonRootBlockDevices) d.Set("block_device", nonRootBlockDevices)

View File

@ -88,6 +88,11 @@ func TestAccAWSInstance_blockDevices(t *testing.T) {
fmt.Errorf("block device doesn't exist: /dev/sdb") fmt.Errorf("block device doesn't exist: /dev/sdb")
} }
// Check if the third block device exists.
if _, ok := blockDevices["/dev/sdc"]; !ok {
fmt.Errorf("block device doesn't exist: /dev/sdc")
}
return nil return nil
} }
} }
@ -114,11 +119,22 @@ func TestAccAWSInstance_blockDevices(t *testing.T) {
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "root_block_device.0.volume_type", "gp2"), "aws_instance.foo", "root_block_device.0.volume_type", "gp2"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.#", "1"), "aws_instance.foo", "block_device.#", "2"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.172787947.device_name", "/dev/sdb"), "aws_instance.foo", "block_device.172787947.device_name", "/dev/sdb"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.172787947.volume_size", "9"), "aws_instance.foo", "block_device.172787947.volume_size", "9"),
resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.172787947.iops", "0"),
// Check provisioned SSD device
resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.3336996981.volume_type", "io1"),
resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.3336996981.device_name", "/dev/sdc"),
resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.3336996981.volume_size", "10"),
resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.3336996981.iops", "100"),
testCheck(), testCheck(),
), ),
}, },
@ -391,6 +407,12 @@ resource "aws_instance" "foo" {
device_name = "/dev/sdb" device_name = "/dev/sdb"
volume_size = 9 volume_size = 9
} }
block_device {
device_name = "/dev/sdc"
volume_size = 10
volume_type = "io1"
iops = 100
}
} }
` `

View File

@ -5,9 +5,10 @@ import (
"log" "log"
"time" "time"
"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/ec2"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"github.com/mitchellh/goamz/ec2"
) )
func resourceAwsInternetGateway() *schema.Resource { func resourceAwsInternetGateway() *schema.Resource {
@ -28,7 +29,7 @@ func resourceAwsInternetGateway() *schema.Resource {
} }
func resourceAwsInternetGatewayCreate(d *schema.ResourceData, meta interface{}) error { func resourceAwsInternetGatewayCreate(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn ec2conn := meta.(*AWSClient).awsEC2conn
// Create the gateway // Create the gateway
log.Printf("[DEBUG] Creating internet gateway") log.Printf("[DEBUG] Creating internet gateway")
@ -38,8 +39,8 @@ func resourceAwsInternetGatewayCreate(d *schema.ResourceData, meta interface{})
} }
// Get the ID and store it // Get the ID and store it
ig := &resp.InternetGateway ig := resp.InternetGateway
d.SetId(ig.InternetGatewayId) d.SetId(*ig.InternetGatewayID)
log.Printf("[INFO] InternetGateway ID: %s", d.Id()) log.Printf("[INFO] InternetGateway ID: %s", d.Id())
// Attach the new gateway to the correct vpc // Attach the new gateway to the correct vpc
@ -47,7 +48,7 @@ func resourceAwsInternetGatewayCreate(d *schema.ResourceData, meta interface{})
} }
func resourceAwsInternetGatewayRead(d *schema.ResourceData, meta interface{}) error { func resourceAwsInternetGatewayRead(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn ec2conn := meta.(*AWSClient).awsEC2conn
igRaw, _, err := IGStateRefreshFunc(ec2conn, d.Id())() igRaw, _, err := IGStateRefreshFunc(ec2conn, d.Id())()
if err != nil { if err != nil {
@ -64,10 +65,10 @@ func resourceAwsInternetGatewayRead(d *schema.ResourceData, meta interface{}) er
// Gateway exists but not attached to the VPC // Gateway exists but not attached to the VPC
d.Set("vpc_id", "") d.Set("vpc_id", "")
} else { } else {
d.Set("vpc_id", ig.Attachments[0].VpcId) d.Set("vpc_id", ig.Attachments[0].VPCID)
} }
d.Set("tags", tagsToMap(ig.Tags)) d.Set("tags", tagsToMapSDK(ig.Tags))
return nil return nil
} }
@ -85,9 +86,9 @@ func resourceAwsInternetGatewayUpdate(d *schema.ResourceData, meta interface{})
} }
} }
ec2conn := meta.(*AWSClient).ec2conn ec2conn := meta.(*AWSClient).awsEC2conn
if err := setTags(ec2conn, d); err != nil { if err := setTagsSDK(ec2conn, d); err != nil {
return err return err
} }
@ -97,7 +98,7 @@ func resourceAwsInternetGatewayUpdate(d *schema.ResourceData, meta interface{})
} }
func resourceAwsInternetGatewayDelete(d *schema.ResourceData, meta interface{}) error { func resourceAwsInternetGatewayDelete(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn ec2conn := meta.(*AWSClient).awsEC2conn
// Detach if it is attached // Detach if it is attached
if err := resourceAwsInternetGatewayDetach(d, meta); err != nil { if err := resourceAwsInternetGatewayDetach(d, meta); err != nil {
@ -107,12 +108,14 @@ func resourceAwsInternetGatewayDelete(d *schema.ResourceData, meta interface{})
log.Printf("[INFO] Deleting Internet Gateway: %s", d.Id()) log.Printf("[INFO] Deleting Internet Gateway: %s", d.Id())
return resource.Retry(5*time.Minute, func() error { return resource.Retry(5*time.Minute, func() error {
_, err := ec2conn.DeleteInternetGateway(d.Id()) err := ec2conn.DeleteInternetGateway(&ec2.DeleteInternetGatewayRequest{
InternetGatewayID: aws.String(d.Id()),
})
if err == nil { if err == nil {
return nil return nil
} }
ec2err, ok := err.(*ec2.Error) ec2err, ok := err.(aws.APIError)
if !ok { if !ok {
return err return err
} }
@ -129,7 +132,7 @@ func resourceAwsInternetGatewayDelete(d *schema.ResourceData, meta interface{})
} }
func resourceAwsInternetGatewayAttach(d *schema.ResourceData, meta interface{}) error { func resourceAwsInternetGatewayAttach(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn ec2conn := meta.(*AWSClient).awsEC2conn
if d.Get("vpc_id").(string) == "" { if d.Get("vpc_id").(string) == "" {
log.Printf( log.Printf(
@ -143,7 +146,10 @@ func resourceAwsInternetGatewayAttach(d *schema.ResourceData, meta interface{})
d.Id(), d.Id(),
d.Get("vpc_id").(string)) d.Get("vpc_id").(string))
_, err := ec2conn.AttachInternetGateway(d.Id(), d.Get("vpc_id").(string)) err := ec2conn.AttachInternetGateway(&ec2.AttachInternetGatewayRequest{
InternetGatewayID: aws.String(d.Id()),
VPCID: aws.String(d.Get("vpc_id").(string)),
})
if err != nil { if err != nil {
return err return err
} }
@ -171,7 +177,7 @@ func resourceAwsInternetGatewayAttach(d *schema.ResourceData, meta interface{})
} }
func resourceAwsInternetGatewayDetach(d *schema.ResourceData, meta interface{}) error { func resourceAwsInternetGatewayDetach(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn ec2conn := meta.(*AWSClient).awsEC2conn
// Get the old VPC ID to detach from // Get the old VPC ID to detach from
vpcID, _ := d.GetChange("vpc_id") vpcID, _ := d.GetChange("vpc_id")
@ -189,9 +195,12 @@ func resourceAwsInternetGatewayDetach(d *schema.ResourceData, meta interface{})
vpcID.(string)) vpcID.(string))
wait := true wait := true
_, err := ec2conn.DetachInternetGateway(d.Id(), vpcID.(string)) err := ec2conn.DetachInternetGateway(&ec2.DetachInternetGatewayRequest{
InternetGatewayID: aws.String(d.Id()),
VPCID: aws.String(vpcID.(string)),
})
if err != nil { if err != nil {
ec2err, ok := err.(*ec2.Error) ec2err, ok := err.(aws.APIError)
if ok { if ok {
if ec2err.Code == "InvalidInternetGatewayID.NotFound" { if ec2err.Code == "InvalidInternetGatewayID.NotFound" {
err = nil err = nil
@ -232,9 +241,11 @@ func resourceAwsInternetGatewayDetach(d *schema.ResourceData, meta interface{})
// an internet gateway. // an internet gateway.
func IGStateRefreshFunc(ec2conn *ec2.EC2, id string) resource.StateRefreshFunc { func IGStateRefreshFunc(ec2conn *ec2.EC2, id string) resource.StateRefreshFunc {
return func() (interface{}, string, error) { return func() (interface{}, string, error) {
resp, err := ec2conn.DescribeInternetGateways([]string{id}, ec2.NewFilter()) resp, err := ec2conn.DescribeInternetGateways(&ec2.DescribeInternetGatewaysRequest{
InternetGatewayIDs: []string{id},
})
if err != nil { if err != nil {
ec2err, ok := err.(*ec2.Error) ec2err, ok := err.(aws.APIError)
if ok && ec2err.Code == "InvalidInternetGatewayID.NotFound" { if ok && ec2err.Code == "InvalidInternetGatewayID.NotFound" {
resp = nil resp = nil
} else { } else {
@ -256,16 +267,18 @@ func IGStateRefreshFunc(ec2conn *ec2.EC2, id string) resource.StateRefreshFunc {
// IGAttachStateRefreshFunc returns a resource.StateRefreshFunc that is used // IGAttachStateRefreshFunc returns a resource.StateRefreshFunc that is used
// watch the state of an internet gateway's attachment. // watch the state of an internet gateway's attachment.
func IGAttachStateRefreshFunc(conn *ec2.EC2, id string, expected string) resource.StateRefreshFunc { func IGAttachStateRefreshFunc(ec2conn *ec2.EC2, id string, expected string) resource.StateRefreshFunc {
var start time.Time var start time.Time
return func() (interface{}, string, error) { return func() (interface{}, string, error) {
if start.IsZero() { if start.IsZero() {
start = time.Now() start = time.Now()
} }
resp, err := conn.DescribeInternetGateways([]string{id}, ec2.NewFilter()) resp, err := ec2conn.DescribeInternetGateways(&ec2.DescribeInternetGatewaysRequest{
InternetGatewayIDs: []string{id},
})
if err != nil { if err != nil {
ec2err, ok := err.(*ec2.Error) ec2err, ok := err.(aws.APIError)
if ok && ec2err.Code == "InvalidInternetGatewayID.NotFound" { if ok && ec2err.Code == "InvalidInternetGatewayID.NotFound" {
resp = nil resp = nil
} else { } else {
@ -291,6 +304,6 @@ func IGAttachStateRefreshFunc(conn *ec2.EC2, id string, expected string) resourc
return ig, "detached", nil return ig, "detached", nil
} }
return ig, ig.Attachments[0].State, nil return ig, *ig.Attachments[0].State, nil
} }
} }

View File

@ -4,9 +4,10 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/ec2"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/goamz/ec2"
) )
func TestAccAWSInternetGateway(t *testing.T) { func TestAccAWSInternetGateway(t *testing.T) {
@ -20,8 +21,8 @@ func TestAccAWSInternetGateway(t *testing.T) {
return fmt.Errorf("IG B is not attached") return fmt.Errorf("IG B is not attached")
} }
id1 := v.Attachments[0].VpcId id1 := v.Attachments[0].VPCID
id2 := v2.Attachments[0].VpcId id2 := v2.Attachments[0].VPCID
if id1 == id2 { if id1 == id2 {
return fmt.Errorf("Both attachment IDs are the same") return fmt.Errorf("Both attachment IDs are the same")
} }
@ -104,8 +105,8 @@ func TestAccInternetGateway_tags(t *testing.T) {
Config: testAccCheckInternetGatewayConfigTagsUpdate, Config: testAccCheckInternetGatewayConfigTagsUpdate,
Check: resource.ComposeTestCheckFunc( Check: resource.ComposeTestCheckFunc(
testAccCheckInternetGatewayExists("aws_internet_gateway.foo", &v), testAccCheckInternetGatewayExists("aws_internet_gateway.foo", &v),
testAccCheckTags(&v.Tags, "foo", ""), testAccCheckTagsSDK(&v.Tags, "foo", ""),
testAccCheckTags(&v.Tags, "bar", "baz"), testAccCheckTagsSDK(&v.Tags, "bar", "baz"),
), ),
}, },
}, },
@ -113,7 +114,7 @@ func TestAccInternetGateway_tags(t *testing.T) {
} }
func testAccCheckInternetGatewayDestroy(s *terraform.State) error { func testAccCheckInternetGatewayDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn ec2conn := testAccProvider.Meta().(*AWSClient).awsEC2conn
for _, rs := range s.RootModule().Resources { for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_internet_gateway" { if rs.Type != "aws_internet_gateway" {
@ -121,8 +122,9 @@ func testAccCheckInternetGatewayDestroy(s *terraform.State) error {
} }
// Try to find the resource // Try to find the resource
resp, err := conn.DescribeInternetGateways( resp, err := ec2conn.DescribeInternetGateways(&ec2.DescribeInternetGatewaysRequest{
[]string{rs.Primary.ID}, ec2.NewFilter()) InternetGatewayIDs: []string{rs.Primary.ID},
})
if err == nil { if err == nil {
if len(resp.InternetGateways) > 0 { if len(resp.InternetGateways) > 0 {
return fmt.Errorf("still exists") return fmt.Errorf("still exists")
@ -132,7 +134,7 @@ func testAccCheckInternetGatewayDestroy(s *terraform.State) error {
} }
// Verify the error is what we want // Verify the error is what we want
ec2err, ok := err.(*ec2.Error) ec2err, ok := err.(aws.APIError)
if !ok { if !ok {
return err return err
} }
@ -155,9 +157,10 @@ func testAccCheckInternetGatewayExists(n string, ig *ec2.InternetGateway) resour
return fmt.Errorf("No ID is set") return fmt.Errorf("No ID is set")
} }
conn := testAccProvider.Meta().(*AWSClient).ec2conn ec2conn := testAccProvider.Meta().(*AWSClient).awsEC2conn
resp, err := conn.DescribeInternetGateways( resp, err := ec2conn.DescribeInternetGateways(&ec2.DescribeInternetGatewaysRequest{
[]string{rs.Primary.ID}, ec2.NewFilter()) InternetGatewayIDs: []string{rs.Primary.ID},
})
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,9 +1,13 @@
package aws package aws
import ( import (
"encoding/base64"
"fmt" "fmt"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/ec2"
) )
func resourceAwsKeyPair() *schema.Resource { func resourceAwsKeyPair() *schema.Resource {
@ -33,42 +37,50 @@ func resourceAwsKeyPair() *schema.Resource {
} }
func resourceAwsKeyPairCreate(d *schema.ResourceData, meta interface{}) error { func resourceAwsKeyPairCreate(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn ec2conn := meta.(*AWSClient).awsEC2conn
keyName := d.Get("key_name").(string) keyName := d.Get("key_name").(string)
publicKey := d.Get("public_key").(string) publicKey := d.Get("public_key").(string)
resp, err := ec2conn.ImportKeyPair(keyName, publicKey) req := &ec2.ImportKeyPairRequest{
KeyName: aws.String(keyName),
PublicKeyMaterial: []byte(base64.StdEncoding.EncodeToString([]byte(publicKey))),
}
resp, err := ec2conn.ImportKeyPair(req)
if err != nil { if err != nil {
return fmt.Errorf("Error import KeyPair: %s", err) return fmt.Errorf("Error import KeyPair: %s", err)
} }
d.SetId(resp.KeyName) d.SetId(*resp.KeyName)
return nil return nil
} }
func resourceAwsKeyPairRead(d *schema.ResourceData, meta interface{}) error { func resourceAwsKeyPairRead(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn ec2conn := meta.(*AWSClient).awsEC2conn
resp, err := ec2conn.KeyPairs([]string{d.Id()}, nil) req := &ec2.DescribeKeyPairsRequest{
KeyNames: []string{d.Id()},
}
resp, err := ec2conn.DescribeKeyPairs(req)
if err != nil { if err != nil {
return fmt.Errorf("Error retrieving KeyPair: %s", err) return fmt.Errorf("Error retrieving KeyPair: %s", err)
} }
for _, keyPair := range resp.Keys { for _, keyPair := range resp.KeyPairs {
if keyPair.Name == d.Id() { if *keyPair.KeyName == d.Id() {
d.Set("key_name", keyPair.Name) d.Set("key_name", keyPair.KeyName)
d.Set("fingerprint", keyPair.Fingerprint) d.Set("fingerprint", keyPair.KeyFingerprint)
return nil return nil
} }
} }
return fmt.Errorf("Unable to find key pair within: %#v", resp.Keys) return fmt.Errorf("Unable to find key pair within: %#v", resp.KeyPairs)
} }
func resourceAwsKeyPairDelete(d *schema.ResourceData, meta interface{}) error { func resourceAwsKeyPairDelete(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn ec2conn := meta.(*AWSClient).awsEC2conn
_, err := ec2conn.DeleteKeyPair(d.Id()) err := ec2conn.DeleteKeyPair(&ec2.DeleteKeyPairRequest{
KeyName: aws.String(d.Id()),
})
return err return err
} }

View File

@ -4,13 +4,14 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/ec2"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/goamz/ec2"
) )
func TestAccAWSKeyPair_normal(t *testing.T) { func TestAccAWSKeyPair_normal(t *testing.T) {
var conf ec2.KeyPair var conf ec2.KeyPairInfo
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
@ -29,7 +30,7 @@ func TestAccAWSKeyPair_normal(t *testing.T) {
} }
func testAccCheckAWSKeyPairDestroy(s *terraform.State) error { func testAccCheckAWSKeyPairDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn ec2conn := testAccProvider.Meta().(*AWSClient).awsEC2conn
for _, rs := range s.RootModule().Resources { for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_key_pair" { if rs.Type != "aws_key_pair" {
@ -37,17 +38,18 @@ func testAccCheckAWSKeyPairDestroy(s *terraform.State) error {
} }
// Try to find key pair // Try to find key pair
resp, err := conn.KeyPairs( resp, err := ec2conn.DescribeKeyPairs(&ec2.DescribeKeyPairsRequest{
[]string{rs.Primary.ID}, nil) KeyNames: []string{rs.Primary.ID},
})
if err == nil { if err == nil {
if len(resp.Keys) > 0 { if len(resp.KeyPairs) > 0 {
return fmt.Errorf("still exist.") return fmt.Errorf("still exist.")
} }
return nil return nil
} }
// Verify the error is what we want // Verify the error is what we want
ec2err, ok := err.(*ec2.Error) ec2err, ok := err.(aws.APIError)
if !ok { if !ok {
return err return err
} }
@ -59,16 +61,16 @@ func testAccCheckAWSKeyPairDestroy(s *terraform.State) error {
return nil return nil
} }
func testAccCheckAWSKeyPairFingerprint(expectedFingerprint string, conf *ec2.KeyPair) resource.TestCheckFunc { func testAccCheckAWSKeyPairFingerprint(expectedFingerprint string, conf *ec2.KeyPairInfo) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
if conf.Fingerprint != expectedFingerprint { if *conf.KeyFingerprint != expectedFingerprint {
return fmt.Errorf("incorrect fingerprint. expected %s, got %s", expectedFingerprint, conf.Fingerprint) return fmt.Errorf("incorrect fingerprint. expected %s, got %s", expectedFingerprint, *conf.KeyFingerprint)
} }
return nil return nil
} }
} }
func testAccCheckAWSKeyPairExists(n string, res *ec2.KeyPair) resource.TestCheckFunc { func testAccCheckAWSKeyPairExists(n string, res *ec2.KeyPairInfo) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n] rs, ok := s.RootModule().Resources[n]
if !ok { if !ok {
@ -79,18 +81,20 @@ func testAccCheckAWSKeyPairExists(n string, res *ec2.KeyPair) resource.TestCheck
return fmt.Errorf("No KeyPair name is set") return fmt.Errorf("No KeyPair name is set")
} }
conn := testAccProvider.Meta().(*AWSClient).ec2conn ec2conn := testAccProvider.Meta().(*AWSClient).awsEC2conn
resp, err := conn.KeyPairs( resp, err := ec2conn.DescribeKeyPairs(&ec2.DescribeKeyPairsRequest{
[]string{rs.Primary.ID}, nil) KeyNames: []string{rs.Primary.ID},
})
if err != nil { if err != nil {
return err return err
} }
if len(resp.Keys) != 1 || if len(resp.KeyPairs) != 1 ||
resp.Keys[0].Name != rs.Primary.ID { *resp.KeyPairs[0].KeyName != rs.Primary.ID {
return fmt.Errorf("KeyPair not found") return fmt.Errorf("KeyPair not found")
} }
*res = resp.Keys[0]
*res = resp.KeyPairs[0]
return nil return nil
} }

View File

@ -5,9 +5,10 @@ import (
"log" "log"
"time" "time"
"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/ec2"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"github.com/mitchellh/goamz/ec2"
) )
func resourceAwsSubnet() *schema.Resource { func resourceAwsSubnet() *schema.Resource {
@ -50,12 +51,12 @@ func resourceAwsSubnet() *schema.Resource {
} }
func resourceAwsSubnetCreate(d *schema.ResourceData, meta interface{}) error { func resourceAwsSubnetCreate(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn ec2conn := meta.(*AWSClient).awsEC2conn
createOpts := &ec2.CreateSubnet{ createOpts := &ec2.CreateSubnetRequest{
AvailabilityZone: d.Get("availability_zone").(string), AvailabilityZone: aws.String(d.Get("availability_zone").(string)),
CidrBlock: d.Get("cidr_block").(string), CIDRBlock: aws.String(d.Get("cidr_block").(string)),
VpcId: d.Get("vpc_id").(string), VPCID: aws.String(d.Get("vpc_id").(string)),
} }
resp, err := ec2conn.CreateSubnet(createOpts) resp, err := ec2conn.CreateSubnet(createOpts)
@ -65,16 +66,16 @@ func resourceAwsSubnetCreate(d *schema.ResourceData, meta interface{}) error {
} }
// Get the ID and store it // Get the ID and store it
subnet := &resp.Subnet subnet := resp.Subnet
d.SetId(subnet.SubnetId) d.SetId(*subnet.SubnetID)
log.Printf("[INFO] Subnet ID: %s", subnet.SubnetId) log.Printf("[INFO] Subnet ID: %s", *subnet.SubnetID)
// Wait for the Subnet to become available // Wait for the Subnet to become available
log.Printf("[DEBUG] Waiting for subnet (%s) to become available", subnet.SubnetId) log.Printf("[DEBUG] Waiting for subnet (%s) to become available", *subnet.SubnetID)
stateConf := &resource.StateChangeConf{ stateConf := &resource.StateChangeConf{
Pending: []string{"pending"}, Pending: []string{"pending"},
Target: "available", Target: "available",
Refresh: SubnetStateRefreshFunc(ec2conn, subnet.SubnetId), Refresh: SubnetStateRefreshFunc(ec2conn, *subnet.SubnetID),
Timeout: 10 * time.Minute, Timeout: 10 * time.Minute,
} }
@ -90,12 +91,14 @@ func resourceAwsSubnetCreate(d *schema.ResourceData, meta interface{}) error {
} }
func resourceAwsSubnetRead(d *schema.ResourceData, meta interface{}) error { func resourceAwsSubnetRead(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn ec2conn := meta.(*AWSClient).awsEC2conn
resp, err := ec2conn.DescribeSubnets([]string{d.Id()}, ec2.NewFilter()) resp, err := ec2conn.DescribeSubnets(&ec2.DescribeSubnetsRequest{
SubnetIDs: []string{d.Id()},
})
if err != nil { if err != nil {
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidSubnetID.NotFound" { if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidSubnetID.NotFound" {
// Update state to indicate the subnet no longer exists. // Update state to indicate the subnet no longer exists.
d.SetId("") d.SetId("")
return nil return nil
@ -108,35 +111,35 @@ func resourceAwsSubnetRead(d *schema.ResourceData, meta interface{}) error {
subnet := &resp.Subnets[0] subnet := &resp.Subnets[0]
d.Set("vpc_id", subnet.VpcId) d.Set("vpc_id", subnet.VPCID)
d.Set("availability_zone", subnet.AvailabilityZone) d.Set("availability_zone", subnet.AvailabilityZone)
d.Set("cidr_block", subnet.CidrBlock) d.Set("cidr_block", subnet.CIDRBlock)
d.Set("map_public_ip_on_launch", subnet.MapPublicIpOnLaunch) d.Set("map_public_ip_on_launch", subnet.MapPublicIPOnLaunch)
d.Set("tags", tagsToMap(subnet.Tags)) d.Set("tags", tagsToMapSDK(subnet.Tags))
return nil return nil
} }
func resourceAwsSubnetUpdate(d *schema.ResourceData, meta interface{}) error { func resourceAwsSubnetUpdate(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn ec2conn := meta.(*AWSClient).awsEC2conn
d.Partial(true) d.Partial(true)
if err := setTags(ec2conn, d); err != nil { if err := setTagsSDK(ec2conn, d); err != nil {
return err return err
} else { } else {
d.SetPartial("tags") d.SetPartial("tags")
} }
if d.HasChange("map_public_ip_on_launch") { if d.HasChange("map_public_ip_on_launch") {
modifyOpts := &ec2.ModifySubnetAttribute{ modifyOpts := &ec2.ModifySubnetAttributeRequest{
SubnetId: d.Id(), SubnetID: aws.String(d.Id()),
MapPublicIpOnLaunch: true, MapPublicIPOnLaunch: &ec2.AttributeBooleanValue{aws.Boolean(true)},
} }
log.Printf("[DEBUG] Subnet modify attributes: %#v", modifyOpts) log.Printf("[DEBUG] Subnet modify attributes: %#v", modifyOpts)
_, err := ec2conn.ModifySubnetAttribute(modifyOpts) err := ec2conn.ModifySubnetAttribute(modifyOpts)
if err != nil { if err != nil {
return err return err
@ -151,11 +154,16 @@ func resourceAwsSubnetUpdate(d *schema.ResourceData, meta interface{}) error {
} }
func resourceAwsSubnetDelete(d *schema.ResourceData, meta interface{}) error { func resourceAwsSubnetDelete(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn ec2conn := meta.(*AWSClient).awsEC2conn
log.Printf("[INFO] Deleting subnet: %s", d.Id()) log.Printf("[INFO] Deleting subnet: %s", d.Id())
if _, err := ec2conn.DeleteSubnet(d.Id()); err != nil {
ec2err, ok := err.(*ec2.Error) err := ec2conn.DeleteSubnet(&ec2.DeleteSubnetRequest{
SubnetID: aws.String(d.Id()),
})
if err != nil {
ec2err, ok := err.(aws.APIError)
if ok && ec2err.Code == "InvalidSubnetID.NotFound" { if ok && ec2err.Code == "InvalidSubnetID.NotFound" {
return nil return nil
} }
@ -169,9 +177,11 @@ func resourceAwsSubnetDelete(d *schema.ResourceData, meta interface{}) error {
// SubnetStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch a Subnet. // SubnetStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch a Subnet.
func SubnetStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc { func SubnetStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc {
return func() (interface{}, string, error) { return func() (interface{}, string, error) {
resp, err := conn.DescribeSubnets([]string{id}, ec2.NewFilter()) resp, err := conn.DescribeSubnets(&ec2.DescribeSubnetsRequest{
SubnetIDs: []string{id},
})
if err != nil { if err != nil {
if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidSubnetID.NotFound" { if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidSubnetID.NotFound" {
resp = nil resp = nil
} else { } else {
log.Printf("Error on SubnetStateRefresh: %s", err) log.Printf("Error on SubnetStateRefresh: %s", err)
@ -186,6 +196,6 @@ func SubnetStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc
} }
subnet := &resp.Subnets[0] subnet := &resp.Subnets[0]
return subnet, subnet.State, nil return subnet, *subnet.State, nil
} }
} }

View File

@ -4,21 +4,22 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/ec2"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/goamz/ec2"
) )
func TestAccAWSSubnet(t *testing.T) { func TestAccAWSSubnet(t *testing.T) {
var v ec2.Subnet var v ec2.Subnet
testCheck := func(*terraform.State) error { testCheck := func(*terraform.State) error {
if v.CidrBlock != "10.1.1.0/24" { if *v.CIDRBlock != "10.1.1.0/24" {
return fmt.Errorf("bad cidr: %s", v.CidrBlock) return fmt.Errorf("bad cidr: %s", *v.CIDRBlock)
} }
if v.MapPublicIpOnLaunch != true { if *v.MapPublicIPOnLaunch != true {
return fmt.Errorf("bad MapPublicIpOnLaunch: %t", v.MapPublicIpOnLaunch) return fmt.Errorf("bad MapPublicIpOnLaunch: %t", *v.MapPublicIPOnLaunch)
} }
return nil return nil
@ -42,7 +43,7 @@ func TestAccAWSSubnet(t *testing.T) {
} }
func testAccCheckSubnetDestroy(s *terraform.State) error { func testAccCheckSubnetDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn conn := testAccProvider.Meta().(*AWSClient).awsEC2conn
for _, rs := range s.RootModule().Resources { for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_subnet" { if rs.Type != "aws_subnet" {
@ -50,8 +51,9 @@ func testAccCheckSubnetDestroy(s *terraform.State) error {
} }
// Try to find the resource // Try to find the resource
resp, err := conn.DescribeSubnets( resp, err := conn.DescribeSubnets(&ec2.DescribeSubnetsRequest{
[]string{rs.Primary.ID}, ec2.NewFilter()) SubnetIDs: []string{rs.Primary.ID},
})
if err == nil { if err == nil {
if len(resp.Subnets) > 0 { if len(resp.Subnets) > 0 {
return fmt.Errorf("still exist.") return fmt.Errorf("still exist.")
@ -61,7 +63,7 @@ func testAccCheckSubnetDestroy(s *terraform.State) error {
} }
// Verify the error is what we want // Verify the error is what we want
ec2err, ok := err.(*ec2.Error) ec2err, ok := err.(aws.APIError)
if !ok { if !ok {
return err return err
} }
@ -84,9 +86,10 @@ func testAccCheckSubnetExists(n string, v *ec2.Subnet) resource.TestCheckFunc {
return fmt.Errorf("No ID is set") return fmt.Errorf("No ID is set")
} }
conn := testAccProvider.Meta().(*AWSClient).ec2conn conn := testAccProvider.Meta().(*AWSClient).awsEC2conn
resp, err := conn.DescribeSubnets( resp, err := conn.DescribeSubnets(&ec2.DescribeSubnetsRequest{
[]string{rs.Primary.ID}, ec2.NewFilter()) SubnetIDs: []string{rs.Primary.ID},
})
if err != nil { if err != nil {
return err return err
} }

View File

@ -0,0 +1,106 @@
package aws
// TODO: Clint: consolidate tags and tags_sdk
// tags_sdk and tags_sdk_test are used only for transition to aws-sdk-go
// and will replace tags and tags_test when the transition to aws-sdk-go/ec2 is
// complete
import (
"log"
"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/ec2"
"github.com/hashicorp/terraform/helper/schema"
)
// tagsSchema returns the schema to use for tags.
//
// TODO: uncomment this when we replace the original tags.go
//
// func tagsSchema() *schema.Schema {
// return &schema.Schema{
// Type: schema.TypeMap,
// Optional: true,
// }
// }
// setTags is a helper to set the tags for a resource. It expects the
// tags field to be named "tags"
func setTagsSDK(conn *ec2.EC2, d *schema.ResourceData) error {
if d.HasChange("tags") {
oraw, nraw := d.GetChange("tags")
o := oraw.(map[string]interface{})
n := nraw.(map[string]interface{})
create, remove := diffTagsSDK(tagsFromMapSDK(o), tagsFromMapSDK(n))
// Set tags
if len(remove) > 0 {
log.Printf("[DEBUG] Removing tags: %#v", remove)
err := conn.DeleteTags(&ec2.DeleteTagsRequest{
Resources: []string{d.Id()},
Tags: remove,
})
if err != nil {
return err
}
}
if len(create) > 0 {
log.Printf("[DEBUG] Creating tags: %#v", create)
err := conn.CreateTags(&ec2.CreateTagsRequest{
Resources: []string{d.Id()},
Tags: create,
})
if err != nil {
return err
}
}
}
return nil
}
// diffTags takes our tags locally and the ones remotely and returns
// the set of tags that must be created, and the set of tags that must
// be destroyed.
func diffTagsSDK(oldTags, newTags []ec2.Tag) ([]ec2.Tag, []ec2.Tag) {
// First, we're creating everything we have
create := make(map[string]interface{})
for _, t := range newTags {
create[*t.Key] = *t.Value
}
// Build the list of what to remove
var remove []ec2.Tag
for _, t := range oldTags {
old, ok := create[*t.Key]
if !ok || old != *t.Value {
// Delete it!
remove = append(remove, t)
}
}
return tagsFromMapSDK(create), remove
}
// tagsFromMap returns the tags for the given map of data.
func tagsFromMapSDK(m map[string]interface{}) []ec2.Tag {
result := make([]ec2.Tag, 0, len(m))
for k, v := range m {
result = append(result, ec2.Tag{
Key: aws.String(k),
Value: aws.String(v.(string)),
})
}
return result
}
// tagsToMap turns the list of tags into a map.
func tagsToMapSDK(ts []ec2.Tag) map[string]string {
result := make(map[string]string)
for _, t := range ts {
result[*t.Key] = *t.Value
}
return result
}

View File

@ -0,0 +1,85 @@
package aws
import (
"fmt"
"reflect"
"testing"
"github.com/hashicorp/aws-sdk-go/gen/ec2"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestDiffTagsSDK(t *testing.T) {
cases := []struct {
Old, New map[string]interface{}
Create, Remove map[string]string
}{
// Basic add/remove
{
Old: map[string]interface{}{
"foo": "bar",
},
New: map[string]interface{}{
"bar": "baz",
},
Create: map[string]string{
"bar": "baz",
},
Remove: map[string]string{
"foo": "bar",
},
},
// Modify
{
Old: map[string]interface{}{
"foo": "bar",
},
New: map[string]interface{}{
"foo": "baz",
},
Create: map[string]string{
"foo": "baz",
},
Remove: map[string]string{
"foo": "bar",
},
},
}
for i, tc := range cases {
c, r := diffTagsSDK(tagsFromMapSDK(tc.Old), tagsFromMapSDK(tc.New))
cm := tagsToMapSDK(c)
rm := tagsToMapSDK(r)
if !reflect.DeepEqual(cm, tc.Create) {
t.Fatalf("%d: bad create: %#v", i, cm)
}
if !reflect.DeepEqual(rm, tc.Remove) {
t.Fatalf("%d: bad remove: %#v", i, rm)
}
}
}
// testAccCheckTags can be used to check the tags on a resource.
func testAccCheckTagsSDK(
ts *[]ec2.Tag, key string, value string) resource.TestCheckFunc {
return func(s *terraform.State) error {
m := tagsToMapSDK(*ts)
v, ok := m[key]
if value != "" && !ok {
return fmt.Errorf("Missing tag: %s", key)
} else if value == "" && ok {
return fmt.Errorf("Extra tag: %s", key)
}
if value == "" {
return nil
}
if v != value {
return fmt.Errorf("%s: bad value: %s", key, v)
}
return nil
}
}

View File

@ -455,12 +455,27 @@ func resourceCloudStackNetworkACLRuleDeleteRule(
func resourceCloudStackNetworkACLRuleHash(v interface{}) int { func resourceCloudStackNetworkACLRuleHash(v interface{}) int {
var buf bytes.Buffer var buf bytes.Buffer
m := v.(map[string]interface{}) m := v.(map[string]interface{})
// This is a little ugly, but it's needed because these arguments have
// a default value that needs to be part of the string to hash
var action, trafficType string
if a, ok := m["action"]; ok {
action = a.(string)
} else {
action = "allow"
}
if t, ok := m["traffic_type"]; ok {
trafficType = t.(string)
} else {
trafficType = "ingress"
}
buf.WriteString(fmt.Sprintf( buf.WriteString(fmt.Sprintf(
"%s-%s-%s-%s-", "%s-%s-%s-%s-",
m["action"].(string), action,
m["source_cidr"].(string), m["source_cidr"].(string),
m["protocol"].(string), m["protocol"].(string),
m["traffic_type"].(string))) trafficType))
if v, ok := m["icmp_type"]; ok { if v, ok := m["icmp_type"]; ok {
buf.WriteString(fmt.Sprintf("%d-", v.(int))) buf.WriteString(fmt.Sprintf("%d-", v.(int)))

View File

@ -190,7 +190,6 @@ resource "cloudstack_network_acl_rule" "foo" {
aclid = "${cloudstack_network_acl.foo.id}" aclid = "${cloudstack_network_acl.foo.id}"
rule { rule {
action = "allow"
source_cidr = "172.16.100.0/24" source_cidr = "172.16.100.0/24"
protocol = "tcp" protocol = "tcp"
ports = ["80", "443"] ports = ["80", "443"]

View File

@ -26,6 +26,12 @@ func resourceCloudStackPortForward() *schema.Resource {
ForceNew: true, ForceNew: true,
}, },
"managed": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"forward": &schema.Schema{ "forward": &schema.Schema{
Type: schema.TypeSet, Type: schema.TypeSet,
Required: true, Required: true,
@ -73,7 +79,7 @@ func resourceCloudStackPortForwardCreate(d *schema.ResourceData, meta interface{
} }
// We need to set this upfront in order to be able to save a partial state // We need to set this upfront in order to be able to save a partial state
d.SetId(d.Get("ipaddress").(string)) d.SetId(ipaddressid)
// Create all forwards that are configured // Create all forwards that are configured
if rs := d.Get("forward").(*schema.Set); rs.Len() > 0 { if rs := d.Get("forward").(*schema.Set); rs.Len() > 0 {
@ -85,7 +91,7 @@ func resourceCloudStackPortForwardCreate(d *schema.ResourceData, meta interface{
for _, forward := range rs.List() { for _, forward := range rs.List() {
// Create a single forward // Create a single forward
err := resourceCloudStackPortForwardCreateForward(d, meta, ipaddressid, forward.(map[string]interface{})) err := resourceCloudStackPortForwardCreateForward(d, meta, forward.(map[string]interface{}))
// We need to update this first to preserve the correct state // We need to update this first to preserve the correct state
forwards.Add(forward) forwards.Add(forward)
@ -101,7 +107,7 @@ func resourceCloudStackPortForwardCreate(d *schema.ResourceData, meta interface{
} }
func resourceCloudStackPortForwardCreateForward( func resourceCloudStackPortForwardCreateForward(
d *schema.ResourceData, meta interface{}, ipaddressid string, forward map[string]interface{}) error { d *schema.ResourceData, meta interface{}, forward map[string]interface{}) error {
cs := meta.(*cloudstack.CloudStackClient) cs := meta.(*cloudstack.CloudStackClient)
// Make sure all required parameters are there // Make sure all required parameters are there
@ -110,14 +116,18 @@ func resourceCloudStackPortForwardCreateForward(
} }
// Retrieve the virtual_machine UUID // Retrieve the virtual_machine UUID
virtualmachineid, e := retrieveUUID(cs, "virtual_machine", forward["virtual_machine"].(string)) vm, _, err := cs.VirtualMachine.GetVirtualMachineByName(forward["virtual_machine"].(string))
if e != nil { if err != nil {
return e.Error() return err
} }
// Create a new parameter struct // Create a new parameter struct
p := cs.Firewall.NewCreatePortForwardingRuleParams(ipaddressid, forward["private_port"].(int), p := cs.Firewall.NewCreatePortForwardingRuleParams(d.Id(), forward["private_port"].(int),
forward["protocol"].(string), forward["public_port"].(int), virtualmachineid) forward["protocol"].(string), forward["public_port"].(int), vm.Id)
// Set the network ID of the default network, needed when public IP address
// is not associated with any Guest network yet (VPC case)
p.SetNetworkid(vm.Nic[0].Networkid)
// Do not open the firewall automatically in any case // Do not open the firewall automatically in any case
p.SetOpenfirewall(false) p.SetOpenfirewall(false)
@ -154,7 +164,8 @@ func resourceCloudStackPortForwardRead(d *schema.ResourceData, meta interface{})
r, count, err := cs.Firewall.GetPortForwardingRuleByID(id.(string)) r, count, err := cs.Firewall.GetPortForwardingRuleByID(id.(string))
// If the count == 0, there is no object found for this UUID // If the count == 0, there is no object found for this UUID
if err != nil { if err != nil {
if count != 0 { if count == 0 {
forward["uuid"] = ""
continue continue
} }
@ -180,9 +191,52 @@ func resourceCloudStackPortForwardRead(d *schema.ResourceData, meta interface{})
} }
} }
// If this is a managed resource, add all unknown forwards to dummy forwards
managed := d.Get("managed").(bool)
if managed {
// Get all the forwards from the running environment
p := cs.Firewall.NewListPortForwardingRulesParams()
p.SetIpaddressid(d.Id())
p.SetListall(true)
r, err := cs.Firewall.ListPortForwardingRules(p)
if err != nil {
return err
}
// Add all UUIDs to the uuids map
uuids := make(map[string]interface{}, len(r.PortForwardingRules))
for _, r := range r.PortForwardingRules {
uuids[r.Id] = r.Id
}
// Delete all expected UUIDs from the uuids map
for _, forward := range forwards.List() {
forward := forward.(map[string]interface{})
for _, id := range forward["uuids"].(map[string]interface{}) {
delete(uuids, id.(string))
}
}
for uuid, _ := range uuids {
// Make a dummy forward to hold the unknown UUID
forward := map[string]interface{}{
"protocol": "N/A",
"private_port": 0,
"public_port": 0,
"virtual_machine": uuid,
"uuid": uuid,
}
// Add the dummy forward to the forwards set
forwards.Add(forward)
}
}
if forwards.Len() > 0 { if forwards.Len() > 0 {
d.Set("forward", forwards) d.Set("forward", forwards)
} else { } else if !managed {
d.SetId("") d.SetId("")
} }
@ -190,14 +244,6 @@ func resourceCloudStackPortForwardRead(d *schema.ResourceData, meta interface{})
} }
func resourceCloudStackPortForwardUpdate(d *schema.ResourceData, meta interface{}) error { func resourceCloudStackPortForwardUpdate(d *schema.ResourceData, meta interface{}) error {
cs := meta.(*cloudstack.CloudStackClient)
// Retrieve the ipaddress UUID
ipaddressid, e := retrieveUUID(cs, "ipaddress", d.Get("ipaddress").(string))
if e != nil {
return e.Error()
}
// Check if the forward set as a whole has changed // Check if the forward set as a whole has changed
if d.HasChange("forward") { if d.HasChange("forward") {
o, n := d.GetChange("forward") o, n := d.GetChange("forward")
@ -220,7 +266,7 @@ func resourceCloudStackPortForwardUpdate(d *schema.ResourceData, meta interface{
// Then loop through al the currently configured forwards and create the new ones // Then loop through al the currently configured forwards and create the new ones
for _, forward := range nrs.List() { for _, forward := range nrs.List() {
err := resourceCloudStackPortForwardCreateForward( err := resourceCloudStackPortForwardCreateForward(
d, meta, ipaddressid, forward.(map[string]interface{})) d, meta, forward.(map[string]interface{}))
// We need to update this first to preserve the correct state // We need to update this first to preserve the correct state
forwards.Add(forward) forwards.Add(forward)

View File

@ -483,14 +483,19 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error
d.Set("can_ip_forward", instance.CanIpForward) d.Set("can_ip_forward", instance.CanIpForward)
// Set the service accounts // Set the service accounts
for i, serviceAccount := range instance.ServiceAccounts { serviceAccounts := make([]map[string]interface{}, 0, 1)
prefix := fmt.Sprintf("service_account.%d", i) for _, serviceAccount := range instance.ServiceAccounts {
d.Set(prefix+".email", serviceAccount.Email) scopes := make([]string, len(serviceAccount.Scopes))
d.Set(prefix+".scopes.#", len(serviceAccount.Scopes)) for i, scope := range serviceAccount.Scopes {
for j, scope := range serviceAccount.Scopes { scopes[i] = scope
d.Set(fmt.Sprintf("%s.scopes.%d", prefix, j), scope)
} }
serviceAccounts = append(serviceAccounts, map[string]interface{}{
"email": serviceAccount.Email,
"scopes": scopes,
})
} }
d.Set("service_account", serviceAccounts)
networksCount := d.Get("network.#").(int) networksCount := d.Get("network.#").(int)
networkInterfacesCount := d.Get("network_interface.#").(int) networkInterfacesCount := d.Get("network_interface.#").(int)
@ -506,13 +511,10 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error
// Use the first external IP found for the default connection info. // Use the first external IP found for the default connection info.
externalIP := "" externalIP := ""
internalIP := "" internalIP := ""
networks := make([]map[string]interface{}, 0, 1)
if networksCount > 0 { if networksCount > 0 {
// TODO: Remove this when realizing deprecation of .network // TODO: Remove this when realizing deprecation of .network
for i, iface := range instance.NetworkInterfaces { for _, iface := range instance.NetworkInterfaces {
prefix := fmt.Sprintf("network.%d", i)
d.Set(prefix+".name", iface.Name)
log.Printf(prefix+".name = %s", iface.Name)
var natIP string var natIP string
for _, config := range iface.AccessConfigs { for _, config := range iface.AccessConfigs {
if config.Type == "ONE_TO_ONE_NAT" { if config.Type == "ONE_TO_ONE_NAT" {
@ -524,23 +526,28 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error
if externalIP == "" && natIP != "" { if externalIP == "" && natIP != "" {
externalIP = natIP externalIP = natIP
} }
d.Set(prefix+".external_address", natIP)
d.Set(prefix+".internal_address", iface.NetworkIP) network := make(map[string]interface{})
network["name"] = iface.Name
network["external_address"] = natIP
network["internal_address"] = iface.NetworkIP
networks = append(networks, network)
} }
} }
d.Set("network", networks)
networkInterfaces := make([]map[string]interface{}, 0, 1)
if networkInterfacesCount > 0 { if networkInterfacesCount > 0 {
for i, iface := range instance.NetworkInterfaces { for _, iface := range instance.NetworkInterfaces {
prefix := fmt.Sprintf("network_interface.%d", i)
d.Set(prefix+".name", iface.Name)
// The first non-empty ip is left in natIP // The first non-empty ip is left in natIP
var natIP string var natIP string
for j, config := range iface.AccessConfigs { accessConfigs := make(
acPrefix := fmt.Sprintf("%s.access_config.%d", prefix, j) []map[string]interface{}, 0, len(iface.AccessConfigs))
d.Set(acPrefix+".nat_ip", config.NatIP) for _, config := range iface.AccessConfigs {
accessConfigs = append(accessConfigs, map[string]interface{}{
"nat_ip": config.NatIP,
})
if natIP == "" { if natIP == "" {
natIP = config.NatIP natIP = config.NatIP
} }
@ -550,13 +557,18 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error
externalIP = natIP externalIP = natIP
} }
d.Set(prefix+".address", iface.NetworkIP)
if internalIP == "" { if internalIP == "" {
internalIP = iface.NetworkIP internalIP = iface.NetworkIP
} }
networkInterfaces = append(networkInterfaces, map[string]interface{}{
"name": iface.Name,
"address": iface.NetworkIP,
"access_config": accessConfigs,
})
} }
} }
d.Set("network_interface", networkInterfaces)
// Fall back on internal ip if there is no external ip. This makes sense in the situation where // Fall back on internal ip if there is no external ip. This makes sense in the situation where
// terraform is being used on a cloud instance and can therefore access the instances it creates // terraform is being used on a cloud instance and can therefore access the instances it creates

View File

@ -14,6 +14,7 @@ type ColorizeUi struct {
OutputColor string OutputColor string
InfoColor string InfoColor string
ErrorColor string ErrorColor string
WarnColor string
Ui cli.Ui Ui cli.Ui
} }
@ -33,6 +34,10 @@ func (u *ColorizeUi) Error(message string) {
u.Ui.Error(u.colorize(message, u.ErrorColor)) u.Ui.Error(u.colorize(message, u.ErrorColor))
} }
func (u *ColorizeUi) Warn(message string) {
u.Ui.Warn(u.colorize(message, u.WarnColor))
}
func (u *ColorizeUi) colorize(message string, color string) string { func (u *ColorizeUi) colorize(message string, color string) string {
if color == "" { if color == "" {
return message return message

View File

@ -33,9 +33,9 @@ func validateContext(ctx *terraform.Context, ui cli.Ui) bool {
"fix these before continuing.\n") "fix these before continuing.\n")
if len(ws) > 0 { if len(ws) > 0 {
ui.Output("Warnings:\n") ui.Warn("Warnings:\n")
for _, w := range ws { for _, w := range ws {
ui.Output(fmt.Sprintf(" * %s", w)) ui.Warn(fmt.Sprintf(" * %s", w))
} }
if len(es) > 0 { if len(es) > 0 {
@ -44,13 +44,16 @@ func validateContext(ctx *terraform.Context, ui cli.Ui) bool {
} }
if len(es) > 0 { if len(es) > 0 {
ui.Output("Errors:\n") ui.Error("Errors:\n")
for _, e := range es { for _, e := range es {
ui.Output(fmt.Sprintf(" * %s", e)) ui.Error(fmt.Sprintf(" * %s", e))
} }
}
return false return false
} else {
ui.Warn(fmt.Sprintf("\n"+
"No errors found. Continuing with %d warning(s).\n", len(ws)))
return true
}
} }
return true return true

View File

@ -120,7 +120,7 @@ func (c *InitCommand) Run(args []string) int {
} }
// Initialize a blank state file with remote enabled // Initialize a blank state file with remote enabled
remoteCmd := &RemoteCommand{ remoteCmd := &RemoteConfigCommand{
Meta: c.Meta, Meta: c.Meta,
remoteConf: remoteConf, remoteConf: remoteConf,
} }

View File

@ -331,6 +331,7 @@ func (m *Meta) process(args []string, vars bool) []string {
Ui: &ColorizeUi{ Ui: &ColorizeUi{
Colorize: m.Colorize(), Colorize: m.Colorize(),
ErrorColor: "[red]", ErrorColor: "[red]",
WarnColor: "[yellow]",
Ui: m.oldUi, Ui: m.oldUi,
}, },
} }

View File

@ -1,339 +1,57 @@
package command package command
import ( import (
"flag"
"fmt"
"log"
"os"
"strings" "strings"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
) )
// remoteCommandConfig is used to encapsulate our configuration
type remoteCommandConfig struct {
disableRemote bool
pullOnDisable bool
statePath string
backupPath string
}
// RemoteCommand is a Command implementation that is used to
// enable and disable remote state management
type RemoteCommand struct { type RemoteCommand struct {
Meta Meta
conf remoteCommandConfig
remoteConf terraform.RemoteState
} }
func (c *RemoteCommand) Run(args []string) int { func (c *RemoteCommand) Run(argsRaw []string) int {
// Duplicate the args so we can munge them without affecting
// future subcommand invocations which will do the same.
args := make([]string, len(argsRaw))
copy(args, argsRaw)
args = c.Meta.process(args, false) args = c.Meta.process(args, false)
config := make(map[string]string)
cmdFlags := flag.NewFlagSet("remote", flag.ContinueOnError) if len(args) == 0 {
cmdFlags.BoolVar(&c.conf.disableRemote, "disable", false, "") c.Ui.Error(c.Help())
cmdFlags.BoolVar(&c.conf.pullOnDisable, "pull", true, "")
cmdFlags.StringVar(&c.conf.statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&c.conf.backupPath, "backup", "", "path")
cmdFlags.StringVar(&c.remoteConf.Type, "backend", "atlas", "")
cmdFlags.Var((*FlagKV)(&config), "backend-config", "config")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1 return 1
} }
// Show help if given no inputs switch args[0] {
if !c.conf.disableRemote && c.remoteConf.Type == "atlas" && len(config) == 0 { case "config":
cmdFlags.Usage() cmd := &RemoteConfigCommand{Meta: c.Meta}
return cmd.Run(args[1:])
case "pull":
cmd := &RemotePullCommand{Meta: c.Meta}
return cmd.Run(args[1:])
case "push":
cmd := &RemotePushCommand{Meta: c.Meta}
return cmd.Run(args[1:])
default:
c.Ui.Error(c.Help())
return 1 return 1
} }
// Set the local state path
c.statePath = c.conf.statePath
// Populate the various configurations
c.remoteConf.Config = config
// Get the state information. We specifically request the cache only
// for the remote state here because it is possible the remote state
// is invalid and we don't want to error.
stateOpts := c.StateOpts()
stateOpts.RemoteCacheOnly = true
if _, err := c.StateRaw(stateOpts); err != nil {
c.Ui.Error(fmt.Sprintf("Error loading local state: %s", err))
return 1
}
// Get the local and remote [cached] state
localState := c.stateResult.Local.State()
var remoteState *terraform.State
if remote := c.stateResult.Remote; remote != nil {
remoteState = remote.State()
}
// Check if remote state is being disabled
if c.conf.disableRemote {
if !remoteState.IsRemote() {
c.Ui.Error(fmt.Sprintf("Remote state management not enabled! Aborting."))
return 1
}
if !localState.Empty() {
c.Ui.Error(fmt.Sprintf("State file already exists at '%s'. Aborting.",
c.conf.statePath))
return 1
}
return c.disableRemoteState()
}
// Ensure there is no conflict
haveCache := !remoteState.Empty()
haveLocal := !localState.Empty()
switch {
case haveCache && haveLocal:
c.Ui.Error(fmt.Sprintf("Remote state is enabled, but non-managed state file '%s' is also present!",
c.conf.statePath))
return 1
case !haveCache && !haveLocal:
// If we don't have either state file, initialize a blank state file
return c.initBlankState()
case haveCache && !haveLocal:
// Update the remote state target potentially
return c.updateRemoteConfig()
case !haveCache && haveLocal:
// Enable remote state management
return c.enableRemoteState()
}
panic("unhandled case")
}
// disableRemoteState is used to disable remote state management,
// and move the state file into place.
func (c *RemoteCommand) disableRemoteState() int {
if c.stateResult == nil {
c.Ui.Error(fmt.Sprintf(
"Internal error. State() must be called internally before remote\n" +
"state can be disabled. Please report this as a bug."))
return 1
}
if !c.stateResult.State.State().IsRemote() {
c.Ui.Error(fmt.Sprintf(
"Remote state is not enabled. Can't disable remote state."))
return 1
}
local := c.stateResult.Local
remote := c.stateResult.Remote
// Ensure we have the latest state before disabling
if c.conf.pullOnDisable {
log.Printf("[INFO] Refreshing local state from remote server")
if err := remote.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf(
"Failed to refresh from remote state: %s", err))
return 1
}
// Exit if we were unable to update
if change := remote.RefreshResult(); !change.SuccessfulPull() {
c.Ui.Error(fmt.Sprintf("%s", change))
return 1
} else {
log.Printf("[INFO] %s", change)
}
}
// Clear the remote management, and copy into place
newState := remote.State()
newState.Remote = nil
if err := local.WriteState(newState); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %s",
c.conf.statePath, err))
return 1
}
if err := local.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %s",
c.conf.statePath, err))
return 1
}
// Remove the old state file
if err := os.Remove(c.stateResult.RemotePath); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to remove the local state file: %v", err))
return 1
}
return 0
}
// validateRemoteConfig is used to verify that the remote configuration
// we have is valid
func (c *RemoteCommand) validateRemoteConfig() error {
conf := c.remoteConf
_, err := remote.NewClient(conf.Type, conf.Config)
if err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
}
return err
}
// initBlank state is used to initialize a blank state that is
// remote enabled
func (c *RemoteCommand) initBlankState() int {
// Validate the remote configuration
if err := c.validateRemoteConfig(); err != nil {
return 1
}
// Make a blank state, attach the remote configuration
blank := terraform.NewState()
blank.Remote = &c.remoteConf
// Persist the state
remote := &state.LocalState{Path: c.stateResult.RemotePath}
if err := remote.WriteState(blank); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err))
return 1
}
if err := remote.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err))
return 1
}
// Success!
c.Ui.Output("Initialized blank state with remote state enabled!")
return 0
}
// updateRemoteConfig is used to update the configuration of the
// remote state store
func (c *RemoteCommand) updateRemoteConfig() int {
// Validate the remote configuration
if err := c.validateRemoteConfig(); err != nil {
return 1
}
// Read in the local state, which is just the cache of the remote state
remote := c.stateResult.Remote.Cache
// Update the configuration
state := remote.State()
state.Remote = &c.remoteConf
if err := remote.WriteState(state); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
if err := remote.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
// Success!
c.Ui.Output("Remote configuration updated")
return 0
}
// enableRemoteState is used to enable remote state management
// and to move a state file into place
func (c *RemoteCommand) enableRemoteState() int {
// Validate the remote configuration
if err := c.validateRemoteConfig(); err != nil {
return 1
}
// Read the local state
local := c.stateResult.Local
if err := local.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read local state: %s", err))
return 1
}
// Backup the state file before we modify it
backupPath := c.conf.backupPath
if backupPath != "-" {
// Provide default backup path if none provided
if backupPath == "" {
backupPath = c.conf.statePath + DefaultBackupExtention
}
log.Printf("[INFO] Writing backup state to: %s", backupPath)
backup := &state.LocalState{Path: backupPath}
if err := backup.WriteState(local.State()); err != nil {
c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
return 1
}
if err := backup.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
return 1
}
}
// Update the local configuration, move into place
state := local.State()
state.Remote = &c.remoteConf
remote := c.stateResult.Remote
if err := remote.WriteState(state); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
if err := remote.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
// Remove the original, local state file
log.Printf("[INFO] Removing state file: %s", c.conf.statePath)
if err := os.Remove(c.conf.statePath); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to remove state file '%s': %v",
c.conf.statePath, err))
return 1
}
// Success!
c.Ui.Output("Remote state management enabled")
return 0
} }
func (c *RemoteCommand) Help() string { func (c *RemoteCommand) Help() string {
helpText := ` helpText := `
Usage: terraform remote [options] Usage: terraform remote <subcommand> [options]
Configures Terraform to use a remote state server. This allows state Configure remote state storage with Terraform.
to be pulled down when necessary and then pushed to the server when
updated. In this mode, the state file does not need to be stored durably
since the remote server provides the durability.
Options: Available subcommands:
-backend=Atlas Specifies the type of remote backend. Must be one config Configure the remote storage settings.
of Atlas, Consul, or HTTP. Defaults to Atlas. pull Sync the remote storage by downloading to local storage.
push Sync the remote storage by uploading the local storage.
-backend-config="k=v" Specifies configuration for the remote storage
backend. This can be specified multiple times.
-backup=path Path to backup the existing state file before
modifying. Defaults to the "-state" path with
".backup" extension. Set to "-" to disable backup.
-disable Disables remote state management and migrates the state
to the -state path.
-pull=true Controls if the remote state is pulled before disabling.
This defaults to true to ensure the latest state is cached
before disabling.
-state=path Path to read state. Defaults to "terraform.tfstate"
unless remote state is enabled.
` `
return strings.TrimSpace(helpText) return strings.TrimSpace(helpText)
} }
func (c *RemoteCommand) Synopsis() string { func (c *RemoteCommand) Synopsis() string {
return "Configures remote state management" return "Configure remote state storage"
} }

339
command/remote_config.go Normal file
View File

@ -0,0 +1,339 @@
package command
import (
"flag"
"fmt"
"log"
"os"
"strings"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
)
// remoteCommandConfig is used to encapsulate our configuration
type remoteCommandConfig struct {
disableRemote bool
pullOnDisable bool
statePath string
backupPath string
}
// RemoteConfigCommand is a Command implementation that is used to
// enable and disable remote state management
type RemoteConfigCommand struct {
Meta
conf remoteCommandConfig
remoteConf terraform.RemoteState
}
func (c *RemoteConfigCommand) Run(args []string) int {
args = c.Meta.process(args, false)
config := make(map[string]string)
cmdFlags := flag.NewFlagSet("remote", flag.ContinueOnError)
cmdFlags.BoolVar(&c.conf.disableRemote, "disable", false, "")
cmdFlags.BoolVar(&c.conf.pullOnDisable, "pull", true, "")
cmdFlags.StringVar(&c.conf.statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&c.conf.backupPath, "backup", "", "path")
cmdFlags.StringVar(&c.remoteConf.Type, "backend", "atlas", "")
cmdFlags.Var((*FlagKV)(&config), "backend-config", "config")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
// Show help if given no inputs
if !c.conf.disableRemote && c.remoteConf.Type == "atlas" && len(config) == 0 {
cmdFlags.Usage()
return 1
}
// Set the local state path
c.statePath = c.conf.statePath
// Populate the various configurations
c.remoteConf.Config = config
// Get the state information. We specifically request the cache only
// for the remote state here because it is possible the remote state
// is invalid and we don't want to error.
stateOpts := c.StateOpts()
stateOpts.RemoteCacheOnly = true
if _, err := c.StateRaw(stateOpts); err != nil {
c.Ui.Error(fmt.Sprintf("Error loading local state: %s", err))
return 1
}
// Get the local and remote [cached] state
localState := c.stateResult.Local.State()
var remoteState *terraform.State
if remote := c.stateResult.Remote; remote != nil {
remoteState = remote.State()
}
// Check if remote state is being disabled
if c.conf.disableRemote {
if !remoteState.IsRemote() {
c.Ui.Error(fmt.Sprintf("Remote state management not enabled! Aborting."))
return 1
}
if !localState.Empty() {
c.Ui.Error(fmt.Sprintf("State file already exists at '%s'. Aborting.",
c.conf.statePath))
return 1
}
return c.disableRemoteState()
}
// Ensure there is no conflict
haveCache := !remoteState.Empty()
haveLocal := !localState.Empty()
switch {
case haveCache && haveLocal:
c.Ui.Error(fmt.Sprintf("Remote state is enabled, but non-managed state file '%s' is also present!",
c.conf.statePath))
return 1
case !haveCache && !haveLocal:
// If we don't have either state file, initialize a blank state file
return c.initBlankState()
case haveCache && !haveLocal:
// Update the remote state target potentially
return c.updateRemoteConfig()
case !haveCache && haveLocal:
// Enable remote state management
return c.enableRemoteState()
}
panic("unhandled case")
}
// disableRemoteState is used to disable remote state management,
// and move the state file into place.
func (c *RemoteConfigCommand) disableRemoteState() int {
if c.stateResult == nil {
c.Ui.Error(fmt.Sprintf(
"Internal error. State() must be called internally before remote\n" +
"state can be disabled. Please report this as a bug."))
return 1
}
if !c.stateResult.State.State().IsRemote() {
c.Ui.Error(fmt.Sprintf(
"Remote state is not enabled. Can't disable remote state."))
return 1
}
local := c.stateResult.Local
remote := c.stateResult.Remote
// Ensure we have the latest state before disabling
if c.conf.pullOnDisable {
log.Printf("[INFO] Refreshing local state from remote server")
if err := remote.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf(
"Failed to refresh from remote state: %s", err))
return 1
}
// Exit if we were unable to update
if change := remote.RefreshResult(); !change.SuccessfulPull() {
c.Ui.Error(fmt.Sprintf("%s", change))
return 1
} else {
log.Printf("[INFO] %s", change)
}
}
// Clear the remote management, and copy into place
newState := remote.State()
newState.Remote = nil
if err := local.WriteState(newState); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %s",
c.conf.statePath, err))
return 1
}
if err := local.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %s",
c.conf.statePath, err))
return 1
}
// Remove the old state file
if err := os.Remove(c.stateResult.RemotePath); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to remove the local state file: %v", err))
return 1
}
return 0
}
// validateRemoteConfig is used to verify that the remote configuration
// we have is valid
func (c *RemoteConfigCommand) validateRemoteConfig() error {
conf := c.remoteConf
_, err := remote.NewClient(conf.Type, conf.Config)
if err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
}
return err
}
// initBlank state is used to initialize a blank state that is
// remote enabled
func (c *RemoteConfigCommand) initBlankState() int {
// Validate the remote configuration
if err := c.validateRemoteConfig(); err != nil {
return 1
}
// Make a blank state, attach the remote configuration
blank := terraform.NewState()
blank.Remote = &c.remoteConf
// Persist the state
remote := &state.LocalState{Path: c.stateResult.RemotePath}
if err := remote.WriteState(blank); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err))
return 1
}
if err := remote.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err))
return 1
}
// Success!
c.Ui.Output("Initialized blank state with remote state enabled!")
return 0
}
// updateRemoteConfig is used to update the configuration of the
// remote state store
func (c *RemoteConfigCommand) updateRemoteConfig() int {
// Validate the remote configuration
if err := c.validateRemoteConfig(); err != nil {
return 1
}
// Read in the local state, which is just the cache of the remote state
remote := c.stateResult.Remote.Cache
// Update the configuration
state := remote.State()
state.Remote = &c.remoteConf
if err := remote.WriteState(state); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
if err := remote.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
// Success!
c.Ui.Output("Remote configuration updated")
return 0
}
// enableRemoteState is used to enable remote state management
// and to move a state file into place
func (c *RemoteConfigCommand) enableRemoteState() int {
// Validate the remote configuration
if err := c.validateRemoteConfig(); err != nil {
return 1
}
// Read the local state
local := c.stateResult.Local
if err := local.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read local state: %s", err))
return 1
}
// Backup the state file before we modify it
backupPath := c.conf.backupPath
if backupPath != "-" {
// Provide default backup path if none provided
if backupPath == "" {
backupPath = c.conf.statePath + DefaultBackupExtention
}
log.Printf("[INFO] Writing backup state to: %s", backupPath)
backup := &state.LocalState{Path: backupPath}
if err := backup.WriteState(local.State()); err != nil {
c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
return 1
}
if err := backup.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
return 1
}
}
// Update the local configuration, move into place
state := local.State()
state.Remote = &c.remoteConf
remote := c.stateResult.Remote
if err := remote.WriteState(state); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
if err := remote.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
return 1
}
// Remove the original, local state file
log.Printf("[INFO] Removing state file: %s", c.conf.statePath)
if err := os.Remove(c.conf.statePath); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to remove state file '%s': %v",
c.conf.statePath, err))
return 1
}
// Success!
c.Ui.Output("Remote state management enabled")
return 0
}
func (c *RemoteConfigCommand) Help() string {
helpText := `
Usage: terraform remote [options]
Configures Terraform to use a remote state server. This allows state
to be pulled down when necessary and then pushed to the server when
updated. In this mode, the state file does not need to be stored durably
since the remote server provides the durability.
Options:
-backend=Atlas Specifies the type of remote backend. Must be one
of Atlas, Consul, or HTTP. Defaults to Atlas.
-backend-config="k=v" Specifies configuration for the remote storage
backend. This can be specified multiple times.
-backup=path Path to backup the existing state file before
modifying. Defaults to the "-state" path with
".backup" extension. Set to "-" to disable backup.
-disable Disables remote state management and migrates the state
to the -state path.
-pull=true Controls if the remote state is pulled before disabling.
This defaults to true to ensure the latest state is cached
before disabling.
-state=path Path to read state. Defaults to "terraform.tfstate"
unless remote state is enabled.
`
return strings.TrimSpace(helpText)
}
func (c *RemoteConfigCommand) Synopsis() string {
return "Configures remote state management"
}

View File

@ -8,11 +8,11 @@ import (
"github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state"
) )
type PullCommand struct { type RemotePullCommand struct {
Meta Meta
} }
func (c *PullCommand) Run(args []string) int { func (c *RemotePullCommand) Run(args []string) int {
args = c.Meta.process(args, false) args = c.Meta.process(args, false)
cmdFlags := flag.NewFlagSet("pull", flag.ContinueOnError) cmdFlags := flag.NewFlagSet("pull", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
@ -67,7 +67,7 @@ func (c *PullCommand) Run(args []string) int {
return 0 return 0
} }
func (c *PullCommand) Help() string { func (c *RemotePullCommand) Help() string {
helpText := ` helpText := `
Usage: terraform pull [options] Usage: terraform pull [options]
@ -77,6 +77,6 @@ Usage: terraform pull [options]
return strings.TrimSpace(helpText) return strings.TrimSpace(helpText)
} }
func (c *PullCommand) Synopsis() string { func (c *RemotePullCommand) Synopsis() string {
return "Refreshes the local state copy from the remote server" return "Refreshes the local state copy from the remote server"
} }

View File

@ -15,12 +15,12 @@ import (
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
func TestPull_noRemote(t *testing.T) { func TestRemotePull_noRemote(t *testing.T) {
tmp, cwd := testCwd(t) tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd) defer testFixCwd(t, tmp, cwd)
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &PullCommand{ c := &RemotePullCommand{
Meta: Meta{ Meta: Meta{
ContextOpts: testCtxConfig(testProvider()), ContextOpts: testCtxConfig(testProvider()),
Ui: ui, Ui: ui,
@ -33,7 +33,7 @@ func TestPull_noRemote(t *testing.T) {
} }
} }
func TestPull_local(t *testing.T) { func TestRemotePull_local(t *testing.T) {
tmp, cwd := testCwd(t) tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd) defer testFixCwd(t, tmp, cwd)
@ -62,7 +62,7 @@ func TestPull_local(t *testing.T) {
} }
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &PullCommand{ c := &RemotePullCommand{
Meta: Meta{ Meta: Meta{
ContextOpts: testCtxConfig(testProvider()), ContextOpts: testCtxConfig(testProvider()),
Ui: ui, Ui: ui,

View File

@ -8,11 +8,11 @@ import (
"github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state"
) )
type PushCommand struct { type RemotePushCommand struct {
Meta Meta
} }
func (c *PushCommand) Run(args []string) int { func (c *RemotePushCommand) Run(args []string) int {
var force bool var force bool
args = c.Meta.process(args, false) args = c.Meta.process(args, false)
cmdFlags := flag.NewFlagSet("push", flag.ContinueOnError) cmdFlags := flag.NewFlagSet("push", flag.ContinueOnError)
@ -71,7 +71,7 @@ func (c *PushCommand) Run(args []string) int {
return 0 return 0
} }
func (c *PushCommand) Help() string { func (c *RemotePushCommand) Help() string {
helpText := ` helpText := `
Usage: terraform push [options] Usage: terraform push [options]
@ -87,6 +87,6 @@ Options:
return strings.TrimSpace(helpText) return strings.TrimSpace(helpText)
} }
func (c *PushCommand) Synopsis() string { func (c *RemotePushCommand) Synopsis() string {
return "Uploads the the local state to the remote server" return "Uploads the the local state to the remote server"
} }

View File

@ -9,12 +9,12 @@ import (
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
func TestPush_noRemote(t *testing.T) { func TestRemotePush_noRemote(t *testing.T) {
tmp, cwd := testCwd(t) tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd) defer testFixCwd(t, tmp, cwd)
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &PushCommand{ c := &RemotePushCommand{
Meta: Meta{ Meta: Meta{
ContextOpts: testCtxConfig(testProvider()), ContextOpts: testCtxConfig(testProvider()),
Ui: ui, Ui: ui,
@ -27,7 +27,7 @@ func TestPush_noRemote(t *testing.T) {
} }
} }
func TestPush_local(t *testing.T) { func TestRemotePush_local(t *testing.T) {
tmp, cwd := testCwd(t) tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd) defer testFixCwd(t, tmp, cwd)
@ -56,7 +56,7 @@ func TestPush_local(t *testing.T) {
} }
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &PushCommand{ c := &RemotePushCommand{
Meta: Meta{ Meta: Meta{
ContextOpts: testCtxConfig(testProvider()), ContextOpts: testCtxConfig(testProvider()),
Ui: ui, Ui: ui,

View File

@ -13,7 +13,7 @@ import (
) )
// Test disabling remote management // Test disabling remote management
func TestRemote_disable(t *testing.T) { func TestRemoteConfig_disable(t *testing.T) {
tmp, cwd := testCwd(t) tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd) defer testFixCwd(t, tmp, cwd)
@ -39,7 +39,7 @@ func TestRemote_disable(t *testing.T) {
} }
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &RemoteCommand{ c := &RemoteConfigCommand{
Meta: Meta{ Meta: Meta{
ContextOpts: testCtxConfig(testProvider()), ContextOpts: testCtxConfig(testProvider()),
Ui: ui, Ui: ui,
@ -68,7 +68,7 @@ func TestRemote_disable(t *testing.T) {
} }
// Test disabling remote management without pulling // Test disabling remote management without pulling
func TestRemote_disable_noPull(t *testing.T) { func TestRemoteConfig_disable_noPull(t *testing.T) {
tmp, cwd := testCwd(t) tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd) defer testFixCwd(t, tmp, cwd)
@ -94,7 +94,7 @@ func TestRemote_disable_noPull(t *testing.T) {
} }
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &RemoteCommand{ c := &RemoteConfigCommand{
Meta: Meta{ Meta: Meta{
ContextOpts: testCtxConfig(testProvider()), ContextOpts: testCtxConfig(testProvider()),
Ui: ui, Ui: ui,
@ -122,12 +122,12 @@ func TestRemote_disable_noPull(t *testing.T) {
} }
// Test disabling remote management when not enabled // Test disabling remote management when not enabled
func TestRemote_disable_notEnabled(t *testing.T) { func TestRemoteConfig_disable_notEnabled(t *testing.T) {
tmp, cwd := testCwd(t) tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd) defer testFixCwd(t, tmp, cwd)
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &RemoteCommand{ c := &RemoteConfigCommand{
Meta: Meta{ Meta: Meta{
ContextOpts: testCtxConfig(testProvider()), ContextOpts: testCtxConfig(testProvider()),
Ui: ui, Ui: ui,
@ -141,7 +141,7 @@ func TestRemote_disable_notEnabled(t *testing.T) {
} }
// Test disabling remote management with a state file in the way // Test disabling remote management with a state file in the way
func TestRemote_disable_otherState(t *testing.T) { func TestRemoteConfig_disable_otherState(t *testing.T) {
tmp, cwd := testCwd(t) tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd) defer testFixCwd(t, tmp, cwd)
@ -171,7 +171,7 @@ func TestRemote_disable_otherState(t *testing.T) {
} }
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &RemoteCommand{ c := &RemoteConfigCommand{
Meta: Meta{ Meta: Meta{
ContextOpts: testCtxConfig(testProvider()), ContextOpts: testCtxConfig(testProvider()),
Ui: ui, Ui: ui,
@ -185,7 +185,7 @@ func TestRemote_disable_otherState(t *testing.T) {
} }
// Test the case where both managed and non managed state present // Test the case where both managed and non managed state present
func TestRemote_managedAndNonManaged(t *testing.T) { func TestRemoteConfig_managedAndNonManaged(t *testing.T) {
tmp, cwd := testCwd(t) tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd) defer testFixCwd(t, tmp, cwd)
@ -215,7 +215,7 @@ func TestRemote_managedAndNonManaged(t *testing.T) {
} }
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &RemoteCommand{ c := &RemoteConfigCommand{
Meta: Meta{ Meta: Meta{
ContextOpts: testCtxConfig(testProvider()), ContextOpts: testCtxConfig(testProvider()),
Ui: ui, Ui: ui,
@ -229,12 +229,12 @@ func TestRemote_managedAndNonManaged(t *testing.T) {
} }
// Test initializing blank state // Test initializing blank state
func TestRemote_initBlank(t *testing.T) { func TestRemoteConfig_initBlank(t *testing.T) {
tmp, cwd := testCwd(t) tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd) defer testFixCwd(t, tmp, cwd)
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &RemoteCommand{ c := &RemoteConfigCommand{
Meta: Meta{ Meta: Meta{
ContextOpts: testCtxConfig(testProvider()), ContextOpts: testCtxConfig(testProvider()),
Ui: ui, Ui: ui,
@ -269,12 +269,12 @@ func TestRemote_initBlank(t *testing.T) {
} }
// Test initializing without remote settings // Test initializing without remote settings
func TestRemote_initBlank_missingRemote(t *testing.T) { func TestRemoteConfig_initBlank_missingRemote(t *testing.T) {
tmp, cwd := testCwd(t) tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd) defer testFixCwd(t, tmp, cwd)
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &RemoteCommand{ c := &RemoteConfigCommand{
Meta: Meta{ Meta: Meta{
ContextOpts: testCtxConfig(testProvider()), ContextOpts: testCtxConfig(testProvider()),
Ui: ui, Ui: ui,
@ -288,7 +288,7 @@ func TestRemote_initBlank_missingRemote(t *testing.T) {
} }
// Test updating remote config // Test updating remote config
func TestRemote_updateRemote(t *testing.T) { func TestRemoteConfig_updateRemote(t *testing.T) {
tmp, cwd := testCwd(t) tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd) defer testFixCwd(t, tmp, cwd)
@ -310,7 +310,7 @@ func TestRemote_updateRemote(t *testing.T) {
} }
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &RemoteCommand{ c := &RemoteConfigCommand{
Meta: Meta{ Meta: Meta{
ContextOpts: testCtxConfig(testProvider()), ContextOpts: testCtxConfig(testProvider()),
Ui: ui, Ui: ui,
@ -345,7 +345,7 @@ func TestRemote_updateRemote(t *testing.T) {
} }
// Test enabling remote state // Test enabling remote state
func TestRemote_enableRemote(t *testing.T) { func TestRemoteConfig_enableRemote(t *testing.T) {
tmp, cwd := testCwd(t) tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd) defer testFixCwd(t, tmp, cwd)
@ -365,7 +365,7 @@ func TestRemote_enableRemote(t *testing.T) {
} }
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &RemoteCommand{ c := &RemoteConfigCommand{
Meta: Meta{ Meta: Meta{
ContextOpts: testCtxConfig(testProvider()), ContextOpts: testCtxConfig(testProvider()),
Ui: ui, Ui: ui,

View File

@ -80,18 +80,6 @@ func init() {
}, nil }, nil
}, },
"pull": func() (cli.Command, error) {
return &command.PullCommand{
Meta: meta,
}, nil
},
"push": func() (cli.Command, error) {
return &command.PushCommand{
Meta: meta,
}, nil
},
"refresh": func() (cli.Command, error) { "refresh": func() (cli.Command, error) {
return &command.RefreshCommand{ return &command.RefreshCommand{
Meta: meta, Meta: meta,

View File

@ -17,6 +17,7 @@ var Funcs map[string]ast.Function
func init() { func init() {
Funcs = map[string]ast.Function{ Funcs = map[string]ast.Function{
"file": interpolationFuncFile(), "file": interpolationFuncFile(),
"format": interpolationFuncFormat(),
"join": interpolationFuncJoin(), "join": interpolationFuncJoin(),
"element": interpolationFuncElement(), "element": interpolationFuncElement(),
"replace": interpolationFuncReplace(), "replace": interpolationFuncReplace(),
@ -66,6 +67,21 @@ func interpolationFuncFile() ast.Function {
} }
} }
// interpolationFuncFormat implements the "replace" function that does
// string replacement.
func interpolationFuncFormat() ast.Function {
return ast.Function{
ArgTypes: []ast.Type{ast.TypeString},
Variadic: true,
VariadicType: ast.TypeAny,
ReturnType: ast.TypeString,
Callback: func(args []interface{}) (interface{}, error) {
format := args[0].(string)
return fmt.Sprintf(format, args[1:]...), nil
},
}
}
// interpolationFuncJoin implements the "join" function that allows // interpolationFuncJoin implements the "join" function that allows
// multi-variable values to be joined by some character. // multi-variable values to be joined by some character.
func interpolationFuncJoin() ast.Function { func interpolationFuncJoin() ast.Function {

View File

@ -70,6 +70,42 @@ func TestInterpolateFuncFile(t *testing.T) {
}) })
} }
func TestInterpolateFuncFormat(t *testing.T) {
testFunction(t, testFunctionConfig{
Cases: []testFunctionCase{
{
`${format("hello")}`,
"hello",
false,
},
{
`${format("hello %s", "world")}`,
"hello world",
false,
},
{
`${format("hello %d", 42)}`,
"hello 42",
false,
},
{
`${format("hello %05d", 42)}`,
"hello 00042",
false,
},
{
`${format("hello %05d", 12345)}`,
"hello 12345",
false,
},
},
})
}
func TestInterpolateFuncJoin(t *testing.T) { func TestInterpolateFuncJoin(t *testing.T) {
testFunction(t, testFunctionConfig{ testFunction(t, testFunctionConfig{
Cases: []testFunctionCase{ Cases: []testFunctionCase{

View File

@ -48,7 +48,8 @@ type Type uint32
const ( const (
TypeInvalid Type = 0 TypeInvalid Type = 0
TypeString Type = 1 << iota TypeAny Type = 1 << iota
TypeString
TypeInt TypeInt
TypeFloat TypeFloat
) )

View File

@ -6,16 +6,18 @@ import "fmt"
const ( const (
_Type_name_0 = "TypeInvalid" _Type_name_0 = "TypeInvalid"
_Type_name_1 = "TypeString" _Type_name_1 = "TypeAny"
_Type_name_2 = "TypeInt" _Type_name_2 = "TypeString"
_Type_name_3 = "TypeFloat" _Type_name_3 = "TypeInt"
_Type_name_4 = "TypeFloat"
) )
var ( var (
_Type_index_0 = [...]uint8{0, 11} _Type_index_0 = [...]uint8{0, 11}
_Type_index_1 = [...]uint8{0, 10} _Type_index_1 = [...]uint8{0, 7}
_Type_index_2 = [...]uint8{0, 7} _Type_index_2 = [...]uint8{0, 10}
_Type_index_3 = [...]uint8{0, 9} _Type_index_3 = [...]uint8{0, 7}
_Type_index_4 = [...]uint8{0, 9}
) )
func (i Type) String() string { func (i Type) String() string {
@ -28,6 +30,8 @@ func (i Type) String() string {
return _Type_name_2 return _Type_name_2
case i == 8: case i == 8:
return _Type_name_3 return _Type_name_3
case i == 16:
return _Type_name_4
default: default:
return fmt.Sprintf("Type(%d)", i) return fmt.Sprintf("Type(%d)", i)
} }

View File

@ -174,6 +174,10 @@ func (tc *typeCheckCall) TypeCheck(v *TypeCheck) (ast.Node, error) {
// Verify the args // Verify the args
for i, expected := range function.ArgTypes { for i, expected := range function.ArgTypes {
if expected == ast.TypeAny {
continue
}
if args[i] != expected { if args[i] != expected {
cn := v.ImplicitConversion(args[i], expected, tc.n.Args[i]) cn := v.ImplicitConversion(args[i], expected, tc.n.Args[i])
if cn != nil { if cn != nil {
@ -188,7 +192,7 @@ func (tc *typeCheckCall) TypeCheck(v *TypeCheck) (ast.Node, error) {
} }
// If we're variadic, then verify the types there // If we're variadic, then verify the types there
if function.Variadic { if function.Variadic && function.VariadicType != ast.TypeAny {
args = args[len(function.ArgTypes):] args = args[len(function.ArgTypes):]
for i, t := range args { for i, t := range args {
if t != function.VariadicType { if t != function.VariadicType {

View File

@ -208,6 +208,15 @@ func TestEval(t *testing.T) {
"foo 42", "foo 42",
ast.TypeString, ast.TypeString,
}, },
// Multiline
{
"foo ${42+\n1.0}",
nil,
false,
"foo 43",
ast.TypeString,
},
} }
for _, tc := range cases { for _, tc := range cases {

View File

@ -137,6 +137,25 @@ func (d *ResourceData) Partial(on bool) {
// will be returned. // will be returned.
func (d *ResourceData) Set(key string, value interface{}) error { func (d *ResourceData) Set(key string, value interface{}) error {
d.once.Do(d.init) d.once.Do(d.init)
// If the value is a pointer to a non-struct, get its value and
// use that. This allows Set to take a pointer to primitives to
// simplify the interface.
reflectVal := reflect.ValueOf(value)
if reflectVal.Kind() == reflect.Ptr {
if reflectVal.IsNil() {
// If the pointer is nil, then the value is just nil
value = nil
} else {
// Otherwise, we dereference the pointer as long as its not
// a pointer to a struct, since struct pointers are allowed.
reflectVal = reflect.Indirect(reflectVal)
if reflectVal.Kind() != reflect.Struct {
value = reflectVal.Interface()
}
}
}
return d.setWriter.WriteField(strings.Split(key, "."), value) return d.setWriter.WriteField(strings.Split(key, "."), value)
} }

View File

@ -1178,6 +1178,8 @@ func TestResourceDataHasChange(t *testing.T) {
} }
func TestResourceDataSet(t *testing.T) { func TestResourceDataSet(t *testing.T) {
var testNilPtr *string
cases := []struct { cases := []struct {
Schema map[string]*Schema Schema map[string]*Schema
State *terraform.InstanceState State *terraform.InstanceState
@ -1588,6 +1590,72 @@ func TestResourceDataSet(t *testing.T) {
GetKey: "ratios", GetKey: "ratios",
GetValue: []interface{}{1.0, 2.2, 5.5}, GetValue: []interface{}{1.0, 2.2, 5.5},
}, },
// #13: Basic pointer
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Diff: nil,
Key: "availability_zone",
Value: testPtrTo("foo"),
GetKey: "availability_zone",
GetValue: "foo",
},
// #14: Basic nil value
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Diff: nil,
Key: "availability_zone",
Value: testPtrTo(nil),
GetKey: "availability_zone",
GetValue: "",
},
// #15: Basic nil pointer
{
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
State: nil,
Diff: nil,
Key: "availability_zone",
Value: testNilPtr,
GetKey: "availability_zone",
GetValue: "",
},
} }
for i, tc := range cases { for i, tc := range cases {
@ -2788,3 +2856,7 @@ func TestResourceDataSetId_override(t *testing.T) {
t.Fatalf("bad: %#v", actual) t.Fatalf("bad: %#v", actual)
} }
} }
func testPtrTo(raw interface{}) interface{} {
return &raw
}

View File

@ -113,6 +113,21 @@ type Schema struct {
// //
// NOTE: This currently does not work. // NOTE: This currently does not work.
ComputedWhen []string ComputedWhen []string
// When Deprecated is set, this attribute is deprecated.
//
// A deprecated field still works, but will probably stop working in near
// future. This string is the message shown to the user with instructions on
// how to address the deprecation.
Deprecated string
// When Removed is set, this attribute has been removed from the schema
//
// Removed attributes can be left in the Schema to generate informative error
// messages for the user when they show up in resource configurations.
// This string is the message shown to the user with instructions on
// what do to about the removed attribute.
Removed string
} }
// SchemaDefaultFunc is a function called to return a default value for // SchemaDefaultFunc is a function called to return a default value for
@ -877,7 +892,7 @@ func (m schemaMap) validate(
raw, err = schema.DefaultFunc() raw, err = schema.DefaultFunc()
if err != nil { if err != nil {
return nil, []error{fmt.Errorf( return nil, []error{fmt.Errorf(
"%s, error loading default: %s", k, err)} "%q, error loading default: %s", k, err)}
} }
// We're okay as long as we had a value set // We're okay as long as we had a value set
@ -886,7 +901,7 @@ func (m schemaMap) validate(
if !ok { if !ok {
if schema.Required { if schema.Required {
return nil, []error{fmt.Errorf( return nil, []error{fmt.Errorf(
"%s: required field is not set", k)} "%q: required field is not set", k)}
} }
return nil, nil return nil, nil
@ -895,7 +910,7 @@ func (m schemaMap) validate(
if !schema.Required && !schema.Optional { if !schema.Required && !schema.Optional {
// This is a computed-only field // This is a computed-only field
return nil, []error{fmt.Errorf( return nil, []error{fmt.Errorf(
"%s: this field cannot be set", k)} "%q: this field cannot be set", k)}
} }
return m.validateType(k, raw, schema, c) return m.validateType(k, raw, schema, c)
@ -1066,16 +1081,30 @@ func (m schemaMap) validateType(
raw interface{}, raw interface{},
schema *Schema, schema *Schema,
c *terraform.ResourceConfig) ([]string, []error) { c *terraform.ResourceConfig) ([]string, []error) {
var ws []string
var es []error
switch schema.Type { switch schema.Type {
case TypeSet: case TypeSet:
fallthrough fallthrough
case TypeList: case TypeList:
return m.validateList(k, raw, schema, c) ws, es = m.validateList(k, raw, schema, c)
case TypeMap: case TypeMap:
return m.validateMap(k, raw, schema, c) ws, es = m.validateMap(k, raw, schema, c)
default: default:
return m.validatePrimitive(k, raw, schema, c) ws, es = m.validatePrimitive(k, raw, schema, c)
} }
if schema.Deprecated != "" {
ws = append(ws, fmt.Sprintf(
"%q: [DEPRECATED] %s", k, schema.Deprecated))
}
if schema.Removed != "" {
es = append(es, fmt.Errorf(
"%q: [REMOVED] %s", k, schema.Removed))
}
return ws, es
} }
// Zero returns the zero value for a type. // Zero returns the zero value for a type.

View File

@ -2583,15 +2583,15 @@ func TestSchemaMap_InternalValidate(t *testing.T) {
} }
func TestSchemaMap_Validate(t *testing.T) { func TestSchemaMap_Validate(t *testing.T) {
cases := []struct { cases := map[string]struct {
Schema map[string]*Schema Schema map[string]*Schema
Config map[string]interface{} Config map[string]interface{}
Vars map[string]string Vars map[string]string
Warn bool
Err bool Err bool
Errors []error
Warnings []string
}{ }{
// #0 Good "Good": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"availability_zone": &Schema{ "availability_zone": &Schema{
Type: TypeString, Type: TypeString,
@ -2606,8 +2606,7 @@ func TestSchemaMap_Validate(t *testing.T) {
}, },
}, },
// #1 Good, because the var is not set and that error will come elsewhere "Good, because the var is not set and that error will come elsewhere": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"size": &Schema{ "size": &Schema{
Type: TypeInt, Type: TypeInt,
@ -2624,8 +2623,7 @@ func TestSchemaMap_Validate(t *testing.T) {
}, },
}, },
// #2 Required field not set "Required field not set": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"availability_zone": &Schema{ "availability_zone": &Schema{
Type: TypeString, Type: TypeString,
@ -2638,8 +2636,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true, Err: true,
}, },
// #3 Invalid type "Invalid basic type": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"port": &Schema{ "port": &Schema{
Type: TypeInt, Type: TypeInt,
@ -2654,8 +2651,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true, Err: true,
}, },
// #4 "Invalid complex type": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"user_data": &Schema{ "user_data": &Schema{
Type: TypeString, Type: TypeString,
@ -2674,8 +2670,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true, Err: true,
}, },
// #5 Bad type, interpolated "Bad type, interpolated": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"size": &Schema{ "size": &Schema{
Type: TypeInt, Type: TypeInt,
@ -2694,8 +2689,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true, Err: true,
}, },
// #6 Required but has DefaultFunc "Required but has DefaultFunc": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"availability_zone": &Schema{ "availability_zone": &Schema{
Type: TypeString, Type: TypeString,
@ -2709,8 +2703,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Config: nil, Config: nil,
}, },
// #7 Required but has DefaultFunc return nil "Required but has DefaultFunc return nil": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"availability_zone": &Schema{ "availability_zone": &Schema{
Type: TypeString, Type: TypeString,
@ -2726,8 +2719,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true, Err: true,
}, },
// #8 Optional sub-resource "Optional sub-resource": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"ingress": &Schema{ "ingress": &Schema{
Type: TypeList, Type: TypeList,
@ -2747,8 +2739,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: false, Err: false,
}, },
// #9 Not a list "Not a list": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"ingress": &Schema{ "ingress": &Schema{
Type: TypeList, Type: TypeList,
@ -2770,8 +2761,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true, Err: true,
}, },
// #10 Required sub-resource field "Required sub-resource field": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"ingress": &Schema{ "ingress": &Schema{
Type: TypeList, Type: TypeList,
@ -2795,8 +2785,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true, Err: true,
}, },
// #11 Good sub-resource "Good sub-resource": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"ingress": &Schema{ "ingress": &Schema{
Type: TypeList, Type: TypeList,
@ -2823,8 +2812,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: false, Err: false,
}, },
// #12 Invalid/unknown field "Invalid/unknown field": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"availability_zone": &Schema{ "availability_zone": &Schema{
Type: TypeString, Type: TypeString,
@ -2841,8 +2829,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true, Err: true,
}, },
// #13 Computed field set "Computed field set": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"availability_zone": &Schema{ "availability_zone": &Schema{
Type: TypeString, Type: TypeString,
@ -2857,8 +2844,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true, Err: true,
}, },
// #14 Not a set "Not a set": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"ports": &Schema{ "ports": &Schema{
Type: TypeSet, Type: TypeSet,
@ -2877,8 +2863,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true, Err: true,
}, },
// #15 Maps "Maps": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"user_data": &Schema{ "user_data": &Schema{
Type: TypeMap, Type: TypeMap,
@ -2893,8 +2878,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true, Err: true,
}, },
// #16 "Good map: data surrounded by extra slice": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"user_data": &Schema{ "user_data": &Schema{
Type: TypeMap, Type: TypeMap,
@ -2911,8 +2895,7 @@ func TestSchemaMap_Validate(t *testing.T) {
}, },
}, },
// #17 "Good map": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"user_data": &Schema{ "user_data": &Schema{
Type: TypeMap, Type: TypeMap,
@ -2927,8 +2910,7 @@ func TestSchemaMap_Validate(t *testing.T) {
}, },
}, },
// #18 "Bad map: just a slice": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"user_data": &Schema{ "user_data": &Schema{
Type: TypeMap, Type: TypeMap,
@ -2945,8 +2927,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true, Err: true,
}, },
// #19 "Good set: config has slice with single interpolated value": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"security_groups": &Schema{ "security_groups": &Schema{
Type: TypeSet, Type: TypeSet,
@ -2967,8 +2948,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: false, Err: false,
}, },
// #20 "Bad set: config has single interpolated value": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"security_groups": &Schema{ "security_groups": &Schema{
Type: TypeSet, Type: TypeSet,
@ -2986,8 +2966,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true, Err: true,
}, },
// #21 Bad, subresource should not allow unknown elements "Bad, subresource should not allow unknown elements": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"ingress": &Schema{ "ingress": &Schema{
Type: TypeList, Type: TypeList,
@ -3015,8 +2994,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true, Err: true,
}, },
// #22 Bad, subresource should not allow invalid types "Bad, subresource should not allow invalid types": {
{
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"ingress": &Schema{ "ingress": &Schema{
Type: TypeList, Type: TypeList,
@ -3042,9 +3020,74 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true, Err: true,
}, },
"Deprecated attribute usage generates warning, but not error": {
Schema: map[string]*Schema{
"old_news": &Schema{
Type: TypeString,
Optional: true,
Deprecated: "please use 'new_news' instead",
},
},
Config: map[string]interface{}{
"old_news": "extra extra!",
},
Err: false,
Warnings: []string{
"\"old_news\": [DEPRECATED] please use 'new_news' instead",
},
},
"Deprecated generates no warnings if attr not used": {
Schema: map[string]*Schema{
"old_news": &Schema{
Type: TypeString,
Optional: true,
Deprecated: "please use 'new_news' instead",
},
},
Err: false,
Warnings: nil,
},
"Removed attribute usage generates error": {
Schema: map[string]*Schema{
"long_gone": &Schema{
Type: TypeString,
Optional: true,
Removed: "no longer supported by Cloud API",
},
},
Config: map[string]interface{}{
"long_gone": "still here!",
},
Err: true,
Errors: []error{
fmt.Errorf("\"long_gone\": [REMOVED] no longer supported by Cloud API"),
},
},
"Removed generates no errors if attr not used": {
Schema: map[string]*Schema{
"long_gone": &Schema{
Type: TypeString,
Optional: true,
Removed: "no longer supported by Cloud API",
},
},
Err: false,
},
} }
for i, tc := range cases { for tn, tc := range cases {
c, err := config.NewRawConfig(tc.Config) c, err := config.NewRawConfig(tc.Config)
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -3063,18 +3106,24 @@ func TestSchemaMap_Validate(t *testing.T) {
ws, es := schemaMap(tc.Schema).Validate(terraform.NewResourceConfig(c)) ws, es := schemaMap(tc.Schema).Validate(terraform.NewResourceConfig(c))
if (len(es) > 0) != tc.Err { if (len(es) > 0) != tc.Err {
if len(es) == 0 { if len(es) == 0 {
t.Errorf("%d: no errors", i) t.Errorf("%q: no errors", tn)
} }
for _, e := range es { for _, e := range es {
t.Errorf("%d: err: %s", i, e) t.Errorf("%q: err: %s", tn, e)
} }
t.FailNow() t.FailNow()
} }
if (len(ws) > 0) != tc.Warn { if !reflect.DeepEqual(ws, tc.Warnings) {
t.Fatalf("%d: ws: %#v", i, ws) t.Fatalf("%q: warnings:\n\nexpected: %#v\ngot:%#v", tn, tc.Warnings, ws)
}
if tc.Errors != nil {
if !reflect.DeepEqual(es, tc.Errors) {
t.Fatalf("%q: errors:\n\nexpected: %q\ngot: %q", tn, tc.Errors, es)
}
} }
} }
} }

64
state/remote/file.go Normal file
View File

@ -0,0 +1,64 @@
package remote
import (
"bytes"
"crypto/md5"
"fmt"
"io"
"os"
)
func fileFactory(conf map[string]string) (Client, error) {
path, ok := conf["path"]
if !ok {
return nil, fmt.Errorf("missing 'path' configuration")
}
return &FileClient{
Path: path,
}, nil
}
// FileClient is a remote client that stores data locally on disk.
// This is only used for development reasons to test remote state... locally.
type FileClient struct {
Path string
}
func (c *FileClient) Get() (*Payload, error) {
var buf bytes.Buffer
f, err := os.Open(c.Path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
defer f.Close()
if _, err := io.Copy(&buf, f); err != nil {
return nil, err
}
md5 := md5.Sum(buf.Bytes())
return &Payload{
Data: buf.Bytes(),
MD5: md5[:],
}, nil
}
func (c *FileClient) Put(data []byte) error {
f, err := os.Create(c.Path)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(data)
return err
}
func (c *FileClient) Delete() error {
return os.Remove(c.Path)
}

29
state/remote/file_test.go Normal file
View File

@ -0,0 +1,29 @@
package remote
import (
"io/ioutil"
"os"
"testing"
)
func TestFileClient_impl(t *testing.T) {
var _ Client = new(FileClient)
}
func TestFileClient(t *testing.T) {
tf, err := ioutil.TempFile("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
tf.Close()
defer os.Remove(tf.Name())
client, err := fileFactory(map[string]string{
"path": tf.Name(),
})
if err != nil {
t.Fatalf("bad: %s", err)
}
testClient(t, client)
}

View File

@ -39,4 +39,7 @@ var BuiltinClients = map[string]Factory{
"atlas": atlasFactory, "atlas": atlasFactory,
"consul": consulFactory, "consul": consulFactory,
"http": httpFactory, "http": httpFactory,
// This is used for development purposes only.
"_local": fileFactory,
} }

View File

@ -3845,6 +3845,136 @@ func TestContext2Apply_errorDestroy_createBeforeDestroy(t *testing.T) {
} }
} }
func TestContext2Apply_multiDepose_createBeforeDestroy(t *testing.T) {
m := testModule(t, "apply-multi-depose-create-before-destroy")
p := testProvider("aws")
p.DiffFn = testDiffFn
ps := map[string]ResourceProviderFactory{"aws": testProviderFuncFixed(p)}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{ID: "foo"},
},
},
},
},
}
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: ps,
State: state,
})
createdInstanceId := "bar"
// Create works
createFunc := func(is *InstanceState) (*InstanceState, error) {
return &InstanceState{ID: createdInstanceId}, nil
}
// Destroy starts broken
destroyFunc := func(is *InstanceState) (*InstanceState, error) {
return is, fmt.Errorf("destroy failed")
}
p.ApplyFn = func(info *InstanceInfo, is *InstanceState, id *InstanceDiff) (*InstanceState, error) {
if id.Destroy {
return destroyFunc(is)
} else {
return createFunc(is)
}
}
if _, err := ctx.Plan(nil); err != nil {
t.Fatalf("err: %s", err)
}
// Destroy is broken, so even though CBD successfully replaces the instance,
// we'll have to save the Deposed instance to destroy later
state, err := ctx.Apply()
if err == nil {
t.Fatal("should have error")
}
checkStateString(t, state, `
aws_instance.web: (1 deposed)
ID = bar
Deposed ID 1 = foo
`)
createdInstanceId = "baz"
ctx = testContext2(t, &ContextOpts{
Module: m,
Providers: ps,
State: state,
})
if _, err := ctx.Plan(nil); err != nil {
t.Fatalf("err: %s", err)
}
// We're replacing the primary instance once again. Destroy is _still_
// broken, so the Deposed list gets longer
state, err = ctx.Apply()
if err == nil {
t.Fatal("should have error")
}
checkStateString(t, state, `
aws_instance.web: (2 deposed)
ID = baz
Deposed ID 1 = foo
Deposed ID 2 = bar
`)
// Destroy partially fixed!
destroyFunc = func(is *InstanceState) (*InstanceState, error) {
if is.ID == "foo" || is.ID == "baz" {
return nil, nil
} else {
return is, fmt.Errorf("destroy partially failed")
}
}
createdInstanceId = "qux"
if _, err := ctx.Plan(nil); err != nil {
t.Fatalf("err: %s", err)
}
state, err = ctx.Apply()
// Expect error because 1/2 of Deposed destroys failed
if err == nil {
t.Fatal("should have error")
}
// foo and baz are now gone, bar sticks around
checkStateString(t, state, `
aws_instance.web: (1 deposed)
ID = qux
Deposed ID 1 = bar
`)
// Destroy working fully!
destroyFunc = func(is *InstanceState) (*InstanceState, error) {
return nil, nil
}
createdInstanceId = "quux"
if _, err := ctx.Plan(nil); err != nil {
t.Fatalf("err: %s", err)
}
state, err = ctx.Apply()
if err != nil {
t.Fatal("should not have error:", err)
}
// And finally the state is clean
checkStateString(t, state, `
aws_instance.web:
ID = quux
`)
}
func TestContext2Apply_provisionerResourceRef(t *testing.T) { func TestContext2Apply_provisionerResourceRef(t *testing.T) {
m := testModule(t, "apply-provisioner-resource-ref") m := testModule(t, "apply-provisioner-resource-ref")
p := testProvider("aws") p := testProvider("aws")
@ -5343,6 +5473,15 @@ func testProvisioner() *MockResourceProvisioner {
return p return p
} }
func checkStateString(t *testing.T, state *State, expected string) {
actual := strings.TrimSpace(state.String())
expected = strings.TrimSpace(expected)
if actual != expected {
t.Fatalf("state does not match! actual:\n%s\n\nexpected:\n%s", actual, expected)
}
}
const testContextGraph = ` const testContextGraph = `
root: root root: root
aws_instance.bar aws_instance.bar

20
terraform/eval_error.go Normal file
View File

@ -0,0 +1,20 @@
package terraform
// EvalReturnError is an EvalNode implementation that returns an
// error if it is present.
//
// This is useful for scenarios where an error has been captured by
// another EvalNode (like EvalApply) for special EvalTree-based error
// handling, and that handling has completed, so the error should be
// returned normally.
type EvalReturnError struct {
Error *error
}
func (n *EvalReturnError) Eval(ctx EvalContext) (interface{}, error) {
if n.Error == nil {
return nil, nil
}
return nil, *n.Error
}

View File

@ -3,7 +3,8 @@ package terraform
// EvalIf is an EvalNode that is a conditional. // EvalIf is an EvalNode that is a conditional.
type EvalIf struct { type EvalIf struct {
If func(EvalContext) (bool, error) If func(EvalContext) (bool, error)
Node EvalNode Then EvalNode
Else EvalNode
} }
// TODO: test // TODO: test
@ -14,7 +15,11 @@ func (n *EvalIf) Eval(ctx EvalContext) (interface{}, error) {
} }
if yes { if yes {
return EvalRaw(n.Node, ctx) return EvalRaw(n.Then, ctx)
} else {
if n.Else != nil {
return EvalRaw(n.Else, ctx)
}
} }
return nil, nil return nil, nil

View File

@ -5,16 +5,77 @@ import (
) )
// EvalReadState is an EvalNode implementation that reads the // EvalReadState is an EvalNode implementation that reads the
// InstanceState for a specific resource out of the state. // primary InstanceState for a specific resource out of the state.
type EvalReadState struct { type EvalReadState struct {
Name string Name string
Tainted bool
TaintedIndex int
Output **InstanceState Output **InstanceState
} }
// TODO: test
func (n *EvalReadState) Eval(ctx EvalContext) (interface{}, error) { func (n *EvalReadState) Eval(ctx EvalContext) (interface{}, error) {
return readInstanceFromState(ctx, n.Name, n.Output, func(rs *ResourceState) (*InstanceState, error) {
return rs.Primary, nil
})
}
// EvalReadStateTainted is an EvalNode implementation that reads a
// tainted InstanceState for a specific resource out of the state
type EvalReadStateTainted struct {
Name string
Output **InstanceState
// Index indicates which instance in the Tainted list to target, or -1 for
// the last item.
Index int
}
func (n *EvalReadStateTainted) Eval(ctx EvalContext) (interface{}, error) {
return readInstanceFromState(ctx, n.Name, n.Output, func(rs *ResourceState) (*InstanceState, error) {
// Get the index. If it is negative, then we get the last one
idx := n.Index
if idx < 0 {
idx = len(rs.Tainted) - 1
}
if idx >= 0 && idx < len(rs.Tainted) {
return rs.Tainted[idx], nil
} else {
return nil, fmt.Errorf("bad tainted index: %d, for resource: %#v", idx, rs)
}
})
}
// EvalReadStateDeposed is an EvalNode implementation that reads the
// deposed InstanceState for a specific resource out of the state
type EvalReadStateDeposed struct {
Name string
Output **InstanceState
// Index indicates which instance in the Deposed list to target, or -1 for
// the last item.
Index int
}
func (n *EvalReadStateDeposed) Eval(ctx EvalContext) (interface{}, error) {
return readInstanceFromState(ctx, n.Name, n.Output, func(rs *ResourceState) (*InstanceState, error) {
// Get the index. If it is negative, then we get the last one
idx := n.Index
if idx < 0 {
idx = len(rs.Deposed) - 1
}
if idx >= 0 && idx < len(rs.Deposed) {
return rs.Deposed[idx], nil
} else {
return nil, fmt.Errorf("bad deposed index: %d, for resource: %#v", idx, rs)
}
})
}
// Does the bulk of the work for the various flavors of ReadState eval nodes.
// Each node just provides a reader function to get from the ResourceState to the
// InstanceState, and this takes care of all the plumbing.
func readInstanceFromState(
ctx EvalContext,
resourceName string,
output **InstanceState,
readerFn func(*ResourceState) (*InstanceState, error),
) (*InstanceState, error) {
state, lock := ctx.State() state, lock := ctx.State()
// Get a read lock so we can access this instance // Get a read lock so we can access this instance
@ -28,33 +89,23 @@ func (n *EvalReadState) Eval(ctx EvalContext) (interface{}, error) {
} }
// Look for the resource state. If we don't have one, then it is okay. // Look for the resource state. If we don't have one, then it is okay.
rs := mod.Resources[n.Name] rs := mod.Resources[resourceName]
if rs == nil { if rs == nil {
return nil, nil return nil, nil
} }
var result *InstanceState // Use the delegate function to get the instance state from the resource state
if !n.Tainted { is, err := readerFn(rs)
// Return the primary if err != nil {
result = rs.Primary return nil, err
} else {
// Get the index. If it is negative, then we get the last one
idx := n.TaintedIndex
if idx < 0 {
idx = len(rs.Tainted) - 1
}
if idx >= 0 && idx < len(rs.Tainted) {
// Return the proper tainted resource
result = rs.Tainted[idx]
}
} }
// Write the result to the output pointer // Write the result to the output pointer
if n.Output != nil { if output != nil {
*n.Output = result *output = is
} }
return result, nil return is, nil
} }
// EvalRequireState is an EvalNode implementation that early exits // EvalRequireState is an EvalNode implementation that early exits
@ -98,20 +149,85 @@ func (n *EvalUpdateStateHook) Eval(ctx EvalContext) (interface{}, error) {
return nil, nil return nil, nil
} }
// EvalWriteState is an EvalNode implementation that reads the // EvalWriteState is an EvalNode implementation that writes the
// InstanceState for a specific resource out of the state. // primary InstanceState for a specific resource into the state.
type EvalWriteState struct { type EvalWriteState struct {
Name string Name string
ResourceType string ResourceType string
Dependencies []string Dependencies []string
State **InstanceState State **InstanceState
Tainted *bool
TaintedIndex int
TaintedClearPrimary bool
} }
// TODO: test
func (n *EvalWriteState) Eval(ctx EvalContext) (interface{}, error) { func (n *EvalWriteState) Eval(ctx EvalContext) (interface{}, error) {
return writeInstanceToState(ctx, n.Name, n.ResourceType, n.Dependencies,
func(rs *ResourceState) error {
rs.Primary = *n.State
return nil
},
)
}
// EvalWriteStateTainted is an EvalNode implementation that writes
// an InstanceState out to the Tainted list of a resource in the state.
type EvalWriteStateTainted struct {
Name string
ResourceType string
Dependencies []string
State **InstanceState
// Index indicates which instance in the Tainted list to target, or -1 to append.
Index int
}
// EvalWriteStateTainted is an EvalNode implementation that writes the
// one of the tainted InstanceStates for a specific resource out of the state.
func (n *EvalWriteStateTainted) Eval(ctx EvalContext) (interface{}, error) {
return writeInstanceToState(ctx, n.Name, n.ResourceType, n.Dependencies,
func(rs *ResourceState) error {
if n.Index == -1 {
rs.Tainted = append(rs.Tainted, *n.State)
} else {
rs.Tainted[n.Index] = *n.State
}
return nil
},
)
}
// EvalWriteStateDeposed is an EvalNode implementation that writes
// an InstanceState out to the Deposed list of a resource in the state.
type EvalWriteStateDeposed struct {
Name string
ResourceType string
Dependencies []string
State **InstanceState
// Index indicates which instance in the Deposed list to target, or -1 to append.
Index int
}
func (n *EvalWriteStateDeposed) Eval(ctx EvalContext) (interface{}, error) {
return writeInstanceToState(ctx, n.Name, n.ResourceType, n.Dependencies,
func(rs *ResourceState) error {
if n.Index == -1 {
rs.Deposed = append(rs.Deposed, *n.State)
} else {
rs.Deposed[n.Index] = *n.State
}
return nil
},
)
}
// Pulls together the common tasks of the EvalWriteState nodes. All the args
// are passed directly down from the EvalNode along with a `writer` function
// which is yielded the *ResourceState and is responsible for writing an
// InstanceState to the proper field in the ResourceState.
func writeInstanceToState(
ctx EvalContext,
resourceName string,
resourceType string,
dependencies []string,
writerFn func(*ResourceState) error,
) (*InstanceState, error) {
state, lock := ctx.State() state, lock := ctx.State()
if state == nil { if state == nil {
return nil, fmt.Errorf("cannot write state to nil state") return nil, fmt.Errorf("cannot write state to nil state")
@ -128,35 +244,55 @@ func (n *EvalWriteState) Eval(ctx EvalContext) (interface{}, error) {
} }
// Look for the resource state. // Look for the resource state.
rs := mod.Resources[n.Name] rs := mod.Resources[resourceName]
if rs == nil { if rs == nil {
rs = &ResourceState{} rs = &ResourceState{}
rs.init() rs.init()
mod.Resources[n.Name] = rs mod.Resources[resourceName] = rs
} }
rs.Type = n.ResourceType rs.Type = resourceType
rs.Dependencies = n.Dependencies rs.Dependencies = dependencies
if n.Tainted != nil && *n.Tainted { if err := writerFn(rs); err != nil {
if n.TaintedIndex != -1 { return nil, err
rs.Tainted[n.TaintedIndex] = *n.State
} else {
rs.Tainted = append(rs.Tainted, *n.State)
}
if n.TaintedClearPrimary {
rs.Primary = nil
}
} else {
// Set the primary state
rs.Primary = *n.State
} }
return nil, nil return nil, nil
} }
// EvalClearPrimaryState is an EvalNode implementation that clears the primary
// instance from a resource state.
type EvalClearPrimaryState struct {
Name string
}
func (n *EvalClearPrimaryState) Eval(ctx EvalContext) (interface{}, error) {
state, lock := ctx.State()
// Get a read lock so we can access this instance
lock.RLock()
defer lock.RUnlock()
// Look for the module state. If we don't have one, then it doesn't matter.
mod := state.ModuleByPath(ctx.Path())
if mod == nil {
return nil, nil
}
// Look for the resource state. If we don't have one, then it is okay.
rs := mod.Resources[n.Name]
if rs == nil {
return nil, nil
}
// Clear primary from the resource state
rs.Primary = nil
return nil, nil
}
// EvalDeposeState is an EvalNode implementation that takes the primary // EvalDeposeState is an EvalNode implementation that takes the primary
// out of a state and makes it tainted. This is done at the beggining of // out of a state and makes it Deposed. This is done at the beginning of
// create-before-destroy calls so that the create can create while preserving // create-before-destroy calls so that the create can create while preserving
// the old state of the to-be-destroyed resource. // the old state of the to-be-destroyed resource.
type EvalDeposeState struct { type EvalDeposeState struct {
@ -188,8 +324,8 @@ func (n *EvalDeposeState) Eval(ctx EvalContext) (interface{}, error) {
return nil, nil return nil, nil
} }
// Depose to the tainted // Depose
rs.Tainted = append(rs.Tainted, rs.Primary) rs.Deposed = append(rs.Deposed, rs.Primary)
rs.Primary = nil rs.Primary = nil
return nil, nil return nil, nil
@ -221,15 +357,15 @@ func (n *EvalUndeposeState) Eval(ctx EvalContext) (interface{}, error) {
return nil, nil return nil, nil
} }
// If we don't have any tainted, then we don't have anything to do // If we don't have any desposed resource, then we don't have anything to do
if len(rs.Tainted) == 0 { if len(rs.Deposed) == 0 {
return nil, nil return nil, nil
} }
// Undepose to the tainted // Undepose
idx := len(rs.Tainted) - 1 idx := len(rs.Deposed) - 1
rs.Primary = rs.Tainted[idx] rs.Primary = rs.Deposed[idx]
rs.Tainted[idx] = nil rs.Deposed[idx] = nil
return nil, nil return nil, nil
} }

View File

@ -66,3 +66,163 @@ func TestEvalUpdateStateHook(t *testing.T) {
t.Fatalf("bad: %#v", mockHook.PostStateUpdateState) t.Fatalf("bad: %#v", mockHook.PostStateUpdateState)
} }
} }
func TestEvalReadState(t *testing.T) {
var output *InstanceState
cases := map[string]struct {
Resources map[string]*ResourceState
Node EvalNode
ExpectedInstanceId string
}{
"ReadState gets primary instance state": {
Resources: map[string]*ResourceState{
"aws_instance.bar": &ResourceState{
Primary: &InstanceState{
ID: "i-abc123",
},
},
},
Node: &EvalReadState{
Name: "aws_instance.bar",
Output: &output,
},
ExpectedInstanceId: "i-abc123",
},
"ReadStateTainted gets tainted instance": {
Resources: map[string]*ResourceState{
"aws_instance.bar": &ResourceState{
Tainted: []*InstanceState{
&InstanceState{ID: "i-abc123"},
},
},
},
Node: &EvalReadStateTainted{
Name: "aws_instance.bar",
Output: &output,
Index: 0,
},
ExpectedInstanceId: "i-abc123",
},
"ReadStateDeposed gets deposed instance": {
Resources: map[string]*ResourceState{
"aws_instance.bar": &ResourceState{
Deposed: []*InstanceState{
&InstanceState{ID: "i-abc123"},
},
},
},
Node: &EvalReadStateDeposed{
Name: "aws_instance.bar",
Output: &output,
Index: 0,
},
ExpectedInstanceId: "i-abc123",
},
}
for k, c := range cases {
ctx := new(MockEvalContext)
ctx.StateState = &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: c.Resources,
},
},
}
ctx.StateLock = new(sync.RWMutex)
ctx.PathPath = rootModulePath
result, err := c.Node.Eval(ctx)
if err != nil {
t.Fatalf("[%s] Got err: %#v", k, err)
}
expected := c.ExpectedInstanceId
if !(result != nil && result.(*InstanceState).ID == expected) {
t.Fatalf("[%s] Expected return with ID %#v, got: %#v", k, expected, result)
}
if !(output != nil && output.ID == expected) {
t.Fatalf("[%s] Expected output with ID %#v, got: %#v", k, expected, output)
}
output = nil
}
}
func TestEvalWriteState(t *testing.T) {
state := &State{}
ctx := new(MockEvalContext)
ctx.StateState = state
ctx.StateLock = new(sync.RWMutex)
ctx.PathPath = rootModulePath
is := &InstanceState{ID: "i-abc123"}
node := &EvalWriteState{
Name: "restype.resname",
ResourceType: "restype",
State: &is,
}
_, err := node.Eval(ctx)
if err != nil {
t.Fatalf("Got err: %#v", err)
}
checkStateString(t, state, `
restype.resname:
ID = i-abc123
`)
}
func TestEvalWriteStateTainted(t *testing.T) {
state := &State{}
ctx := new(MockEvalContext)
ctx.StateState = state
ctx.StateLock = new(sync.RWMutex)
ctx.PathPath = rootModulePath
is := &InstanceState{ID: "i-abc123"}
node := &EvalWriteStateTainted{
Name: "restype.resname",
ResourceType: "restype",
State: &is,
Index: -1,
}
_, err := node.Eval(ctx)
if err != nil {
t.Fatalf("Got err: %#v", err)
}
checkStateString(t, state, `
restype.resname: (1 tainted)
ID = <not created>
Tainted ID 1 = i-abc123
`)
}
func TestEvalWriteStateDeposed(t *testing.T) {
state := &State{}
ctx := new(MockEvalContext)
ctx.StateState = state
ctx.StateLock = new(sync.RWMutex)
ctx.PathPath = rootModulePath
is := &InstanceState{ID: "i-abc123"}
node := &EvalWriteStateDeposed{
Name: "restype.resname",
ResourceType: "restype",
State: &is,
Index: -1,
}
_, err := node.Eval(ctx)
if err != nil {
t.Fatalf("Got err: %#v", err)
}
checkStateString(t, state, `
restype.resname: (1 deposed)
ID = <not created>
Deposed ID 1 = i-abc123
`)
}

View File

@ -293,24 +293,16 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
View: n.Resource.Id(), View: n.Resource.Id(),
}) })
if n.Resource.Lifecycle.CreateBeforeDestroy { steps = append(steps, &DeposedTransformer{
// If we're only destroying tainted resources, then we only
// want to find tainted resources and destroy them here.
steps = append(steps, &TaintedTransformer{
State: state, State: state,
View: n.Resource.Id(), View: n.Resource.Id(),
Deposed: n.Resource.Lifecycle.CreateBeforeDestroy,
DeposedInclude: true,
}) })
}
case DestroyTainted: case DestroyTainted:
// If we're only destroying tainted resources, then we only // If we're only destroying tainted resources, then we only
// want to find tainted resources and destroy them here. // want to find tainted resources and destroy them here.
steps = append(steps, &TaintedTransformer{ steps = append(steps, &TaintedTransformer{
State: state, State: state,
View: n.Resource.Id(), View: n.Resource.Id(),
Deposed: n.Resource.Lifecycle.CreateBeforeDestroy,
DeposedInclude: false,
}) })
} }

View File

@ -498,7 +498,7 @@ func (m *ModuleState) prune() {
for k, v := range m.Resources { for k, v := range m.Resources {
v.prune() v.prune()
if (v.Primary == nil || v.Primary.ID == "") && len(v.Tainted) == 0 { if (v.Primary == nil || v.Primary.ID == "") && len(v.Tainted) == 0 && len(v.Deposed) == 0 {
delete(m.Resources, k) delete(m.Resources, k)
} }
} }
@ -548,7 +548,12 @@ func (m *ModuleState) String() string {
taintStr = fmt.Sprintf(" (%d tainted)", len(rs.Tainted)) taintStr = fmt.Sprintf(" (%d tainted)", len(rs.Tainted))
} }
buf.WriteString(fmt.Sprintf("%s:%s\n", k, taintStr)) deposedStr := ""
if len(rs.Deposed) > 0 {
deposedStr = fmt.Sprintf(" (%d deposed)", len(rs.Deposed))
}
buf.WriteString(fmt.Sprintf("%s:%s%s\n", k, taintStr, deposedStr))
buf.WriteString(fmt.Sprintf(" ID = %s\n", id)) buf.WriteString(fmt.Sprintf(" ID = %s\n", id))
var attributes map[string]string var attributes map[string]string
@ -574,6 +579,10 @@ func (m *ModuleState) String() string {
buf.WriteString(fmt.Sprintf(" Tainted ID %d = %s\n", idx+1, t.ID)) buf.WriteString(fmt.Sprintf(" Tainted ID %d = %s\n", idx+1, t.ID))
} }
for idx, t := range rs.Deposed {
buf.WriteString(fmt.Sprintf(" Deposed ID %d = %s\n", idx+1, t.ID))
}
if len(rs.Dependencies) > 0 { if len(rs.Dependencies) > 0 {
buf.WriteString(fmt.Sprintf("\n Dependencies:\n")) buf.WriteString(fmt.Sprintf("\n Dependencies:\n"))
for _, dep := range rs.Dependencies { for _, dep := range rs.Dependencies {
@ -644,6 +653,16 @@ type ResourceState struct {
// However, in pathological cases, it is possible for the number // However, in pathological cases, it is possible for the number
// of instances to accumulate. // of instances to accumulate.
Tainted []*InstanceState `json:"tainted,omitempty"` Tainted []*InstanceState `json:"tainted,omitempty"`
// Deposed is used in the mechanics of CreateBeforeDestroy: the existing
// Primary is Deposed to get it out of the way for the replacement Primary to
// be created by Apply. If the replacement Primary creates successfully, the
// Deposed instance is cleaned up. If there were problems creating the
// replacement, the instance remains in the Deposed list so it can be
// destroyed in a future run. Functionally, Deposed instances are very
// similar to Tainted instances in that Terraform is only tracking them in
// order to remember to destroy them.
Deposed []*InstanceState `json:"deposed,omitempty"`
} }
// Equal tests whether two ResourceStates are equal. // Equal tests whether two ResourceStates are equal.
@ -744,6 +763,12 @@ func (r *ResourceState) deepcopy() *ResourceState {
n.Tainted = append(n.Tainted, inst.deepcopy()) n.Tainted = append(n.Tainted, inst.deepcopy())
} }
} }
if r.Deposed != nil {
n.Deposed = make([]*InstanceState, 0, len(r.Deposed))
for _, inst := range r.Deposed {
n.Deposed = append(n.Deposed, inst.deepcopy())
}
}
return n return n
} }
@ -762,6 +787,19 @@ func (r *ResourceState) prune() {
} }
r.Tainted = r.Tainted[:n] r.Tainted = r.Tainted[:n]
n = len(r.Deposed)
for i := 0; i < n; i++ {
inst := r.Deposed[i]
if inst == nil || inst.ID == "" {
copy(r.Deposed[i:], r.Deposed[i+1:])
r.Deposed[n-1] = nil
n--
i--
}
}
r.Deposed = r.Deposed[:n]
} }
func (r *ResourceState) sort() { func (r *ResourceState) sort() {

View File

@ -443,9 +443,9 @@ aws_instance.bar:
` `
const testTerraformApplyErrorDestroyCreateBeforeDestroyStr = ` const testTerraformApplyErrorDestroyCreateBeforeDestroyStr = `
aws_instance.bar: (1 tainted) aws_instance.bar: (1 deposed)
ID = foo ID = foo
Tainted ID 1 = bar Deposed ID 1 = bar
` `
const testTerraformApplyErrorPartialStr = ` const testTerraformApplyErrorPartialStr = `

View File

@ -0,0 +1,8 @@
resource "aws_instance" "web" {
// require_new is a special attribute recognized by testDiffFn that forces
// a new resource on every apply
require_new = "yes"
lifecycle {
create_before_destroy = true
}
}

View File

@ -0,0 +1,153 @@
package terraform
import "fmt"
// DeposedTransformer is a GraphTransformer that adds deposed resources
// to the graph.
type DeposedTransformer struct {
// State is the global state. We'll automatically find the correct
// ModuleState based on the Graph.Path that is being transformed.
State *State
// View, if non-empty, is the ModuleState.View used around the state
// to find deposed resources.
View string
}
func (t *DeposedTransformer) Transform(g *Graph) error {
state := t.State.ModuleByPath(g.Path)
if state == nil {
// If there is no state for our module there can't be any deposed
// resources, since they live in the state.
return nil
}
// If we have a view, apply it now
if t.View != "" {
state = state.View(t.View)
}
// Go through all the resources in our state to look for deposed resources
for k, rs := range state.Resources {
// If we have no deposed resources, then move on
if len(rs.Deposed) == 0 {
continue
}
deposed := rs.Deposed
for i, _ := range deposed {
g.Add(&graphNodeDeposedResource{
Index: i,
ResourceName: k,
ResourceType: rs.Type,
})
}
}
return nil
}
// graphNodeDeposedResource is the graph vertex representing a deposed resource.
type graphNodeDeposedResource struct {
Index int
ResourceName string
ResourceType string
}
func (n *graphNodeDeposedResource) Name() string {
return fmt.Sprintf("%s (deposed #%d)", n.ResourceName, n.Index)
}
func (n *graphNodeDeposedResource) ProvidedBy() []string {
return []string{resourceProvider(n.ResourceName)}
}
// GraphNodeEvalable impl.
func (n *graphNodeDeposedResource) EvalTree() EvalNode {
var provider ResourceProvider
var state *InstanceState
seq := &EvalSequence{Nodes: make([]EvalNode, 0, 5)}
// Build instance info
info := &InstanceInfo{Id: n.ResourceName, Type: n.ResourceType}
seq.Nodes = append(seq.Nodes, &EvalInstanceInfo{Info: info})
// Refresh the resource
seq.Nodes = append(seq.Nodes, &EvalOpFilter{
Ops: []walkOperation{walkRefresh},
Node: &EvalSequence{
Nodes: []EvalNode{
&EvalGetProvider{
Name: n.ProvidedBy()[0],
Output: &provider,
},
&EvalReadStateDeposed{
Name: n.ResourceName,
Output: &state,
Index: n.Index,
},
&EvalRefresh{
Info: info,
Provider: &provider,
State: &state,
Output: &state,
},
&EvalWriteStateDeposed{
Name: n.ResourceName,
ResourceType: n.ResourceType,
State: &state,
Index: n.Index,
},
},
},
})
// Apply
var diff *InstanceDiff
var err error
seq.Nodes = append(seq.Nodes, &EvalOpFilter{
Ops: []walkOperation{walkApply},
Node: &EvalSequence{
Nodes: []EvalNode{
&EvalGetProvider{
Name: n.ProvidedBy()[0],
Output: &provider,
},
&EvalReadStateDeposed{
Name: n.ResourceName,
Output: &state,
Index: n.Index,
},
&EvalDiffDestroy{
Info: info,
State: &state,
Output: &diff,
},
&EvalApply{
Info: info,
State: &state,
Diff: &diff,
Provider: &provider,
Output: &state,
Error: &err,
},
// Always write the resource back to the state deposed... if it
// was successfully destroyed it will be pruned. If it was not, it will
// be caught on the next run.
&EvalWriteStateDeposed{
Name: n.ResourceName,
ResourceType: n.ResourceType,
State: &state,
Index: n.Index,
},
&EvalReturnError{
Error: &err,
},
&EvalUpdateStateHook{},
},
},
})
return seq
}

View File

@ -285,7 +285,7 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode {
diffApply.Destroy = false diffApply.Destroy = false
return true, nil return true, nil
}, },
Node: EvalNoop{}, Then: EvalNoop{},
}, },
&EvalIf{ &EvalIf{
@ -301,7 +301,7 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode {
return createBeforeDestroyEnabled, nil return createBeforeDestroyEnabled, nil
}, },
Node: &EvalDeposeState{ Then: &EvalDeposeState{
Name: n.stateId(), Name: n.stateId(),
}, },
}, },
@ -382,7 +382,7 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode {
failure := tainted || err != nil failure := tainted || err != nil
return createBeforeDestroyEnabled && failure, nil return createBeforeDestroyEnabled && failure, nil
}, },
Node: &EvalUndeposeState{ Then: &EvalUndeposeState{
Name: n.stateId(), Name: n.stateId(),
}, },
}, },
@ -395,14 +395,35 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode {
Diff: nil, Diff: nil,
}, },
&EvalWriteState{ &EvalIf{
If: func(ctx EvalContext) (bool, error) {
return tainted, nil
},
Then: &EvalSequence{
Nodes: []EvalNode{
&EvalWriteStateTainted{
Name: n.stateId(), Name: n.stateId(),
ResourceType: n.Resource.Type, ResourceType: n.Resource.Type,
Dependencies: n.DependentOn(), Dependencies: n.DependentOn(),
State: &state, State: &state,
Tainted: &tainted, Index: -1,
TaintedIndex: -1, },
TaintedClearPrimary: !n.Resource.Lifecycle.CreateBeforeDestroy, &EvalIf{
If: func(ctx EvalContext) (bool, error) {
return !n.Resource.Lifecycle.CreateBeforeDestroy, nil
},
Then: &EvalClearPrimaryState{
Name: n.stateId(),
},
},
},
},
Else: &EvalWriteState{
Name: n.stateId(),
ResourceType: n.Resource.Type,
Dependencies: n.DependentOn(),
State: &state,
},
}, },
&EvalApplyPost{ &EvalApplyPost{
Info: info, Info: info,
@ -480,18 +501,26 @@ func (n *graphNodeExpandedResourceDestroy) EvalTree() EvalNode {
return true, EvalEarlyExitError{} return true, EvalEarlyExitError{}
}, },
Node: EvalNoop{}, Then: EvalNoop{},
}, },
&EvalGetProvider{ &EvalGetProvider{
Name: n.ProvidedBy()[0], Name: n.ProvidedBy()[0],
Output: &provider, Output: &provider,
}, },
&EvalReadState{ &EvalIf{
If: func(ctx EvalContext) (bool, error) {
return n.Resource.Lifecycle.CreateBeforeDestroy, nil
},
Then: &EvalReadStateTainted{
Name: n.stateId(), Name: n.stateId(),
Output: &state, Output: &state,
Tainted: n.Resource.Lifecycle.CreateBeforeDestroy, Index: -1,
TaintedIndex: -1, },
Else: &EvalReadState{
Name: n.stateId(),
Output: &state,
},
}, },
&EvalRequireState{ &EvalRequireState{
State: &state, State: &state,

View File

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
) )
// TraintedTransformer is a GraphTransformer that adds tainted resources // TaintedTransformer is a GraphTransformer that adds tainted resources
// to the graph. // to the graph.
type TaintedTransformer struct { type TaintedTransformer struct {
// State is the global state. We'll automatically find the correct // State is the global state. We'll automatically find the correct
@ -14,12 +14,6 @@ type TaintedTransformer struct {
// View, if non-empty, is the ModuleState.View used around the state // View, if non-empty, is the ModuleState.View used around the state
// to find tainted resources. // to find tainted resources.
View string View string
// Deposed, if set to true, assumes that the last tainted index
// represents a "deposed" resource, or a resource that was previously
// a primary but is now tainted since it is demoted.
Deposed bool
DeposedInclude bool
} }
func (t *TaintedTransformer) Transform(g *Graph) error { func (t *TaintedTransformer) Transform(g *Graph) error {
@ -43,17 +37,6 @@ func (t *TaintedTransformer) Transform(g *Graph) error {
} }
tainted := rs.Tainted tainted := rs.Tainted
// If we expect a deposed resource, then shuffle a bit
if t.Deposed {
if t.DeposedInclude {
// Only include the deposed resource
tainted = rs.Tainted[len(rs.Tainted)-1:]
} else {
// Exclude the deposed resource
tainted = rs.Tainted[:len(rs.Tainted)-1]
}
}
for i, _ := range tainted { for i, _ := range tainted {
// Add the graph node and make the connection from any untainted // Add the graph node and make the connection from any untainted
// resources with this name to the tainted resource, so that // resources with this name to the tainted resource, so that
@ -88,7 +71,6 @@ func (n *graphNodeTaintedResource) ProvidedBy() []string {
func (n *graphNodeTaintedResource) EvalTree() EvalNode { func (n *graphNodeTaintedResource) EvalTree() EvalNode {
var provider ResourceProvider var provider ResourceProvider
var state *InstanceState var state *InstanceState
tainted := true
seq := &EvalSequence{Nodes: make([]EvalNode, 0, 5)} seq := &EvalSequence{Nodes: make([]EvalNode, 0, 5)}
@ -105,10 +87,9 @@ func (n *graphNodeTaintedResource) EvalTree() EvalNode {
Name: n.ProvidedBy()[0], Name: n.ProvidedBy()[0],
Output: &provider, Output: &provider,
}, },
&EvalReadState{ &EvalReadStateTainted{
Name: n.ResourceName, Name: n.ResourceName,
Tainted: true, Index: n.Index,
TaintedIndex: n.Index,
Output: &state, Output: &state,
}, },
&EvalRefresh{ &EvalRefresh{
@ -117,12 +98,11 @@ func (n *graphNodeTaintedResource) EvalTree() EvalNode {
State: &state, State: &state,
Output: &state, Output: &state,
}, },
&EvalWriteState{ &EvalWriteStateTainted{
Name: n.ResourceName, Name: n.ResourceName,
ResourceType: n.ResourceType, ResourceType: n.ResourceType,
State: &state, State: &state,
Tainted: &tainted, Index: n.Index,
TaintedIndex: n.Index,
}, },
}, },
}, },
@ -138,10 +118,9 @@ func (n *graphNodeTaintedResource) EvalTree() EvalNode {
Name: n.ProvidedBy()[0], Name: n.ProvidedBy()[0],
Output: &provider, Output: &provider,
}, },
&EvalReadState{ &EvalReadStateTainted{
Name: n.ResourceName, Name: n.ResourceName,
Tainted: true, Index: n.Index,
TaintedIndex: n.Index,
Output: &state, Output: &state,
}, },
&EvalDiffDestroy{ &EvalDiffDestroy{
@ -156,12 +135,11 @@ func (n *graphNodeTaintedResource) EvalTree() EvalNode {
Provider: &provider, Provider: &provider,
Output: &state, Output: &state,
}, },
&EvalWriteState{ &EvalWriteStateTainted{
Name: n.ResourceName, Name: n.ResourceName,
ResourceType: n.ResourceType, ResourceType: n.ResourceType,
State: &state, State: &state,
Tainted: &tainted, Index: n.Index,
TaintedIndex: n.Index,
}, },
&EvalUpdateStateHook{}, &EvalUpdateStateHook{},
}, },

View File

@ -31,11 +31,10 @@ Available commands are:
init Initializes Terraform configuration from a module init Initializes Terraform configuration from a module
output Read an output from a state file output Read an output from a state file
plan Generate and show an execution plan plan Generate and show an execution plan
pull Refreshes the local state copy from the remote server
push Uploads the the local state to the remote server
refresh Update local state file against real resources refresh Update local state file against real resources
remote Configures remote state management remote Configure remote state storage
show Inspect Terraform state or plan show Inspect Terraform state or plan
taint Manually mark a resource for recreation
version Prints the Terraform version version Prints the Terraform version
``` ```

View File

@ -1,23 +0,0 @@
---
layout: "docs"
page_title: "Command: pull"
sidebar_current: "docs-commands-pull"
description: |-
The `terraform pull` refreshes the cached state file from the
remote server when remote state storage is enabled.
---
# Command: pull
The `terraform pull` refreshes the cached state file from the
remote server when remote state storage is enabled. The [`remote`
command](/docs/commands/remote.html) should be used to enable
remote state storage.
## Usage
Usage: `terraform pull`
The `pull` command is invoked without options to refresh the
cache copy of the state.

View File

@ -1,27 +0,0 @@
---
layout: "docs"
page_title: "Command: push"
sidebar_current: "docs-commands-push"
description: |-
The `terraform push` command is used to push a cached local copy
of the state to a remote storage server.
---
# Command: push
The `terraform push` uploads the cached state file to the
remote server when remote state storage is enabled. The [`remote`
command](/docs/commands/remote.html) should be used to enable
remote state storage.
Uploading is typically done automatically when running a Terraform
command that modifies state, but this can be used to retry uploads
if a transient failure occurs.
## Usage
Usage: `terraform push`
The `push` command is invoked without options to upload the
local cached state to the remote storage server.

View File

@ -0,0 +1,81 @@
---
layout: "docs"
page_title: "Command: remote config"
sidebar_current: "docs-commands-remote-config"
description: |-
The `terraform remote config` command is used to configure Terraform to make
use of remote state storage, change remote storage configuration, or
to disable it.
---
# Command: remote config
The `terraform remote config` command is used to configure use of remote
state storage. By default, Terraform persists its state only to a local
disk. When remote state storage is enabled, Terraform will automatically
fetch the latest state from the remote server when necessary and if any
updates are made, the newest state is persisted back to the remote server.
In this mode, users do not need to durably store the state using version
control or shared storaged.
## Usage
Usage: `terraform remote config [options]`
The `remote config` command can be used to enable remote storage, change
configuration or disable the use of remote storage. Terraform supports multiple types
of storage backends, specified by using the `-backend` flag. By default,
Atlas is assumed to be the storage backend. Each backend expects different,
configuration arguments documented below.
When remote storage is enabled, an existing local state file can be migrated.
By default, `remote config` will look for the "terraform.tfstate" file, but that
can be specified by the `-state` flag. If no state file exists, a blank
state will be configured.
When remote storage is disabled, the existing remote state is migrated
to a local file. This defaults to the `-state` path during restore.
The following backends are supported:
* Atlas - Stores the state in Atlas. Requires the `-name` and `-access-token` flag.
The `-address` flag can optionally be provided.
* Consul - Stores the state in the KV store at a given path.
Requires the `path` flag. The `-address` and `-access-token`
flag can optionally be provided. Address is assumed to be the
local agent if not provided.
* HTTP - Stores the state using a simple REST client. State will be fetched
via GET, updated via POST, and purged with DELETE. Requires the `-address` flag.
The command-line flags are all optional. The list of available flags are:
* `-address=url` - URL of the remote storage server. Required for HTTP backend,
optional for Atlas and Consul.
* `-access-token=token` - Authentication token for state storage server.
Required for Atlas backend, optional for Consul.
* `-backend=Atlas` - Specifies the type of remote backend. Must be one
of Atlas, Consul, or HTTP. Defaults to Atlas.
* `-backup=path` - Path to backup the existing state file before
modifying. Defaults to the "-state" path with ".backup" extension.
Set to "-" to disable backup.
* `-disable` - Disables remote state management and migrates the state
to the `-state` path.
* `-name=name` - Name of the state file in the state storage server.
Required for Atlas backend.
* `-path=path` - Path of the remote state in Consul. Required for the
Consul backend.
* `-pull=true` - Controls if the remote state is pulled before disabling.
This defaults to true to ensure the latest state is cached before disabling.
* `-state=path` - Path to read state. Defaults to "terraform.tfstate"
unless remote state is enabled.

View File

@ -0,0 +1,23 @@
---
layout: "docs"
page_title: "Command: remote pull"
sidebar_current: "docs-commands-remote-pull"
description: |-
The `terraform remote pull` refreshes the cached state file from the
remote server when remote state storage is enabled.
---
# Command: remote pull
The `terraform remote pull` refreshes the cached state file from the
remote server when remote state storage is enabled. The [`remote config`
command](/docs/commands/remote-config.html) should be used to enable
remote state storage.
## Usage
Usage: `terraform remote pull`
The `remote pull` command is invoked without options to refresh the
cache copy of the state.

View File

@ -0,0 +1,27 @@
---
layout: "docs"
page_title: "Command: remote push"
sidebar_current: "docs-commands-remote-push"
description: |-
The `terraform remote push` command is used to push a cached local copy
of the state to a remote storage server.
---
# Command: remote push
The `terraform remote push` uploads the cached state file to the
remote server when remote state storage is enabled. The [`remote config`
command](/docs/commands/remote-config.html) should be used to enable
remote state storage.
Uploading is typically done automatically when running a Terraform
command that modifies state, but this can be used to retry uploads
if a transient failure occurs.
## Usage
Usage: `terraform remote push`
The `remote push` command is invoked without options to upload the
local cached state to the remote storage server.

View File

@ -10,72 +10,24 @@ description: |-
# Command: remote # Command: remote
The `terraform remote` command is used to configure use of remote The `terraform remote` command is used to configure all aspects of
state storage. By default, Terraform persists its state only to a local remote state storage. When remote state storage is enabled,
disk. When remote state storage is enabled, Terraform will automatically Terraform will automatically fetch the latest state from the remote
fetch the latest state from the remote server when necessary and if any server when necessary and if any updates are made, the newest state
updates are made, the newest state is persisted back to the remote server. is persisted back to the remote server.
In this mode, users do not need to durably store the state using version In this mode, users do not need to durably store the state using version
control or shared storaged. control or shared storaged.
## Usage ## Usage
Usage: `terraform remote [options]` Usage: `terraform remote SUBCOMMAND [options]`
The `remote` command can be used to enable remote storage, change configuration, The `remote` command behaves as another command that further has more
or disable the use of remote storage. Terraform supports multiple types subcommands. The subcommands available are:
of storage backends, specified by using the `-backend` flag. By default,
Atlas is assumed to be the storage backend. Each backend expects different,
configuration arguments documented below.
When remote storage is enabled, an existing local state file can be migrated.
By default, `remote` will look for the "terraform.tfstate" file, but that
can be specified by the `-state` flag. If no state file exists, a blank
state will be configured.
When remote storage is disabled, the existing remote state is migrated
to a local file. This defaults to the `-state` path during restore.
The following backends are supported:
* Atlas - Stores the state in Atlas. Requires the `-name` and `-access-token` flag.
The `-address` flag can optionally be provided.
* Consul - Stores the state in the KV store at a given path.
Requires the `path` flag. The `-address` and `-access-token`
flag can optionally be provided. Address is assumed to be the
local agent if not provided.
* HTTP - Stores the state using a simple REST client. State will be fetched
via GET, updated via POST, and purged with DELETE. Requires the `-address` flag.
The command-line flags are all optional. The list of available flags are:
* `-address=url` - URL of the remote storage server. Required for HTTP backend,
optional for Atlas and Consul.
* `-access-token=token` - Authentication token for state storage server.
Required for Atlas backend, optional for Consul.
* `-backend=Atlas` - Specifies the type of remote backend. Must be one
of Atlas, Consul, or HTTP. Defaults to Atlas.
* `-backup=path` - Path to backup the existing state file before
modifying. Defaults to the "-state" path with ".backup" extension.
Set to "-" to disable backup.
* `-disable` - Disables remote state management and migrates the state
to the `-state` path.
* `-name=name` - Name of the state file in the state storage server.
Required for Atlas backend.
* `-path=path` - Path of the remote state in Consul. Required for the
Consul backend.
* `-pull=true` - Controls if the remote state is pulled before disabling.
This defaults to true to ensure the latest state is cached before disabling.
* `-state=path` - Path to read state. Defaults to "terraform.tfstate"
unless remote state is enabled.
* [config](/docs/commands/remote-config.html) - Configure the remote storage,
including enabling/disabling it.
* [pull](/docs/commands/remote-pull.html) - Sync the remote storage to
the local storage (download).
* [push](/docs/commands/remote-push.html) - Sync the local storage to
remote storage (upload).

View File

@ -80,6 +80,12 @@ The supported built-in functions are:
in this file are _not_ interpolated. The contents of the file are in this file are _not_ interpolated. The contents of the file are
read as-is. read as-is.
* `format(format, args...)` - Formats a string according to the given
format. The syntax for the format is standard `sprintf` syntax.
Good documentation for the syntax can be [found here](http://golang.org/pkg/fmt/).
Example to zero-prefix a count, used commonly for naming servers:
`format("web-%03d", count.index+1)`.
* `join(delim, list)` - Joins the list with the delimiter. A list is * `join(delim, list)` - Joins the list with the delimiter. A list is
only possible with splat variables from resources with a count only possible with splat variables from resources with a count
greater than one. Example: `join(",", aws_instance.foo.*.id)` greater than one. Example: `join(",", aws_instance.foo.*.id)`

View File

@ -37,6 +37,10 @@ module "child" {
This will work. You've created your first module! You can add resources This will work. You've created your first module! You can add resources
to the child module to see how that interaction works. to the child module to see how that interaction works.
Note: Prior to running the above, you'll have to run
[the get command](/docs/commands/get.html) for Terraform to sync
your modules. This should be instant since the module is just a local path.
## Inputs/Outputs ## Inputs/Outputs
To make modules more useful than simple isolated containers of Terraform To make modules more useful than simple isolated containers of Terraform

View File

@ -101,8 +101,7 @@ you'll have to make. The argument should be a structure implementing
one of the plugin interfaces (depending on what sort of plugin one of the plugin interfaces (depending on what sort of plugin
you're creating). you're creating).
While its not strictly necessary, Terraform plugins follow specific Terraform plugins must follow a very specific naming convention of
naming conventions. The format of the plugin binaries are `terraform-TYPE-NAME`. For example, `terraform-provider-aws`, which
`terraform-TYPE-NAME`. For example, `terraform-provider-aws`. tells Terraform that the plugin is a provider that can be referenced
We recommend you follow this convention to help make it clear what as "aws".
your plugin does to users.

View File

@ -32,7 +32,7 @@ resource "aws_instance" "web" {
## Argument Reference ## Argument Reference
The following arguments are supported: The following arguments are supported in the `provider` block:
* `access_key` - (Required) This is the AWS access key. It must be provided, but * `access_key` - (Required) This is the AWS access key. It must be provided, but
it can also be sourced from the `AWS_ACCESS_KEY_ID` environment variable. it can also be sourced from the `AWS_ACCESS_KEY_ID` environment variable.
@ -42,3 +42,6 @@ The following arguments are supported:
* `region` - (Required) This is the AWS region. It must be provided, but * `region` - (Required) This is the AWS region. It must be provided, but
it can also be sourced from the `AWS_DEFAULT_REGION` environment variables. it can also be sourced from the `AWS_DEFAULT_REGION` environment variables.
In addition to the above parameters, the `AWS_SECURITY_TOKEN` environmental
variable can be set to set an MFA token.

View File

@ -58,6 +58,8 @@ Each `block_device` supports the following:
* `snapshot_id` - (Optional) The Snapshot ID to mount. * `snapshot_id` - (Optional) The Snapshot ID to mount.
* `volume_type` - (Optional) The type of volume. Can be standard, gp2, or io1. Defaults to standard. * `volume_type` - (Optional) The type of volume. Can be standard, gp2, or io1. Defaults to standard.
* `volume_size` - (Optional) The size of the volume in gigabytes. * `volume_size` - (Optional) The size of the volume in gigabytes.
* `iops` - (Optional) The amount of provisioned IOPS. Setting this implies a
volume_type of "io1".
* `delete_on_termination` - (Optional) Should the volume be destroyed on instance termination (defaults true). * `delete_on_termination` - (Optional) Should the volume be destroyed on instance termination (defaults true).
* `encrypted` - (Optional) Should encryption be enabled (defaults false). * `encrypted` - (Optional) Should encryption be enabled (defaults false).
@ -68,6 +70,8 @@ The `root_block_device` mapping supports the following:
is the typical root volume for Linux instances. is the typical root volume for Linux instances.
* `volume_type` - (Optional) The type of volume. Can be standard, gp2, or io1. Defaults to standard. * `volume_type` - (Optional) The type of volume. Can be standard, gp2, or io1. Defaults to standard.
* `volume_size` - (Optional) The size of the volume in gigabytes. * `volume_size` - (Optional) The size of the volume in gigabytes.
* `iops` - (Optional) The amount of provisioned IOPS. Setting this implies a
volume_type of "io1".
* `delete_on_termination` - (Optional) Should the volume be destroyed on instance termination (defaults true). * `delete_on_termination` - (Optional) Should the volume be destroyed on instance termination (defaults true).
## Attributes Reference ## Attributes Reference

View File

@ -22,14 +22,14 @@ resource "aws_security_group" "allow_all" {
ingress { ingress {
from_port = 0 from_port = 0
to_port = 65535 to_port = 65535
protocol = "tcp" protocol = "-1"
cidr_blocks = ["0.0.0.0/0"] cidr_blocks = ["0.0.0.0/0"]
} }
egress { egress {
from_port = 0 from_port = 0
to_port = 65535 to_port = 65535
protocol = "tcp" protocol = "-1"
cidr_blocks = ["0.0.0.0/0"] cidr_blocks = ["0.0.0.0/0"]
} }
} }

View File

@ -32,6 +32,10 @@ The following arguments are supported:
* `ipaddress` - (Required) The IP address for which to create the port forwards. * `ipaddress` - (Required) The IP address for which to create the port forwards.
Changing this forces a new resource to be created. Changing this forces a new resource to be created.
* `managed` - (Optional) USE WITH CAUTION! If enabled all the port forwards for
this IP address will be managed by this resource. This means it will delete
all port forwards that are not in your config! (defaults false)
* `forward` - (Required) Can be specified multiple times. Each forward block supports * `forward` - (Required) Can be specified multiple times. Each forward block supports
fields documented below. fields documented below.

View File

@ -95,6 +95,10 @@
<li<%= sidebar_current("docs-aws-resource-vpc") %>> <li<%= sidebar_current("docs-aws-resource-vpc") %>>
<a href="/docs/providers/aws/r/vpc.html">aws_vpc</a> <a href="/docs/providers/aws/r/vpc.html">aws_vpc</a>
</li> </li>
<li<%= sidebar_current("docs-aws-resource-vpc-peering") %>>
<a href="/docs/providers/aws/r/vpc_peering.html">aws_vpc_peering</a>
</li>
</ul> </ul>
</li> </li>
</ul> </ul>

View File

@ -79,14 +79,6 @@
<a href="/docs/commands/plan.html">plan</a> <a href="/docs/commands/plan.html">plan</a>
</li> </li>
<li<%= sidebar_current("docs-commands-pull") %>>
<a href="/docs/commands/pull.html">pull</a>
</li>
<li<%= sidebar_current("docs-commands-push") %>>
<a href="/docs/commands/push.html">push</a>
</li>
<li<%= sidebar_current("docs-commands-refresh") %>> <li<%= sidebar_current("docs-commands-refresh") %>>
<a href="/docs/commands/refresh.html">refresh</a> <a href="/docs/commands/refresh.html">refresh</a>
</li> </li>