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:
- go test ./...
- make vet
#- go test -race ./...
branches:

View File

@ -1,5 +1,12 @@
## 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:
* **New provider: `dme` (DNSMadeEasy)** [GH-855]
@ -16,6 +23,8 @@ FEATURES:
IMPROVEMENTS:
* **New config function: `format`** - Format a string using `sprintf`
format. [GH-1096]
* **New config function: `replace`** - Search and replace string values.
Search can be a regular expression. See documentation for more
info. [GH-1029]
@ -39,9 +48,15 @@ BUG FIXES:
"resource.0" would ignore the latter completely. [GH-1086]
* providers/aws: manually deleted VPC removes it from the state
* 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: 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)

View File

@ -11,12 +11,13 @@ import (
"github.com/mitchellh/goamz/ec2"
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/elb"
"github.com/hashicorp/aws-sdk-go/gen/rds"
"github.com/hashicorp/aws-sdk-go/gen/route53"
"github.com/hashicorp/aws-sdk-go/gen/s3"
awsEC2 "github.com/hashicorp/aws-sdk-go/gen/ec2"
)
type Config struct {
@ -28,7 +29,7 @@ type Config struct {
type AWSClient struct {
ec2conn *ec2.EC2
ec2conn2 *awsec2.EC2
awsEC2conn *awsEC2.EC2
elbconn *elb.ELB
autoscalingconn *autoscaling.AutoScaling
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
log.Println("[INFO] Initializing Route53 connection")
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 {

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("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("status", *v.DBInstanceStatus)
d.Set("storage_encrypted", *v.StorageEncrypted)

View File

@ -6,9 +6,10 @@ import (
"strings"
"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/schema"
"github.com/mitchellh/goamz/ec2"
)
func resourceAwsEip() *schema.Resource {
@ -59,7 +60,7 @@ func resourceAwsEip() *schema.Resource {
}
func resourceAwsEipCreate(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
ec2conn := meta.(*AWSClient).awsEC2conn
// By default, we're not in a VPC
domainOpt := ""
@ -67,12 +68,12 @@ func resourceAwsEipCreate(d *schema.ResourceData, meta interface{}) error {
domainOpt = "vpc"
}
allocOpts := ec2.AllocateAddress{
Domain: domainOpt,
allocOpts := &ec2.AllocateAddressRequest{
Domain: aws.String(domainOpt),
}
log.Printf("[DEBUG] EIP create configuration: %#v", allocOpts)
allocResp, err := ec2conn.AllocateAddress(&allocOpts)
allocResp, err := ec2conn.AllocateAddress(allocOpts)
if err != nil {
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
log.Printf("[DEBUG] EIP Allocate: %#v", allocResp)
if d.Get("domain").(string) == "vpc" {
d.SetId(allocResp.AllocationId)
d.SetId(*allocResp.AllocationID)
} 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)
}
func resourceAwsEipRead(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
ec2conn := meta.(*AWSClient).awsEC2conn
domain := resourceAwsEipDomain(d)
id := d.Id()
@ -113,9 +114,13 @@ func resourceAwsEipRead(d *schema.ResourceData, meta interface{}) error {
"[DEBUG] EIP describe configuration: %#v, %#v (domain: %s)",
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 ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidAllocationID.NotFound" {
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidAllocationID.NotFound" {
d.SetId("")
return nil
}
@ -125,8 +130,8 @@ func resourceAwsEipRead(d *schema.ResourceData, meta interface{}) error {
// Verify AWS returned our EIP
if len(describeAddresses.Addresses) != 1 ||
describeAddresses.Addresses[0].AllocationId != id ||
describeAddresses.Addresses[0].PublicIp != id {
*describeAddresses.Addresses[0].AllocationID != id ||
*describeAddresses.Addresses[0].PublicIP != id {
if err != nil {
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]
d.Set("association_id", address.AssociationId)
d.Set("instance", address.InstanceId)
d.Set("public_ip", address.PublicIp)
d.Set("private_ip", address.PrivateIpAddress)
d.Set("association_id", address.AssociationID)
d.Set("instance", address.InstanceID)
d.Set("private_ip", address.PrivateIPAddress)
d.Set("public_ip", address.PublicIP)
return nil
}
func resourceAwsEipUpdate(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
ec2conn := meta.(*AWSClient).awsEC2conn
domain := resourceAwsEipDomain(d)
@ -151,22 +156,22 @@ func resourceAwsEipUpdate(d *schema.ResourceData, meta interface{}) error {
if v, ok := d.GetOk("instance"); ok {
instanceId := v.(string)
assocOpts := ec2.AssociateAddress{
InstanceId: instanceId,
PublicIp: d.Id(),
assocOpts := &ec2.AssociateAddressRequest{
InstanceID: aws.String(instanceId),
PublicIP: aws.String(d.Id()),
}
// more unique ID conditionals
if domain == "vpc" {
assocOpts = ec2.AssociateAddress{
InstanceId: instanceId,
AllocationId: d.Id(),
PublicIp: "",
assocOpts = &ec2.AssociateAddressRequest{
InstanceID: aws.String(instanceId),
AllocationID: aws.String(d.Id()),
PublicIP: aws.String(""),
}
}
log.Printf("[DEBUG] EIP associate configuration: %#v (domain: %v)", assocOpts, domain)
_, err := ec2conn.AssociateAddress(&assocOpts)
_, err := ec2conn.AssociateAddress(assocOpts)
if err != nil {
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 {
ec2conn := meta.(*AWSClient).ec2conn
ec2conn := meta.(*AWSClient).awsEC2conn
if err := resourceAwsEipRead(d, meta); err != nil {
return err
@ -192,9 +197,13 @@ func resourceAwsEipDelete(d *schema.ResourceData, meta interface{}) error {
var err error
switch resourceAwsEipDomain(d) {
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":
_, err = ec2conn.DisassociateAddressClassic(d.Get("public_ip").(string))
err = ec2conn.DisassociateAddress(&ec2.DisassociateAddressRequest{
PublicIP: aws.String(d.Get("public_ip").(string)),
})
}
if err != nil {
return err
@ -209,16 +218,20 @@ func resourceAwsEipDelete(d *schema.ResourceData, meta interface{}) error {
log.Printf(
"[DEBUG] EIP release (destroy) address allocation: %v",
d.Id())
_, err = ec2conn.ReleaseAddress(d.Id())
err = ec2conn.ReleaseAddress(&ec2.ReleaseAddressRequest{
AllocationID: aws.String(d.Id()),
})
case "standard":
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 {
return nil
}
if _, ok := err.(*ec2.Error); !ok {
if _, ok := err.(aws.APIError); !ok {
return resource.RetryError{Err: err}
}

View File

@ -5,9 +5,10 @@ import (
"strings"
"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/terraform"
"github.com/mitchellh/goamz/ec2"
)
func TestAccAWSEIP_normal(t *testing.T) {
@ -57,24 +58,28 @@ func TestAccAWSEIP_instance(t *testing.T) {
}
func testAccCheckAWSEIPDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn
conn := testAccProvider.Meta().(*AWSClient).awsEC2conn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_eip" {
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 len(describe.Addresses) != 0 &&
describe.Addresses[0].PublicIp == rs.Primary.ID {
*describe.Addresses[0].PublicIP == rs.Primary.ID {
return fmt.Errorf("EIP still exists")
}
}
// Verify the error
providerErr, ok := err.(*ec2.Error)
providerErr, ok := err.(aws.APIError)
if !ok {
return err
}
@ -89,7 +94,7 @@ func testAccCheckAWSEIPDestroy(s *terraform.State) error {
func testAccCheckAWSEIPAttributes(conf *ec2.Address) resource.TestCheckFunc {
return func(s *terraform.State) error {
if conf.PublicIp == "" {
if *conf.PublicIP == "" {
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")
}
conn := testAccProvider.Meta().(*AWSClient).ec2conn
conn := testAccProvider.Meta().(*AWSClient).awsEC2conn
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 {
return err
}
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")
}
*res = describe.Addresses[0]
} 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 {
return err
}
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")
}
*res = describe.Addresses[0]

View File

@ -186,6 +186,13 @@ func resourceAwsInstance() *schema.Resource {
Computed: true,
ForceNew: true,
},
"iops": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},
},
},
Set: resourceAwsInstanceBlockDevicesHash,
@ -231,6 +238,13 @@ func resourceAwsInstance() *schema.Resource {
Computed: 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 {
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["encrypted"] = vol.Encrypted
blockDevice["iops"] = vol.IOPS
nonRootBlockDevices = append(nonRootBlockDevices, blockDevice)
}
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")
}
// Check if the third block device exists.
if _, ok := blockDevices["/dev/sdc"]; !ok {
fmt.Errorf("block device doesn't exist: /dev/sdc")
}
return nil
}
}
@ -114,11 +119,22 @@ func TestAccAWSInstance_blockDevices(t *testing.T) {
resource.TestCheckResourceAttr(
"aws_instance.foo", "root_block_device.0.volume_type", "gp2"),
resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.#", "1"),
"aws_instance.foo", "block_device.#", "2"),
resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.172787947.device_name", "/dev/sdb"),
resource.TestCheckResourceAttr(
"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(),
),
},
@ -391,6 +407,12 @@ resource "aws_instance" "foo" {
device_name = "/dev/sdb"
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"
"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/schema"
"github.com/mitchellh/goamz/ec2"
)
func resourceAwsInternetGateway() *schema.Resource {
@ -28,7 +29,7 @@ func resourceAwsInternetGateway() *schema.Resource {
}
func resourceAwsInternetGatewayCreate(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
ec2conn := meta.(*AWSClient).awsEC2conn
// Create the gateway
log.Printf("[DEBUG] Creating internet gateway")
@ -38,8 +39,8 @@ func resourceAwsInternetGatewayCreate(d *schema.ResourceData, meta interface{})
}
// Get the ID and store it
ig := &resp.InternetGateway
d.SetId(ig.InternetGatewayId)
ig := resp.InternetGateway
d.SetId(*ig.InternetGatewayID)
log.Printf("[INFO] InternetGateway ID: %s", d.Id())
// 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 {
ec2conn := meta.(*AWSClient).ec2conn
ec2conn := meta.(*AWSClient).awsEC2conn
igRaw, _, err := IGStateRefreshFunc(ec2conn, d.Id())()
if err != nil {
@ -64,10 +65,10 @@ func resourceAwsInternetGatewayRead(d *schema.ResourceData, meta interface{}) er
// Gateway exists but not attached to the VPC
d.Set("vpc_id", "")
} 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
}
@ -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
}
@ -97,7 +98,7 @@ func resourceAwsInternetGatewayUpdate(d *schema.ResourceData, meta interface{})
}
func resourceAwsInternetGatewayDelete(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
ec2conn := meta.(*AWSClient).awsEC2conn
// Detach if it is attached
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())
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 {
return nil
}
ec2err, ok := err.(*ec2.Error)
ec2err, ok := err.(aws.APIError)
if !ok {
return err
}
@ -129,7 +132,7 @@ func resourceAwsInternetGatewayDelete(d *schema.ResourceData, meta interface{})
}
func resourceAwsInternetGatewayAttach(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
ec2conn := meta.(*AWSClient).awsEC2conn
if d.Get("vpc_id").(string) == "" {
log.Printf(
@ -143,7 +146,10 @@ func resourceAwsInternetGatewayAttach(d *schema.ResourceData, meta interface{})
d.Id(),
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 {
return err
}
@ -171,7 +177,7 @@ func resourceAwsInternetGatewayAttach(d *schema.ResourceData, meta interface{})
}
func resourceAwsInternetGatewayDetach(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
ec2conn := meta.(*AWSClient).awsEC2conn
// Get the old VPC ID to detach from
vpcID, _ := d.GetChange("vpc_id")
@ -189,9 +195,12 @@ func resourceAwsInternetGatewayDetach(d *schema.ResourceData, meta interface{})
vpcID.(string))
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 {
ec2err, ok := err.(*ec2.Error)
ec2err, ok := err.(aws.APIError)
if ok {
if ec2err.Code == "InvalidInternetGatewayID.NotFound" {
err = nil
@ -232,9 +241,11 @@ func resourceAwsInternetGatewayDetach(d *schema.ResourceData, meta interface{})
// an internet gateway.
func IGStateRefreshFunc(ec2conn *ec2.EC2, id string) resource.StateRefreshFunc {
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 {
ec2err, ok := err.(*ec2.Error)
ec2err, ok := err.(aws.APIError)
if ok && ec2err.Code == "InvalidInternetGatewayID.NotFound" {
resp = nil
} else {
@ -256,16 +267,18 @@ func IGStateRefreshFunc(ec2conn *ec2.EC2, id string) resource.StateRefreshFunc {
// IGAttachStateRefreshFunc returns a resource.StateRefreshFunc that is used
// 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
return func() (interface{}, string, error) {
if start.IsZero() {
start = time.Now()
}
resp, err := conn.DescribeInternetGateways([]string{id}, ec2.NewFilter())
resp, err := ec2conn.DescribeInternetGateways(&ec2.DescribeInternetGatewaysRequest{
InternetGatewayIDs: []string{id},
})
if err != nil {
ec2err, ok := err.(*ec2.Error)
ec2err, ok := err.(aws.APIError)
if ok && ec2err.Code == "InvalidInternetGatewayID.NotFound" {
resp = nil
} else {
@ -291,6 +304,6 @@ func IGAttachStateRefreshFunc(conn *ec2.EC2, id string, expected string) resourc
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"
"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/terraform"
"github.com/mitchellh/goamz/ec2"
)
func TestAccAWSInternetGateway(t *testing.T) {
@ -20,8 +21,8 @@ func TestAccAWSInternetGateway(t *testing.T) {
return fmt.Errorf("IG B is not attached")
}
id1 := v.Attachments[0].VpcId
id2 := v2.Attachments[0].VpcId
id1 := v.Attachments[0].VPCID
id2 := v2.Attachments[0].VPCID
if id1 == id2 {
return fmt.Errorf("Both attachment IDs are the same")
}
@ -104,8 +105,8 @@ func TestAccInternetGateway_tags(t *testing.T) {
Config: testAccCheckInternetGatewayConfigTagsUpdate,
Check: resource.ComposeTestCheckFunc(
testAccCheckInternetGatewayExists("aws_internet_gateway.foo", &v),
testAccCheckTags(&v.Tags, "foo", ""),
testAccCheckTags(&v.Tags, "bar", "baz"),
testAccCheckTagsSDK(&v.Tags, "foo", ""),
testAccCheckTagsSDK(&v.Tags, "bar", "baz"),
),
},
},
@ -113,7 +114,7 @@ func TestAccInternetGateway_tags(t *testing.T) {
}
func testAccCheckInternetGatewayDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn
ec2conn := testAccProvider.Meta().(*AWSClient).awsEC2conn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_internet_gateway" {
@ -121,8 +122,9 @@ func testAccCheckInternetGatewayDestroy(s *terraform.State) error {
}
// Try to find the resource
resp, err := conn.DescribeInternetGateways(
[]string{rs.Primary.ID}, ec2.NewFilter())
resp, err := ec2conn.DescribeInternetGateways(&ec2.DescribeInternetGatewaysRequest{
InternetGatewayIDs: []string{rs.Primary.ID},
})
if err == nil {
if len(resp.InternetGateways) > 0 {
return fmt.Errorf("still exists")
@ -132,7 +134,7 @@ func testAccCheckInternetGatewayDestroy(s *terraform.State) error {
}
// Verify the error is what we want
ec2err, ok := err.(*ec2.Error)
ec2err, ok := err.(aws.APIError)
if !ok {
return err
}
@ -155,9 +157,10 @@ func testAccCheckInternetGatewayExists(n string, ig *ec2.InternetGateway) resour
return fmt.Errorf("No ID is set")
}
conn := testAccProvider.Meta().(*AWSClient).ec2conn
resp, err := conn.DescribeInternetGateways(
[]string{rs.Primary.ID}, ec2.NewFilter())
ec2conn := testAccProvider.Meta().(*AWSClient).awsEC2conn
resp, err := ec2conn.DescribeInternetGateways(&ec2.DescribeInternetGatewaysRequest{
InternetGatewayIDs: []string{rs.Primary.ID},
})
if err != nil {
return err
}

View File

@ -1,9 +1,13 @@
package aws
import (
"encoding/base64"
"fmt"
"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 {
@ -33,42 +37,50 @@ func resourceAwsKeyPair() *schema.Resource {
}
func resourceAwsKeyPairCreate(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
ec2conn := meta.(*AWSClient).awsEC2conn
keyName := d.Get("key_name").(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 {
return fmt.Errorf("Error import KeyPair: %s", err)
}
d.SetId(resp.KeyName)
d.SetId(*resp.KeyName)
return nil
}
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 {
return fmt.Errorf("Error retrieving KeyPair: %s", err)
}
for _, keyPair := range resp.Keys {
if keyPair.Name == d.Id() {
d.Set("key_name", keyPair.Name)
d.Set("fingerprint", keyPair.Fingerprint)
for _, keyPair := range resp.KeyPairs {
if *keyPair.KeyName == d.Id() {
d.Set("key_name", keyPair.KeyName)
d.Set("fingerprint", keyPair.KeyFingerprint)
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 {
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
}

View File

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

View File

@ -5,9 +5,10 @@ import (
"log"
"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/schema"
"github.com/mitchellh/goamz/ec2"
)
func resourceAwsSubnet() *schema.Resource {
@ -50,12 +51,12 @@ func resourceAwsSubnet() *schema.Resource {
}
func resourceAwsSubnetCreate(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
ec2conn := meta.(*AWSClient).awsEC2conn
createOpts := &ec2.CreateSubnet{
AvailabilityZone: d.Get("availability_zone").(string),
CidrBlock: d.Get("cidr_block").(string),
VpcId: d.Get("vpc_id").(string),
createOpts := &ec2.CreateSubnetRequest{
AvailabilityZone: aws.String(d.Get("availability_zone").(string)),
CIDRBlock: aws.String(d.Get("cidr_block").(string)),
VPCID: aws.String(d.Get("vpc_id").(string)),
}
resp, err := ec2conn.CreateSubnet(createOpts)
@ -65,16 +66,16 @@ func resourceAwsSubnetCreate(d *schema.ResourceData, meta interface{}) error {
}
// Get the ID and store it
subnet := &resp.Subnet
d.SetId(subnet.SubnetId)
log.Printf("[INFO] Subnet ID: %s", subnet.SubnetId)
subnet := resp.Subnet
d.SetId(*subnet.SubnetID)
log.Printf("[INFO] Subnet ID: %s", *subnet.SubnetID)
// 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{
Pending: []string{"pending"},
Target: "available",
Refresh: SubnetStateRefreshFunc(ec2conn, subnet.SubnetId),
Refresh: SubnetStateRefreshFunc(ec2conn, *subnet.SubnetID),
Timeout: 10 * time.Minute,
}
@ -90,12 +91,14 @@ func resourceAwsSubnetCreate(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 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.
d.SetId("")
return nil
@ -108,35 +111,35 @@ func resourceAwsSubnetRead(d *schema.ResourceData, meta interface{}) error {
subnet := &resp.Subnets[0]
d.Set("vpc_id", subnet.VpcId)
d.Set("vpc_id", subnet.VPCID)
d.Set("availability_zone", subnet.AvailabilityZone)
d.Set("cidr_block", subnet.CidrBlock)
d.Set("map_public_ip_on_launch", subnet.MapPublicIpOnLaunch)
d.Set("tags", tagsToMap(subnet.Tags))
d.Set("cidr_block", subnet.CIDRBlock)
d.Set("map_public_ip_on_launch", subnet.MapPublicIPOnLaunch)
d.Set("tags", tagsToMapSDK(subnet.Tags))
return nil
}
func resourceAwsSubnetUpdate(d *schema.ResourceData, meta interface{}) error {
ec2conn := meta.(*AWSClient).ec2conn
ec2conn := meta.(*AWSClient).awsEC2conn
d.Partial(true)
if err := setTags(ec2conn, d); err != nil {
if err := setTagsSDK(ec2conn, d); err != nil {
return err
} else {
d.SetPartial("tags")
}
if d.HasChange("map_public_ip_on_launch") {
modifyOpts := &ec2.ModifySubnetAttribute{
SubnetId: d.Id(),
MapPublicIpOnLaunch: true,
modifyOpts := &ec2.ModifySubnetAttributeRequest{
SubnetID: aws.String(d.Id()),
MapPublicIPOnLaunch: &ec2.AttributeBooleanValue{aws.Boolean(true)},
}
log.Printf("[DEBUG] Subnet modify attributes: %#v", modifyOpts)
_, err := ec2conn.ModifySubnetAttribute(modifyOpts)
err := ec2conn.ModifySubnetAttribute(modifyOpts)
if err != nil {
return err
@ -151,11 +154,16 @@ func resourceAwsSubnetUpdate(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())
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" {
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.
func SubnetStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc {
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 ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidSubnetID.NotFound" {
if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidSubnetID.NotFound" {
resp = nil
} else {
log.Printf("Error on SubnetStateRefresh: %s", err)
@ -186,6 +196,6 @@ func SubnetStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc
}
subnet := &resp.Subnets[0]
return subnet, subnet.State, nil
return subnet, *subnet.State, nil
}
}

View File

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

View File

@ -26,6 +26,12 @@ func resourceCloudStackPortForward() *schema.Resource {
ForceNew: true,
},
"managed": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"forward": &schema.Schema{
Type: schema.TypeSet,
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
d.SetId(d.Get("ipaddress").(string))
d.SetId(ipaddressid)
// Create all forwards that are configured
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() {
// 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
forwards.Add(forward)
@ -101,7 +107,7 @@ func resourceCloudStackPortForwardCreate(d *schema.ResourceData, meta interface{
}
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)
// Make sure all required parameters are there
@ -110,14 +116,18 @@ func resourceCloudStackPortForwardCreateForward(
}
// Retrieve the virtual_machine UUID
virtualmachineid, e := retrieveUUID(cs, "virtual_machine", forward["virtual_machine"].(string))
if e != nil {
return e.Error()
vm, _, err := cs.VirtualMachine.GetVirtualMachineByName(forward["virtual_machine"].(string))
if err != nil {
return err
}
// Create a new parameter struct
p := cs.Firewall.NewCreatePortForwardingRuleParams(ipaddressid, forward["private_port"].(int),
forward["protocol"].(string), forward["public_port"].(int), virtualmachineid)
p := cs.Firewall.NewCreatePortForwardingRuleParams(d.Id(), forward["private_port"].(int),
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
p.SetOpenfirewall(false)
@ -154,7 +164,8 @@ func resourceCloudStackPortForwardRead(d *schema.ResourceData, meta interface{})
r, count, err := cs.Firewall.GetPortForwardingRuleByID(id.(string))
// If the count == 0, there is no object found for this UUID
if err != nil {
if count != 0 {
if count == 0 {
forward["uuid"] = ""
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 {
d.Set("forward", forwards)
} else {
} else if !managed {
d.SetId("")
}
@ -190,14 +244,6 @@ func resourceCloudStackPortForwardRead(d *schema.ResourceData, meta interface{})
}
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
if d.HasChange("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
for _, forward := range nrs.List() {
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
forwards.Add(forward)

View File

@ -483,14 +483,19 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error
d.Set("can_ip_forward", instance.CanIpForward)
// Set the service accounts
for i, serviceAccount := range instance.ServiceAccounts {
prefix := fmt.Sprintf("service_account.%d", i)
d.Set(prefix+".email", serviceAccount.Email)
d.Set(prefix+".scopes.#", len(serviceAccount.Scopes))
for j, scope := range serviceAccount.Scopes {
d.Set(fmt.Sprintf("%s.scopes.%d", prefix, j), scope)
serviceAccounts := make([]map[string]interface{}, 0, 1)
for _, serviceAccount := range instance.ServiceAccounts {
scopes := make([]string, len(serviceAccount.Scopes))
for i, scope := range serviceAccount.Scopes {
scopes[i] = scope
}
serviceAccounts = append(serviceAccounts, map[string]interface{}{
"email": serviceAccount.Email,
"scopes": scopes,
})
}
d.Set("service_account", serviceAccounts)
networksCount := d.Get("network.#").(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.
externalIP := ""
internalIP := ""
networks := make([]map[string]interface{}, 0, 1)
if networksCount > 0 {
// TODO: Remove this when realizing deprecation of .network
for i, iface := range instance.NetworkInterfaces {
prefix := fmt.Sprintf("network.%d", i)
d.Set(prefix+".name", iface.Name)
log.Printf(prefix+".name = %s", iface.Name)
for _, iface := range instance.NetworkInterfaces {
var natIP string
for _, config := range iface.AccessConfigs {
if config.Type == "ONE_TO_ONE_NAT" {
@ -524,23 +526,28 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error
if 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 {
for i, iface := range instance.NetworkInterfaces {
prefix := fmt.Sprintf("network_interface.%d", i)
d.Set(prefix+".name", iface.Name)
for _, iface := range instance.NetworkInterfaces {
// The first non-empty ip is left in natIP
var natIP string
for j, config := range iface.AccessConfigs {
acPrefix := fmt.Sprintf("%s.access_config.%d", prefix, j)
d.Set(acPrefix+".nat_ip", config.NatIP)
accessConfigs := make(
[]map[string]interface{}, 0, len(iface.AccessConfigs))
for _, config := range iface.AccessConfigs {
accessConfigs = append(accessConfigs, map[string]interface{}{
"nat_ip": config.NatIP,
})
if natIP == "" {
natIP = config.NatIP
}
@ -550,13 +557,18 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error
externalIP = natIP
}
d.Set(prefix+".address", iface.NetworkIP)
if internalIP == "" {
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
// 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
InfoColor string
ErrorColor string
WarnColor string
Ui cli.Ui
}
@ -33,6 +34,10 @@ func (u *ColorizeUi) Error(message string) {
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 {
if color == "" {
return message

View File

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

View File

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

View File

@ -1,339 +1,57 @@
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
}
// RemoteCommand is a Command implementation that is used to
// enable and disable remote state management
type RemoteCommand struct {
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)
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 {
if len(args) == 0 {
c.Ui.Error(c.Help())
return 1
}
// Show help if given no inputs
if !c.conf.disableRemote && c.remoteConf.Type == "atlas" && len(config) == 0 {
cmdFlags.Usage()
switch args[0] {
case "config":
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
}
// 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 {
helpText := `
Usage: terraform remote [options]
Usage: terraform remote <subcommand> [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.
Configure remote state storage with Terraform.
Options:
Available subcommands:
-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.
config Configure the remote storage settings.
pull Sync the remote storage by downloading to local storage.
push Sync the remote storage by uploading the local storage.
`
return strings.TrimSpace(helpText)
}
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"
)
type PullCommand struct {
type RemotePullCommand struct {
Meta
}
func (c *PullCommand) Run(args []string) int {
func (c *RemotePullCommand) Run(args []string) int {
args = c.Meta.process(args, false)
cmdFlags := flag.NewFlagSet("pull", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
@ -67,7 +67,7 @@ func (c *PullCommand) Run(args []string) int {
return 0
}
func (c *PullCommand) Help() string {
func (c *RemotePullCommand) Help() string {
helpText := `
Usage: terraform pull [options]
@ -77,6 +77,6 @@ Usage: terraform pull [options]
return strings.TrimSpace(helpText)
}
func (c *PullCommand) Synopsis() string {
func (c *RemotePullCommand) Synopsis() string {
return "Refreshes the local state copy from the remote server"
}

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ import (
)
// Test disabling remote management
func TestRemote_disable(t *testing.T) {
func TestRemoteConfig_disable(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
@ -39,7 +39,7 @@ func TestRemote_disable(t *testing.T) {
}
ui := new(cli.MockUi)
c := &RemoteCommand{
c := &RemoteConfigCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
@ -68,7 +68,7 @@ func TestRemote_disable(t *testing.T) {
}
// Test disabling remote management without pulling
func TestRemote_disable_noPull(t *testing.T) {
func TestRemoteConfig_disable_noPull(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
@ -94,7 +94,7 @@ func TestRemote_disable_noPull(t *testing.T) {
}
ui := new(cli.MockUi)
c := &RemoteCommand{
c := &RemoteConfigCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
@ -122,12 +122,12 @@ func TestRemote_disable_noPull(t *testing.T) {
}
// 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)
defer testFixCwd(t, tmp, cwd)
ui := new(cli.MockUi)
c := &RemoteCommand{
c := &RemoteConfigCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
@ -141,7 +141,7 @@ func TestRemote_disable_notEnabled(t *testing.T) {
}
// 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)
defer testFixCwd(t, tmp, cwd)
@ -171,7 +171,7 @@ func TestRemote_disable_otherState(t *testing.T) {
}
ui := new(cli.MockUi)
c := &RemoteCommand{
c := &RemoteConfigCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
@ -185,7 +185,7 @@ func TestRemote_disable_otherState(t *testing.T) {
}
// 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)
defer testFixCwd(t, tmp, cwd)
@ -215,7 +215,7 @@ func TestRemote_managedAndNonManaged(t *testing.T) {
}
ui := new(cli.MockUi)
c := &RemoteCommand{
c := &RemoteConfigCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
@ -229,12 +229,12 @@ func TestRemote_managedAndNonManaged(t *testing.T) {
}
// Test initializing blank state
func TestRemote_initBlank(t *testing.T) {
func TestRemoteConfig_initBlank(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
ui := new(cli.MockUi)
c := &RemoteCommand{
c := &RemoteConfigCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
@ -269,12 +269,12 @@ func TestRemote_initBlank(t *testing.T) {
}
// Test initializing without remote settings
func TestRemote_initBlank_missingRemote(t *testing.T) {
func TestRemoteConfig_initBlank_missingRemote(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
ui := new(cli.MockUi)
c := &RemoteCommand{
c := &RemoteConfigCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
@ -288,7 +288,7 @@ func TestRemote_initBlank_missingRemote(t *testing.T) {
}
// Test updating remote config
func TestRemote_updateRemote(t *testing.T) {
func TestRemoteConfig_updateRemote(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
@ -310,7 +310,7 @@ func TestRemote_updateRemote(t *testing.T) {
}
ui := new(cli.MockUi)
c := &RemoteCommand{
c := &RemoteConfigCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
@ -345,7 +345,7 @@ func TestRemote_updateRemote(t *testing.T) {
}
// Test enabling remote state
func TestRemote_enableRemote(t *testing.T) {
func TestRemoteConfig_enableRemote(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
@ -365,7 +365,7 @@ func TestRemote_enableRemote(t *testing.T) {
}
ui := new(cli.MockUi)
c := &RemoteCommand{
c := &RemoteConfigCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,

View File

@ -80,18 +80,6 @@ func init() {
}, 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) {
return &command.RefreshCommand{
Meta: meta,

View File

@ -17,6 +17,7 @@ var Funcs map[string]ast.Function
func init() {
Funcs = map[string]ast.Function{
"file": interpolationFuncFile(),
"format": interpolationFuncFormat(),
"join": interpolationFuncJoin(),
"element": interpolationFuncElement(),
"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
// multi-variable values to be joined by some character.
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) {
testFunction(t, testFunctionConfig{
Cases: []testFunctionCase{

View File

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

View File

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

View File

@ -174,6 +174,10 @@ func (tc *typeCheckCall) TypeCheck(v *TypeCheck) (ast.Node, error) {
// Verify the args
for i, expected := range function.ArgTypes {
if expected == ast.TypeAny {
continue
}
if args[i] != expected {
cn := v.ImplicitConversion(args[i], expected, tc.n.Args[i])
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 function.Variadic {
if function.Variadic && function.VariadicType != ast.TypeAny {
args = args[len(function.ArgTypes):]
for i, t := range args {
if t != function.VariadicType {

View File

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

View File

@ -137,6 +137,25 @@ func (d *ResourceData) Partial(on bool) {
// will be returned.
func (d *ResourceData) Set(key string, value interface{}) error {
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)
}

View File

@ -1178,6 +1178,8 @@ func TestResourceDataHasChange(t *testing.T) {
}
func TestResourceDataSet(t *testing.T) {
var testNilPtr *string
cases := []struct {
Schema map[string]*Schema
State *terraform.InstanceState
@ -1588,6 +1590,72 @@ func TestResourceDataSet(t *testing.T) {
GetKey: "ratios",
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 {
@ -2788,3 +2856,7 @@ func TestResourceDataSetId_override(t *testing.T) {
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.
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
@ -877,7 +892,7 @@ func (m schemaMap) validate(
raw, err = schema.DefaultFunc()
if err != nil {
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
@ -886,7 +901,7 @@ func (m schemaMap) validate(
if !ok {
if schema.Required {
return nil, []error{fmt.Errorf(
"%s: required field is not set", k)}
"%q: required field is not set", k)}
}
return nil, nil
@ -895,7 +910,7 @@ func (m schemaMap) validate(
if !schema.Required && !schema.Optional {
// This is a computed-only field
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)
@ -1066,16 +1081,30 @@ func (m schemaMap) validateType(
raw interface{},
schema *Schema,
c *terraform.ResourceConfig) ([]string, []error) {
var ws []string
var es []error
switch schema.Type {
case TypeSet:
fallthrough
case TypeList:
return m.validateList(k, raw, schema, c)
ws, es = m.validateList(k, raw, schema, c)
case TypeMap:
return m.validateMap(k, raw, schema, c)
ws, es = m.validateMap(k, raw, schema, c)
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.

View File

@ -2583,15 +2583,15 @@ func TestSchemaMap_InternalValidate(t *testing.T) {
}
func TestSchemaMap_Validate(t *testing.T) {
cases := []struct {
cases := map[string]struct {
Schema map[string]*Schema
Config map[string]interface{}
Vars map[string]string
Warn bool
Err bool
Errors []error
Warnings []string
}{
// #0 Good
{
"Good": {
Schema: map[string]*Schema{
"availability_zone": &Schema{
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{
"size": &Schema{
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{
"availability_zone": &Schema{
Type: TypeString,
@ -2638,8 +2636,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true,
},
// #3 Invalid type
{
"Invalid basic type": {
Schema: map[string]*Schema{
"port": &Schema{
Type: TypeInt,
@ -2654,8 +2651,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true,
},
// #4
{
"Invalid complex type": {
Schema: map[string]*Schema{
"user_data": &Schema{
Type: TypeString,
@ -2674,8 +2670,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true,
},
// #5 Bad type, interpolated
{
"Bad type, interpolated": {
Schema: map[string]*Schema{
"size": &Schema{
Type: TypeInt,
@ -2694,8 +2689,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true,
},
// #6 Required but has DefaultFunc
{
"Required but has DefaultFunc": {
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
@ -2709,8 +2703,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Config: nil,
},
// #7 Required but has DefaultFunc return nil
{
"Required but has DefaultFunc return nil": {
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
@ -2726,8 +2719,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true,
},
// #8 Optional sub-resource
{
"Optional sub-resource": {
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
@ -2747,8 +2739,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: false,
},
// #9 Not a list
{
"Not a list": {
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
@ -2770,8 +2761,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true,
},
// #10 Required sub-resource field
{
"Required sub-resource field": {
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
@ -2795,8 +2785,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true,
},
// #11 Good sub-resource
{
"Good sub-resource": {
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
@ -2823,8 +2812,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: false,
},
// #12 Invalid/unknown field
{
"Invalid/unknown field": {
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
@ -2841,8 +2829,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true,
},
// #13 Computed field set
{
"Computed field set": {
Schema: map[string]*Schema{
"availability_zone": &Schema{
Type: TypeString,
@ -2857,8 +2844,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true,
},
// #14 Not a set
{
"Not a set": {
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeSet,
@ -2877,8 +2863,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true,
},
// #15 Maps
{
"Maps": {
Schema: map[string]*Schema{
"user_data": &Schema{
Type: TypeMap,
@ -2893,8 +2878,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true,
},
// #16
{
"Good map: data surrounded by extra slice": {
Schema: map[string]*Schema{
"user_data": &Schema{
Type: TypeMap,
@ -2911,8 +2895,7 @@ func TestSchemaMap_Validate(t *testing.T) {
},
},
// #17
{
"Good map": {
Schema: map[string]*Schema{
"user_data": &Schema{
Type: TypeMap,
@ -2927,8 +2910,7 @@ func TestSchemaMap_Validate(t *testing.T) {
},
},
// #18
{
"Bad map: just a slice": {
Schema: map[string]*Schema{
"user_data": &Schema{
Type: TypeMap,
@ -2945,8 +2927,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true,
},
// #19
{
"Good set: config has slice with single interpolated value": {
Schema: map[string]*Schema{
"security_groups": &Schema{
Type: TypeSet,
@ -2967,8 +2948,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: false,
},
// #20
{
"Bad set: config has single interpolated value": {
Schema: map[string]*Schema{
"security_groups": &Schema{
Type: TypeSet,
@ -2986,8 +2966,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true,
},
// #21 Bad, subresource should not allow unknown elements
{
"Bad, subresource should not allow unknown elements": {
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
@ -3015,8 +2994,7 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true,
},
// #22 Bad, subresource should not allow invalid types
{
"Bad, subresource should not allow invalid types": {
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
@ -3042,9 +3020,74 @@ func TestSchemaMap_Validate(t *testing.T) {
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)
if err != nil {
t.Fatalf("err: %s", err)
@ -3063,18 +3106,24 @@ func TestSchemaMap_Validate(t *testing.T) {
ws, es := schemaMap(tc.Schema).Validate(terraform.NewResourceConfig(c))
if (len(es) > 0) != tc.Err {
if len(es) == 0 {
t.Errorf("%d: no errors", i)
t.Errorf("%q: no errors", tn)
}
for _, e := range es {
t.Errorf("%d: err: %s", i, e)
t.Errorf("%q: err: %s", tn, e)
}
t.FailNow()
}
if (len(ws) > 0) != tc.Warn {
t.Fatalf("%d: ws: %#v", i, ws)
if !reflect.DeepEqual(ws, tc.Warnings) {
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,
"consul": consulFactory,
"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) {
m := testModule(t, "apply-provisioner-resource-ref")
p := testProvider("aws")
@ -5343,6 +5473,15 @@ func testProvisioner() *MockResourceProvisioner {
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 = `
root: root
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.
type EvalIf struct {
If func(EvalContext) (bool, error)
Node EvalNode
Then EvalNode
Else EvalNode
}
// TODO: test
@ -14,7 +15,11 @@ func (n *EvalIf) Eval(ctx EvalContext) (interface{}, error) {
}
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

View File

@ -5,16 +5,77 @@ import (
)
// 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 {
Name string
Tainted bool
TaintedIndex int
Output **InstanceState
}
// TODO: test
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()
// 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.
rs := mod.Resources[n.Name]
rs := mod.Resources[resourceName]
if rs == nil {
return nil, nil
}
var result *InstanceState
if !n.Tainted {
// Return the primary
result = rs.Primary
} 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]
}
// Use the delegate function to get the instance state from the resource state
is, err := readerFn(rs)
if err != nil {
return nil, err
}
// Write the result to the output pointer
if n.Output != nil {
*n.Output = result
if output != nil {
*output = is
}
return result, nil
return is, nil
}
// EvalRequireState is an EvalNode implementation that early exits
@ -98,20 +149,85 @@ func (n *EvalUpdateStateHook) Eval(ctx EvalContext) (interface{}, error) {
return nil, nil
}
// EvalWriteState is an EvalNode implementation that reads the
// InstanceState for a specific resource out of the state.
// EvalWriteState is an EvalNode implementation that writes the
// primary InstanceState for a specific resource into the state.
type EvalWriteState struct {
Name string
ResourceType string
Dependencies []string
State **InstanceState
Tainted *bool
TaintedIndex int
TaintedClearPrimary bool
}
// TODO: test
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()
if state == nil {
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.
rs := mod.Resources[n.Name]
rs := mod.Resources[resourceName]
if rs == nil {
rs = &ResourceState{}
rs.init()
mod.Resources[n.Name] = rs
mod.Resources[resourceName] = rs
}
rs.Type = n.ResourceType
rs.Dependencies = n.Dependencies
rs.Type = resourceType
rs.Dependencies = dependencies
if n.Tainted != nil && *n.Tainted {
if n.TaintedIndex != -1 {
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
if err := writerFn(rs); err != nil {
return nil, err
}
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
// 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
// the old state of the to-be-destroyed resource.
type EvalDeposeState struct {
@ -188,8 +324,8 @@ func (n *EvalDeposeState) Eval(ctx EvalContext) (interface{}, error) {
return nil, nil
}
// Depose to the tainted
rs.Tainted = append(rs.Tainted, rs.Primary)
// Depose
rs.Deposed = append(rs.Deposed, rs.Primary)
rs.Primary = nil
return nil, nil
@ -221,15 +357,15 @@ func (n *EvalUndeposeState) Eval(ctx EvalContext) (interface{}, error) {
return nil, nil
}
// If we don't have any tainted, then we don't have anything to do
if len(rs.Tainted) == 0 {
// If we don't have any desposed resource, then we don't have anything to do
if len(rs.Deposed) == 0 {
return nil, nil
}
// Undepose to the tainted
idx := len(rs.Tainted) - 1
rs.Primary = rs.Tainted[idx]
rs.Tainted[idx] = nil
// Undepose
idx := len(rs.Deposed) - 1
rs.Primary = rs.Deposed[idx]
rs.Deposed[idx] = nil
return nil, nil
}

View File

@ -66,3 +66,163 @@ func TestEvalUpdateStateHook(t *testing.T) {
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(),
})
if n.Resource.Lifecycle.CreateBeforeDestroy {
// If we're only destroying tainted resources, then we only
// want to find tainted resources and destroy them here.
steps = append(steps, &TaintedTransformer{
steps = append(steps, &DeposedTransformer{
State: state,
View: n.Resource.Id(),
Deposed: n.Resource.Lifecycle.CreateBeforeDestroy,
DeposedInclude: true,
})
}
case DestroyTainted:
// 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,
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 {
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)
}
}
@ -548,7 +548,12 @@ func (m *ModuleState) String() string {
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))
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))
}
for idx, t := range rs.Deposed {
buf.WriteString(fmt.Sprintf(" Deposed ID %d = %s\n", idx+1, t.ID))
}
if len(rs.Dependencies) > 0 {
buf.WriteString(fmt.Sprintf("\n Dependencies:\n"))
for _, dep := range rs.Dependencies {
@ -644,6 +653,16 @@ type ResourceState struct {
// However, in pathological cases, it is possible for the number
// of instances to accumulate.
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.
@ -744,6 +763,12 @@ func (r *ResourceState) deepcopy() *ResourceState {
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
}
@ -762,6 +787,19 @@ func (r *ResourceState) prune() {
}
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() {

View File

@ -443,9 +443,9 @@ aws_instance.bar:
`
const testTerraformApplyErrorDestroyCreateBeforeDestroyStr = `
aws_instance.bar: (1 tainted)
aws_instance.bar: (1 deposed)
ID = foo
Tainted ID 1 = bar
Deposed ID 1 = bar
`
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
return true, nil
},
Node: EvalNoop{},
Then: EvalNoop{},
},
&EvalIf{
@ -301,7 +301,7 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode {
return createBeforeDestroyEnabled, nil
},
Node: &EvalDeposeState{
Then: &EvalDeposeState{
Name: n.stateId(),
},
},
@ -382,7 +382,7 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode {
failure := tainted || err != nil
return createBeforeDestroyEnabled && failure, nil
},
Node: &EvalUndeposeState{
Then: &EvalUndeposeState{
Name: n.stateId(),
},
},
@ -395,14 +395,35 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode {
Diff: nil,
},
&EvalWriteState{
&EvalIf{
If: func(ctx EvalContext) (bool, error) {
return tainted, nil
},
Then: &EvalSequence{
Nodes: []EvalNode{
&EvalWriteStateTainted{
Name: n.stateId(),
ResourceType: n.Resource.Type,
Dependencies: n.DependentOn(),
State: &state,
Tainted: &tainted,
TaintedIndex: -1,
TaintedClearPrimary: !n.Resource.Lifecycle.CreateBeforeDestroy,
Index: -1,
},
&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{
Info: info,
@ -480,18 +501,26 @@ func (n *graphNodeExpandedResourceDestroy) EvalTree() EvalNode {
return true, EvalEarlyExitError{}
},
Node: EvalNoop{},
Then: EvalNoop{},
},
&EvalGetProvider{
Name: n.ProvidedBy()[0],
Output: &provider,
},
&EvalReadState{
&EvalIf{
If: func(ctx EvalContext) (bool, error) {
return n.Resource.Lifecycle.CreateBeforeDestroy, nil
},
Then: &EvalReadStateTainted{
Name: n.stateId(),
Output: &state,
Tainted: n.Resource.Lifecycle.CreateBeforeDestroy,
TaintedIndex: -1,
Index: -1,
},
Else: &EvalReadState{
Name: n.stateId(),
Output: &state,
},
},
&EvalRequireState{
State: &state,

View File

@ -4,7 +4,7 @@ import (
"fmt"
)
// TraintedTransformer is a GraphTransformer that adds tainted resources
// TaintedTransformer is a GraphTransformer that adds tainted resources
// to the graph.
type TaintedTransformer struct {
// 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
// to find tainted resources.
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 {
@ -43,17 +37,6 @@ func (t *TaintedTransformer) Transform(g *Graph) error {
}
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 {
// Add the graph node and make the connection from any untainted
// resources with this name to the tainted resource, so that
@ -88,7 +71,6 @@ func (n *graphNodeTaintedResource) ProvidedBy() []string {
func (n *graphNodeTaintedResource) EvalTree() EvalNode {
var provider ResourceProvider
var state *InstanceState
tainted := true
seq := &EvalSequence{Nodes: make([]EvalNode, 0, 5)}
@ -105,10 +87,9 @@ func (n *graphNodeTaintedResource) EvalTree() EvalNode {
Name: n.ProvidedBy()[0],
Output: &provider,
},
&EvalReadState{
&EvalReadStateTainted{
Name: n.ResourceName,
Tainted: true,
TaintedIndex: n.Index,
Index: n.Index,
Output: &state,
},
&EvalRefresh{
@ -117,12 +98,11 @@ func (n *graphNodeTaintedResource) EvalTree() EvalNode {
State: &state,
Output: &state,
},
&EvalWriteState{
&EvalWriteStateTainted{
Name: n.ResourceName,
ResourceType: n.ResourceType,
State: &state,
Tainted: &tainted,
TaintedIndex: n.Index,
Index: n.Index,
},
},
},
@ -138,10 +118,9 @@ func (n *graphNodeTaintedResource) EvalTree() EvalNode {
Name: n.ProvidedBy()[0],
Output: &provider,
},
&EvalReadState{
&EvalReadStateTainted{
Name: n.ResourceName,
Tainted: true,
TaintedIndex: n.Index,
Index: n.Index,
Output: &state,
},
&EvalDiffDestroy{
@ -156,12 +135,11 @@ func (n *graphNodeTaintedResource) EvalTree() EvalNode {
Provider: &provider,
Output: &state,
},
&EvalWriteState{
&EvalWriteStateTainted{
Name: n.ResourceName,
ResourceType: n.ResourceType,
State: &state,
Tainted: &tainted,
TaintedIndex: n.Index,
Index: n.Index,
},
&EvalUpdateStateHook{},
},

View File

@ -31,11 +31,10 @@ Available commands are:
init Initializes Terraform configuration from a module
output Read an output from a state file
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
remote Configures remote state management
remote Configure remote state storage
show Inspect Terraform state or plan
taint Manually mark a resource for recreation
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
The `terraform remote` 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.
The `terraform remote` command is used to configure all aspects of
remote state storage. 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 [options]`
Usage: `terraform remote SUBCOMMAND [options]`
The `remote` 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` 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.
The `remote` command behaves as another command that further has more
subcommands. The subcommands available are:
* [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
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
only possible with splat variables from resources with a count
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
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
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
you're creating).
While its not strictly necessary, Terraform plugins follow specific
naming conventions. The format of the plugin binaries are
`terraform-TYPE-NAME`. For example, `terraform-provider-aws`.
We recommend you follow this convention to help make it clear what
your plugin does to users.
Terraform plugins must follow a very specific naming convention of
`terraform-TYPE-NAME`. For example, `terraform-provider-aws`, which
tells Terraform that the plugin is a provider that can be referenced
as "aws".

View File

@ -32,7 +32,7 @@ resource "aws_instance" "web" {
## 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
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
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.
* `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.
* `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).
* `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.
* `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.
* `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).
## Attributes Reference

View File

@ -22,14 +22,14 @@ resource "aws_security_group" "allow_all" {
ingress {
from_port = 0
to_port = 65535
protocol = "tcp"
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 65535
protocol = "tcp"
protocol = "-1"
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.
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
fields documented below.

View File

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

View File

@ -79,14 +79,6 @@
<a href="/docs/commands/plan.html">plan</a>
</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") %>>
<a href="/docs/commands/refresh.html">refresh</a>
</li>