merging from hashicorp master
This commit is contained in:
commit
e59d4fc976
|
@ -10,6 +10,7 @@ install: make updatedeps
|
|||
|
||||
script:
|
||||
- go test ./...
|
||||
- make vet
|
||||
#- go test -race ./...
|
||||
|
||||
branches:
|
||||
|
|
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)))
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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,
|
|
@ -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"
|
||||
}
|
|
@ -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,
|
|
@ -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,
|
||||
|
|
12
commands.go
12
commands.go
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -48,7 +48,8 @@ type Type uint32
|
|||
|
||||
const (
|
||||
TypeInvalid Type = 0
|
||||
TypeString Type = 1 << iota
|
||||
TypeAny Type = 1 << iota
|
||||
TypeString
|
||||
TypeInt
|
||||
TypeFloat
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -39,4 +39,7 @@ var BuiltinClients = map[string]Factory{
|
|||
"atlas": atlasFactory,
|
||||
"consul": consulFactory,
|
||||
"http": httpFactory,
|
||||
|
||||
// This is used for development purposes only.
|
||||
"_local": fileFactory,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
`)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 = `
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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{},
|
||||
},
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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).
|
||||
|
|
|
@ -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)`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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".
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue