diff --git a/.travis.yml b/.travis.yml index cbed9c5f2..275e519bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ install: make updatedeps script: - go test ./... + - make vet #- go test -race ./... branches: diff --git a/CHANGELOG.md b/CHANGELOG.md index b1288e817..8470f90b6 100644 --- a/CHANGELOG.md +++ b/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,11 +23,14 @@ 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] * **New config function: `split`** - Split a value based on a delimiter. This is useful for faking lists as parameters to modules. + * **New resource: `digitalocean_ssh_key`** [GH-1074] * core: The serial of the state is only updated if there is an actual change. This will lower the amount of state changing on things like refresh. @@ -39,9 +49,16 @@ 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/aws: Longer wait times for route53 records (30 mins). [GH-1164] * 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) diff --git a/Makefile b/Makefile index fdb9ab3d6..21a54a8f8 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,9 @@ bin: generate dev: generate @TF_DEV=1 sh -c "'$(CURDIR)/scripts/build.sh'" +quickdev: generate + @TF_QUICKDEV=1 TF_DEV=1 sh -c "'$(CURDIR)/scripts/build.sh'" + # test runs the unit tests and vets the code test: generate TF_ACC= go test $(TEST) $(TESTARGS) -timeout=30s -parallel=4 diff --git a/builtin/providers/aws/network_acl_entry.go b/builtin/providers/aws/network_acl_entry.go index 8ce88d81a..09954083a 100644 --- a/builtin/providers/aws/network_acl_entry.go +++ b/builtin/providers/aws/network_acl_entry.go @@ -2,11 +2,14 @@ package aws import ( "fmt" - "github.com/mitchellh/goamz/ec2" + "strconv" + + "github.com/hashicorp/aws-sdk-go/aws" + "github.com/hashicorp/aws-sdk-go/gen/ec2" ) -func expandNetworkAclEntries(configured []interface{}, entryType string) ([]ec2.NetworkAclEntry, error) { - entries := make([]ec2.NetworkAclEntry, 0, len(configured)) +func expandNetworkAclEntries(configured []interface{}, entryType string) ([]ec2.NetworkACLEntry, error) { + entries := make([]ec2.NetworkACLEntry, 0, len(configured)) for _, eRaw := range configured { data := eRaw.(map[string]interface{}) protocol := data["protocol"].(string) @@ -15,16 +18,16 @@ func expandNetworkAclEntries(configured []interface{}, entryType string) ([]ec2. return nil, fmt.Errorf("Invalid Protocol %s for rule %#v", protocol, data) } p := extractProtocolInteger(data["protocol"].(string)) - e := ec2.NetworkAclEntry{ - Protocol: p, - PortRange: ec2.PortRange{ - From: data["from_port"].(int), - To: data["to_port"].(int), + e := ec2.NetworkACLEntry{ + Protocol: aws.String(strconv.Itoa(p)), + PortRange: &ec2.PortRange{ + From: aws.Integer(data["from_port"].(int)), + To: aws.Integer(data["to_port"].(int)), }, - Egress: (entryType == "egress"), - RuleAction: data["action"].(string), - RuleNumber: data["rule_no"].(int), - CidrBlock: data["cidr_block"].(string), + Egress: aws.Boolean((entryType == "egress")), + RuleAction: aws.String(data["action"].(string)), + RuleNumber: aws.Integer(data["rule_no"].(int)), + CIDRBlock: aws.String(data["cidr_block"].(string)), } entries = append(entries, e) } @@ -33,17 +36,17 @@ func expandNetworkAclEntries(configured []interface{}, entryType string) ([]ec2. } -func flattenNetworkAclEntries(list []ec2.NetworkAclEntry) []map[string]interface{} { +func flattenNetworkAclEntries(list []ec2.NetworkACLEntry) []map[string]interface{} { entries := make([]map[string]interface{}, 0, len(list)) for _, entry := range list { entries = append(entries, map[string]interface{}{ - "from_port": entry.PortRange.From, - "to_port": entry.PortRange.To, - "action": entry.RuleAction, - "rule_no": entry.RuleNumber, - "protocol": extractProtocolString(entry.Protocol), - "cidr_block": entry.CidrBlock, + "from_port": *entry.PortRange.From, + "to_port": *entry.PortRange.To, + "action": *entry.RuleAction, + "rule_no": *entry.RuleNumber, + "protocol": *entry.Protocol, + "cidr_block": *entry.CIDRBlock, }) } return entries diff --git a/builtin/providers/aws/network_acl_entry_test.go b/builtin/providers/aws/network_acl_entry_test.go index a2d60abb8..50f6fdc85 100644 --- a/builtin/providers/aws/network_acl_entry_test.go +++ b/builtin/providers/aws/network_acl_entry_test.go @@ -4,10 +4,11 @@ import ( "reflect" "testing" - "github.com/mitchellh/goamz/ec2" + "github.com/hashicorp/aws-sdk-go/aws" + "github.com/hashicorp/aws-sdk-go/gen/ec2" ) -func Test_expandNetworkAclEntry(t *testing.T) { +func Test_expandNetworkACLEntry(t *testing.T) { input := []interface{}{ map[string]interface{}{ "protocol": "tcp", @@ -28,30 +29,28 @@ func Test_expandNetworkAclEntry(t *testing.T) { } expanded, _ := expandNetworkAclEntries(input, "egress") - expected := []ec2.NetworkAclEntry{ - ec2.NetworkAclEntry{ - Protocol: 6, - PortRange: ec2.PortRange{ - From: 22, - To: 22, + expected := []ec2.NetworkACLEntry{ + ec2.NetworkACLEntry{ + Protocol: aws.String("6"), + PortRange: &ec2.PortRange{ + From: aws.Integer(22), + To: aws.Integer(22), }, - RuleAction: "deny", - RuleNumber: 1, - CidrBlock: "0.0.0.0/0", - Egress: true, - IcmpCode: ec2.IcmpCode{Code: 0, Type: 0}, + RuleAction: aws.String("deny"), + RuleNumber: aws.Integer(1), + CIDRBlock: aws.String("0.0.0.0/0"), + Egress: aws.Boolean(true), }, - ec2.NetworkAclEntry{ - Protocol: 6, - PortRange: ec2.PortRange{ - From: 443, - To: 443, + ec2.NetworkACLEntry{ + Protocol: aws.String("6"), + PortRange: &ec2.PortRange{ + From: aws.Integer(443), + To: aws.Integer(443), }, - RuleAction: "deny", - RuleNumber: 2, - CidrBlock: "0.0.0.0/0", - Egress: true, - IcmpCode: ec2.IcmpCode{Code: 0, Type: 0}, + RuleAction: aws.String("deny"), + RuleNumber: aws.Integer(2), + CIDRBlock: aws.String("0.0.0.0/0"), + Egress: aws.Boolean(true), }, } @@ -64,28 +63,28 @@ func Test_expandNetworkAclEntry(t *testing.T) { } -func Test_flattenNetworkAclEntry(t *testing.T) { +func Test_flattenNetworkACLEntry(t *testing.T) { - apiInput := []ec2.NetworkAclEntry{ - ec2.NetworkAclEntry{ - Protocol: 6, - PortRange: ec2.PortRange{ - From: 22, - To: 22, + apiInput := []ec2.NetworkACLEntry{ + ec2.NetworkACLEntry{ + Protocol: aws.String("tcp"), + PortRange: &ec2.PortRange{ + From: aws.Integer(22), + To: aws.Integer(22), }, - RuleAction: "deny", - RuleNumber: 1, - CidrBlock: "0.0.0.0/0", + RuleAction: aws.String("deny"), + RuleNumber: aws.Integer(1), + CIDRBlock: aws.String("0.0.0.0/0"), }, - ec2.NetworkAclEntry{ - Protocol: 6, - PortRange: ec2.PortRange{ - From: 443, - To: 443, + ec2.NetworkACLEntry{ + Protocol: aws.String("tcp"), + PortRange: &ec2.PortRange{ + From: aws.Integer(443), + To: aws.Integer(443), }, - RuleAction: "deny", - RuleNumber: 2, - CidrBlock: "0.0.0.0/0", + RuleAction: aws.String("deny"), + RuleNumber: aws.Integer(2), + CIDRBlock: aws.String("0.0.0.0/0"), }, } flattened := flattenNetworkAclEntries(apiInput) diff --git a/builtin/providers/aws/resource_aws_db_instance.go b/builtin/providers/aws/resource_aws_db_instance.go index 81ac45f53..e99744a0f 100644 --- a/builtin/providers/aws/resource_aws_db_instance.go +++ b/builtin/providers/aws/resource_aws_db_instance.go @@ -156,7 +156,6 @@ func resourceAwsDbInstance() *schema.Resource { "final_snapshot_identifier": &schema.Schema{ Type: schema.TypeString, Optional: true, - ForceNew: true, }, "db_subnet_group_name": &schema.Schema{ @@ -324,7 +323,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) diff --git a/builtin/providers/aws/resource_aws_eip.go b/builtin/providers/aws/resource_aws_eip.go index 8a56e1de9..103f9bc5a 100644 --- a/builtin/providers/aws/resource_aws_eip.go +++ b/builtin/providers/aws/resource_aws_eip.go @@ -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} } diff --git a/builtin/providers/aws/resource_aws_eip_test.go b/builtin/providers/aws/resource_aws_eip_test.go index d99801db9..79e88b8f3 100644 --- a/builtin/providers/aws/resource_aws_eip_test.go +++ b/builtin/providers/aws/resource_aws_eip_test.go @@ -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] diff --git a/builtin/providers/aws/resource_aws_instance.go b/builtin/providers/aws/resource_aws_instance.go index 4e32dac2c..5475e7454 100644 --- a/builtin/providers/aws/resource_aws_instance.go +++ b/builtin/providers/aws/resource_aws_instance.go @@ -6,14 +6,14 @@ import ( "encoding/hex" "fmt" "log" - "strconv" "strings" "time" + "github.com/hashicorp/aws-sdk-go/aws" + "github.com/hashicorp/aws-sdk-go/gen/ec2" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" - "github.com/mitchellh/goamz/ec2" ) func resourceAwsInstance() *schema.Resource { @@ -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, + }, }, }, }, @@ -239,7 +253,7 @@ func resourceAwsInstance() *schema.Resource { } func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn // Figure out user data userData := "" @@ -247,38 +261,84 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error { userData = v.(string) } + placement := &ec2.Placement{ + AvailabilityZone: aws.String(d.Get("availability_zone").(string)), + Tenancy: aws.String(d.Get("tenancy").(string)), + } + + iam := &ec2.IAMInstanceProfileSpecification{ + Name: aws.String(d.Get("iam_instance_profile").(string)), + } + + // Build the creation struct + runOpts := &ec2.RunInstancesRequest{ + ImageID: aws.String(d.Get("ami").(string)), + Placement: placement, + InstanceType: aws.String(d.Get("instance_type").(string)), + MaxCount: aws.Integer(1), + MinCount: aws.Integer(1), + UserData: aws.String(userData), + EBSOptimized: aws.Boolean(d.Get("ebs_optimized").(bool)), + IAMInstanceProfile: iam, + } + associatePublicIPAddress := false if v := d.Get("associate_public_ip_address"); v != nil { associatePublicIPAddress = v.(bool) } - // Build the creation struct - runOpts := &ec2.RunInstances{ - ImageId: d.Get("ami").(string), - AvailZone: d.Get("availability_zone").(string), - InstanceType: d.Get("instance_type").(string), - KeyName: d.Get("key_name").(string), - SubnetId: d.Get("subnet_id").(string), - PrivateIPAddress: d.Get("private_ip").(string), - AssociatePublicIpAddress: associatePublicIPAddress, - UserData: []byte(userData), - EbsOptimized: d.Get("ebs_optimized").(bool), - IamInstanceProfile: d.Get("iam_instance_profile").(string), - Tenancy: d.Get("tenancy").(string), + // check for non-default Subnet, and cast it to a String + var hasSubnet bool + subnet, hasSubnet := d.GetOk("subnet_id") + subnetID := subnet.(string) + + if hasSubnet && associatePublicIPAddress { + // If we have a non-default VPC / Subnet specified, we can flag + // AssociatePublicIpAddress to get a Public IP assigned. By default these are not provided. + // You cannot specify both SubnetId and the NetworkInterface.0.* parameters though, otherwise + // you get: Network interfaces and an instance-level subnet ID may not be specified on the same request + // You also need to attach Security Groups to the NetworkInterface instead of the instance, + // to avoid: Network interfaces and an instance-level security groups may not be specified on + // the same request + ni := ec2.InstanceNetworkInterfaceSpecification{ + AssociatePublicIPAddress: aws.Boolean(associatePublicIPAddress), + DeviceIndex: aws.Integer(0), + SubnetID: aws.String(subnetID), + } + + if v, ok := d.GetOk("private_ip"); ok { + ni.PrivateIPAddress = aws.String(v.(string)) + } + + runOpts.NetworkInterfaces = []ec2.InstanceNetworkInterfaceSpecification{ni} + } else { + if subnetID != "" { + runOpts.SubnetID = aws.String(subnetID) + } + + if v, ok := d.GetOk("private_ip"); ok { + runOpts.PrivateIPAddress = aws.String(v.(string)) + } + } + + if v, ok := d.GetOk("key_name"); ok { + runOpts.KeyName = aws.String(v.(string)) } if v := d.Get("security_groups"); v != nil { + // Security group names. + // For a nondefault VPC, you must use security group IDs instead. + // See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_RunInstances.html + var groups []string for _, v := range v.(*schema.Set).List() { str := v.(string) - - var g ec2.SecurityGroup - if runOpts.SubnetId != "" { - g.Id = str - } else { - g.Name = str - } - - runOpts.SecurityGroups = append(runOpts.SecurityGroups, g) + groups = append(groups, str) + } + if runOpts.SubnetID != nil && + *runOpts.SubnetID != "" { + runOpts.SecurityGroupIDs = groups + } else { + runOpts.SecurityGroups = groups } } @@ -297,21 +357,27 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error { } if len(blockDevices) > 0 { - runOpts.BlockDevices = make([]ec2.BlockDeviceMapping, len(blockDevices)) + runOpts.BlockDeviceMappings = make([]ec2.BlockDeviceMapping, len(blockDevices)) for i, v := range blockDevices { bd := v.(map[string]interface{}) - runOpts.BlockDevices[i].DeviceName = bd["device_name"].(string) - runOpts.BlockDevices[i].VolumeType = bd["volume_type"].(string) - runOpts.BlockDevices[i].VolumeSize = int64(bd["volume_size"].(int)) - runOpts.BlockDevices[i].DeleteOnTermination = bd["delete_on_termination"].(bool) - if v, ok := bd["virtual_name"].(string); ok { - runOpts.BlockDevices[i].VirtualName = v + runOpts.BlockDeviceMappings[i].DeviceName = aws.String(bd["device_name"].(string)) + runOpts.BlockDeviceMappings[i].EBS = &ec2.EBSBlockDevice{ + VolumeType: aws.String(bd["volume_type"].(string)), + VolumeSize: aws.Integer(bd["volume_size"].(int)), + DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)), } - if v, ok := bd["snapshot_id"].(string); ok { - runOpts.BlockDevices[i].SnapshotId = v + + if v, ok := bd["virtual_name"].(string); ok { + runOpts.BlockDeviceMappings[i].VirtualName = aws.String(v) + } + if v, ok := bd["snapshot_id"].(string); ok && v != "" { + runOpts.BlockDeviceMappings[i].EBS.SnapshotID = aws.String(v) } if v, ok := bd["encrypted"].(bool); ok { - runOpts.BlockDevices[i].Encrypted = v + runOpts.BlockDeviceMappings[i].EBS.Encrypted = aws.Boolean(v) + } + if v, ok := bd["iops"].(int); ok && v > 0 { + runOpts.BlockDeviceMappings[i].EBS.IOPS = aws.Integer(v) } } } @@ -324,21 +390,21 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error { } instance := &runResp.Instances[0] - log.Printf("[INFO] Instance ID: %s", instance.InstanceId) + log.Printf("[INFO] Instance ID: %s", *instance.InstanceID) // Store the resulting ID so we can look this up later - d.SetId(instance.InstanceId) + d.SetId(*instance.InstanceID) // Wait for the instance to become running so we can get some attributes // that aren't available until later. log.Printf( "[DEBUG] Waiting for instance (%s) to become running", - instance.InstanceId) + *instance.InstanceID) stateConf := &resource.StateChangeConf{ Pending: []string{"pending"}, Target: "running", - Refresh: InstanceStateRefreshFunc(ec2conn, instance.InstanceId), + Refresh: InstanceStateRefreshFunc(ec2conn, *instance.InstanceID), Timeout: 10 * time.Minute, Delay: 10 * time.Second, MinTimeout: 3 * time.Second, @@ -348,16 +414,18 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error { if err != nil { return fmt.Errorf( "Error waiting for instance (%s) to become ready: %s", - instance.InstanceId, err) + *instance.InstanceID, err) } instance = instanceRaw.(*ec2.Instance) // Initialize the connection info - d.SetConnInfo(map[string]string{ - "type": "ssh", - "host": instance.PublicIpAddress, - }) + if instance.PublicIPAddress != nil { + d.SetConnInfo(map[string]string{ + "type": "ssh", + "host": *instance.PublicIPAddress, + }) + } // Set our attributes if err := resourceAwsInstanceRead(d, meta); err != nil { @@ -369,13 +437,15 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error { } func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn - resp, err := ec2conn.Instances([]string{d.Id()}, ec2.NewFilter()) + resp, err := ec2conn.DescribeInstances(&ec2.DescribeInstancesRequest{ + InstanceIDs: []string{d.Id()}, + }) if err != nil { // If the instance was not found, return nil so that we can show // that the instance is gone. - if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidInstanceID.NotFound" { + if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidInstanceID.NotFound" { d.SetId("") return nil } @@ -393,28 +463,33 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error { instance := &resp.Reservations[0].Instances[0] // If the instance is terminated, then it is gone - if instance.State.Name == "terminated" { + if *instance.State.Name == "terminated" { d.SetId("") return nil } - d.Set("availability_zone", instance.AvailZone) + d.Set("availability_zone", instance.Placement.AvailabilityZone) d.Set("key_name", instance.KeyName) - d.Set("public_dns", instance.DNSName) - d.Set("public_ip", instance.PublicIpAddress) + d.Set("public_dns", instance.PublicDNSName) + d.Set("public_ip", instance.PublicIPAddress) d.Set("private_dns", instance.PrivateDNSName) - d.Set("private_ip", instance.PrivateIpAddress) - d.Set("subnet_id", instance.SubnetId) - d.Set("ebs_optimized", instance.EbsOptimized) - d.Set("tags", tagsToMap(instance.Tags)) - d.Set("tenancy", instance.Tenancy) + d.Set("private_ip", instance.PrivateIPAddress) + d.Set("subnet_id", instance.SubnetID) + if len(instance.NetworkInterfaces) > 0 { + d.Set("subnet_id", instance.NetworkInterfaces[0].SubnetID) + } else { + d.Set("subnet_id", instance.SubnetID) + } + d.Set("ebs_optimized", instance.EBSOptimized) + d.Set("tags", tagsToMapSDK(instance.Tags)) + d.Set("tenancy", instance.Placement.Tenancy) // Determine whether we're referring to security groups with // IDs or names. We use a heuristic to figure this out. By default, // we use IDs if we're in a VPC. However, if we previously had an // all-name list of security groups, we use names. Or, if we had any // IDs, we use IDs. - useID := instance.SubnetId != "" + useID := *instance.SubnetID != "" if v := d.Get("security_groups"); v != nil { match := false for _, v := range v.(*schema.Set).List() { @@ -431,24 +506,26 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error { sgs := make([]string, len(instance.SecurityGroups)) for i, sg := range instance.SecurityGroups { if useID { - sgs[i] = sg.Id + sgs[i] = *sg.GroupID } else { - sgs[i] = sg.Name + sgs[i] = *sg.GroupName } } d.Set("security_groups", sgs) - blockDevices := make(map[string]ec2.BlockDevice) - for _, bd := range instance.BlockDevices { - blockDevices[bd.VolumeId] = bd + blockDevices := make(map[string]ec2.InstanceBlockDeviceMapping) + for _, bd := range instance.BlockDeviceMappings { + blockDevices[*bd.EBS.VolumeID] = bd } volIDs := make([]string, 0, len(blockDevices)) - for volID := range blockDevices { - volIDs = append(volIDs, volID) + for _, vol := range blockDevices { + volIDs = append(volIDs, *vol.EBS.VolumeID) } - volResp, err := ec2conn.Volumes(volIDs, ec2.NewFilter()) + volResp, err := ec2conn.DescribeVolumes(&ec2.DescribeVolumesRequest{ + VolumeIDs: volIDs, + }) if err != nil { return err } @@ -456,27 +533,25 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error { nonRootBlockDevices := make([]map[string]interface{}, 0) rootBlockDevice := make([]interface{}, 0, 1) for _, vol := range volResp.Volumes { - volSize, err := strconv.Atoi(vol.Size) - if err != nil { - return err - } - blockDevice := make(map[string]interface{}) - blockDevice["device_name"] = blockDevices[vol.VolumeId].DeviceName - blockDevice["volume_type"] = vol.VolumeType - blockDevice["volume_size"] = volSize + blockDevice["device_name"] = *blockDevices[*vol.VolumeID].DeviceName + blockDevice["volume_type"] = *vol.VolumeType + blockDevice["volume_size"] = *vol.Size + if vol.IOPS != nil { + blockDevice["iops"] = *vol.IOPS + } blockDevice["delete_on_termination"] = - blockDevices[vol.VolumeId].DeleteOnTermination + *blockDevices[*vol.VolumeID].EBS.DeleteOnTermination // If this is the root device, save it. We stop here since we // can't put invalid keys into this map. - if blockDevice["device_name"] == instance.RootDeviceName { + if blockDevice["device_name"] == *instance.RootDeviceName { rootBlockDevice = []interface{}{blockDevice} continue } - blockDevice["snapshot_id"] = vol.SnapshotId - blockDevice["encrypted"] = vol.Encrypted + blockDevice["snapshot_id"] = *vol.SnapshotID + blockDevice["encrypted"] = *vol.Encrypted nonRootBlockDevices = append(nonRootBlockDevices, blockDevice) } d.Set("block_device", nonRootBlockDevices) @@ -486,21 +561,25 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error { } func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn - opts := new(ec2.ModifyInstance) - - opts.SetSourceDestCheck = true - opts.SourceDestCheck = d.Get("source_dest_check").(bool) + ec2conn := meta.(*AWSClient).awsEC2conn + opts := new(ec2.ModifyInstanceAttributeRequest) log.Printf("[INFO] Modifying instance %s: %#v", d.Id(), opts) - if _, err := ec2conn.ModifyInstance(d.Id(), opts); err != nil { + err := ec2conn.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeRequest{ + InstanceID: aws.String(d.Id()), + SourceDestCheck: &ec2.AttributeBooleanValue{ + Value: aws.Boolean(d.Get("source_dest_check").(bool)), + }, + }) + + if err != nil { return err } // TODO(mitchellh): wait for the attributes we modified to // persist the change... - if err := setTags(ec2conn, d); err != nil { + if err := setTagsSDK(ec2conn, d); err != nil { return err } else { d.SetPartial("tags") @@ -510,10 +589,13 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error { } func resourceAwsInstanceDelete(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn log.Printf("[INFO] Terminating instance: %s", d.Id()) - if _, err := ec2conn.TerminateInstances([]string{d.Id()}); err != nil { + req := &ec2.TerminateInstancesRequest{ + InstanceIDs: []string{d.Id()}, + } + if _, err := ec2conn.TerminateInstances(req); err != nil { return fmt.Errorf("Error terminating instance: %s", err) } @@ -545,9 +627,11 @@ func resourceAwsInstanceDelete(d *schema.ResourceData, meta interface{}) error { // an EC2 instance. func InstanceStateRefreshFunc(conn *ec2.EC2, instanceID string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - resp, err := conn.Instances([]string{instanceID}, ec2.NewFilter()) + resp, err := conn.DescribeInstances(&ec2.DescribeInstancesRequest{ + InstanceIDs: []string{instanceID}, + }) if err != nil { - if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidInstanceID.NotFound" { + if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidInstanceID.NotFound" { // Set this to nil as if we didn't find anything. resp = nil } else { @@ -563,7 +647,7 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, instanceID string) resource.StateRe } i := &resp.Reservations[0].Instances[0] - return i, i.State.Name, nil + return i, *i.State.Name, nil } } diff --git a/builtin/providers/aws/resource_aws_instance_test.go b/builtin/providers/aws/resource_aws_instance_test.go index c54751f0e..3a9c16588 100644 --- a/builtin/providers/aws/resource_aws_instance_test.go +++ b/builtin/providers/aws/resource_aws_instance_test.go @@ -5,24 +5,25 @@ import ( "reflect" "testing" + "github.com/hashicorp/aws-sdk-go/aws" + "github.com/hashicorp/aws-sdk-go/gen/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/goamz/ec2" ) func TestAccAWSInstance_normal(t *testing.T) { var v ec2.Instance testCheck := func(*terraform.State) error { - if v.AvailZone != "us-west-2a" { - return fmt.Errorf("bad availability zone: %#v", v.AvailZone) + if *v.Placement.AvailabilityZone != "us-west-2a" { + return fmt.Errorf("bad availability zone: %#v", *v.Placement.AvailabilityZone) } if len(v.SecurityGroups) == 0 { return fmt.Errorf("no security groups: %#v", v.SecurityGroups) } - if v.SecurityGroups[0].Name != "tf_test_foo" { + if *v.SecurityGroups[0].GroupName != "tf_test_foo" { return fmt.Errorf("no security groups: %#v", v.SecurityGroups) } @@ -73,9 +74,9 @@ func TestAccAWSInstance_blockDevices(t *testing.T) { return func(*terraform.State) error { // Map out the block devices by name, which should be unique. - blockDevices := make(map[string]ec2.BlockDevice) - for _, blockDevice := range v.BlockDevices { - blockDevices[blockDevice.DeviceName] = blockDevice + blockDevices := make(map[string]ec2.InstanceBlockDeviceMapping) + for _, blockDevice := range v.BlockDeviceMappings { + blockDevices[*blockDevice.DeviceName] = blockDevice } // Check if the root block device exists. @@ -88,6 +89,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 +120,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(), ), }, @@ -131,8 +148,8 @@ func TestAccAWSInstance_sourceDestCheck(t *testing.T) { testCheck := func(enabled bool) resource.TestCheckFunc { return func(*terraform.State) error { - if v.SourceDestCheck != enabled { - return fmt.Errorf("bad source_dest_check: %#v", v.SourceDestCheck) + if *v.SourceDestCheck != enabled { + return fmt.Errorf("bad source_dest_check: %#v", *v.SourceDestCheck) } return nil @@ -190,7 +207,7 @@ func TestAccAWSInstance_vpc(t *testing.T) { }) } -func TestAccInstance_tags(t *testing.T) { +func TestAccAWSInstance_tags(t *testing.T) { var v ec2.Instance resource.Test(t, resource.TestCase{ @@ -202,9 +219,9 @@ func TestAccInstance_tags(t *testing.T) { Config: testAccCheckInstanceConfigTags, Check: resource.ComposeTestCheckFunc( testAccCheckInstanceExists("aws_instance.foo", &v), - testAccCheckTags(&v.Tags, "foo", "bar"), + testAccCheckTagsSDK(&v.Tags, "foo", "bar"), // Guard against regression of https://github.com/hashicorp/terraform/issues/914 - testAccCheckTags(&v.Tags, "#", ""), + testAccCheckTagsSDK(&v.Tags, "#", ""), ), }, @@ -212,21 +229,21 @@ func TestAccInstance_tags(t *testing.T) { Config: testAccCheckInstanceConfigTagsUpdate, Check: resource.ComposeTestCheckFunc( testAccCheckInstanceExists("aws_instance.foo", &v), - testAccCheckTags(&v.Tags, "foo", ""), - testAccCheckTags(&v.Tags, "bar", "baz"), + testAccCheckTagsSDK(&v.Tags, "foo", ""), + testAccCheckTagsSDK(&v.Tags, "bar", "baz"), ), }, }, }) } -func TestAccInstance_privateIP(t *testing.T) { +func TestAccAWSInstance_privateIP(t *testing.T) { var v ec2.Instance testCheckPrivateIP := func() resource.TestCheckFunc { return func(*terraform.State) error { - if v.PrivateIpAddress != "10.1.1.42" { - return fmt.Errorf("bad private IP: %s", v.PrivateIpAddress) + if *v.PrivateIPAddress != "10.1.1.42" { + return fmt.Errorf("bad private IP: %s", *v.PrivateIPAddress) } return nil @@ -249,13 +266,13 @@ func TestAccInstance_privateIP(t *testing.T) { }) } -func TestAccInstance_associatePublicIPAndPrivateIP(t *testing.T) { +func TestAccAWSInstance_associatePublicIPAndPrivateIP(t *testing.T) { var v ec2.Instance testCheckPrivateIP := func() resource.TestCheckFunc { return func(*terraform.State) error { - if v.PrivateIpAddress != "10.1.1.42" { - return fmt.Errorf("bad private IP: %s", v.PrivateIpAddress) + if *v.PrivateIPAddress != "10.1.1.42" { + return fmt.Errorf("bad private IP: %s", *v.PrivateIPAddress) } return nil @@ -279,7 +296,7 @@ func TestAccInstance_associatePublicIPAndPrivateIP(t *testing.T) { } func testAccCheckInstanceDestroy(s *terraform.State) error { - conn := testAccProvider.Meta().(*AWSClient).ec2conn + conn := testAccProvider.Meta().(*AWSClient).awsEC2conn for _, rs := range s.RootModule().Resources { if rs.Type != "aws_instance" { @@ -287,8 +304,9 @@ func testAccCheckInstanceDestroy(s *terraform.State) error { } // Try to find the resource - resp, err := conn.Instances( - []string{rs.Primary.ID}, ec2.NewFilter()) + resp, err := conn.DescribeInstances(&ec2.DescribeInstancesRequest{ + InstanceIDs: []string{rs.Primary.ID}, + }) if err == nil { if len(resp.Reservations) > 0 { return fmt.Errorf("still exist.") @@ -298,7 +316,7 @@ func testAccCheckInstanceDestroy(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 } @@ -321,9 +339,10 @@ func testAccCheckInstanceExists(n string, i *ec2.Instance) resource.TestCheckFun return fmt.Errorf("No ID is set") } - conn := testAccProvider.Meta().(*AWSClient).ec2conn - resp, err := conn.Instances( - []string{rs.Primary.ID}, ec2.NewFilter()) + conn := testAccProvider.Meta().(*AWSClient).awsEC2conn + resp, err := conn.DescribeInstances(&ec2.DescribeInstancesRequest{ + InstanceIDs: []string{rs.Primary.ID}, + }) if err != nil { return err } @@ -391,6 +410,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 + } } ` diff --git a/builtin/providers/aws/resource_aws_internet_gateway.go b/builtin/providers/aws/resource_aws_internet_gateway.go index 39aed079d..499a20ed3 100644 --- a/builtin/providers/aws/resource_aws_internet_gateway.go +++ b/builtin/providers/aws/resource_aws_internet_gateway.go @@ -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,16 +39,21 @@ 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()) + err = setTagsSDK(ec2conn, d) + if err != nil { + return err + } + // Attach the new gateway to the correct vpc return resourceAwsInternetGatewayAttach(d, meta) } 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 +70,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 +91,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 +103,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 +113,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 +137,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 +151,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 +182,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 +200,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 +246,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 +272,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 +309,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 } } diff --git a/builtin/providers/aws/resource_aws_internet_gateway_test.go b/builtin/providers/aws/resource_aws_internet_gateway_test.go index 753fb51d6..26929f466 100644 --- a/builtin/providers/aws/resource_aws_internet_gateway_test.go +++ b/builtin/providers/aws/resource_aws_internet_gateway_test.go @@ -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") } @@ -97,6 +98,7 @@ func TestAccInternetGateway_tags(t *testing.T) { Config: testAccCheckInternetGatewayConfigTags, Check: resource.ComposeTestCheckFunc( testAccCheckInternetGatewayExists("aws_internet_gateway.foo", &v), + testAccCheckTagsSDK(&v.Tags, "foo", "bar"), ), }, @@ -104,8 +106,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 +115,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 +123,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 +135,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 +158,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 } diff --git a/builtin/providers/aws/resource_aws_key_pair.go b/builtin/providers/aws/resource_aws_key_pair.go index 4b0482259..573a93567 100644 --- a/builtin/providers/aws/resource_aws_key_pair.go +++ b/builtin/providers/aws/resource_aws_key_pair.go @@ -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 } diff --git a/builtin/providers/aws/resource_aws_key_pair_test.go b/builtin/providers/aws/resource_aws_key_pair_test.go index 5474a8d25..b601d479a 100644 --- a/builtin/providers/aws/resource_aws_key_pair_test.go +++ b/builtin/providers/aws/resource_aws_key_pair_test.go @@ -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 } diff --git a/builtin/providers/aws/resource_aws_main_route_table_association.go b/builtin/providers/aws/resource_aws_main_route_table_association.go index f656f3760..a489b9a50 100644 --- a/builtin/providers/aws/resource_aws_main_route_table_association.go +++ b/builtin/providers/aws/resource_aws_main_route_table_association.go @@ -4,8 +4,9 @@ import ( "fmt" "log" + "github.com/hashicorp/aws-sdk-go/aws" + "github.com/hashicorp/aws-sdk-go/gen/ec2" "github.com/hashicorp/terraform/helper/schema" - "github.com/mitchellh/goamz/ec2" ) func resourceAwsMainRouteTableAssociation() *schema.Resource { @@ -39,7 +40,7 @@ func resourceAwsMainRouteTableAssociation() *schema.Resource { } func resourceAwsMainRouteTableAssociationCreate(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn vpcId := d.Get("vpc_id").(string) routeTableId := d.Get("route_table_id").(string) @@ -50,23 +51,23 @@ func resourceAwsMainRouteTableAssociationCreate(d *schema.ResourceData, meta int return err } - resp, err := ec2conn.ReassociateRouteTable( - mainAssociation.AssociationId, - routeTableId, - ) + resp, err := ec2conn.ReplaceRouteTableAssociation(&ec2.ReplaceRouteTableAssociationRequest{ + AssociationID: mainAssociation.RouteTableAssociationID, + RouteTableID: aws.String(routeTableId), + }) if err != nil { return err } - d.Set("original_route_table_id", mainAssociation.RouteTableId) - d.SetId(resp.AssociationId) + d.Set("original_route_table_id", mainAssociation.RouteTableID) + d.SetId(*resp.NewAssociationID) log.Printf("[INFO] New main route table association ID: %s", d.Id()) return nil } func resourceAwsMainRouteTableAssociationRead(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn mainAssociation, err := findMainRouteTableAssociation( ec2conn, @@ -75,7 +76,7 @@ func resourceAwsMainRouteTableAssociationRead(d *schema.ResourceData, meta inter return err } - if mainAssociation.AssociationId != d.Id() { + if *mainAssociation.RouteTableAssociationID != d.Id() { // It seems it doesn't exist anymore, so clear the ID d.SetId("") } @@ -87,25 +88,28 @@ func resourceAwsMainRouteTableAssociationRead(d *schema.ResourceData, meta inter // original_route_table_id - this needs to stay recorded as the AWS-created // table from VPC creation. func resourceAwsMainRouteTableAssociationUpdate(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn vpcId := d.Get("vpc_id").(string) routeTableId := d.Get("route_table_id").(string) log.Printf("[INFO] Updating main route table association: %s => %s", vpcId, routeTableId) - resp, err := ec2conn.ReassociateRouteTable(d.Id(), routeTableId) + resp, err := ec2conn.ReplaceRouteTableAssociation(&ec2.ReplaceRouteTableAssociationRequest{ + AssociationID: aws.String(d.Id()), + RouteTableID: aws.String(routeTableId), + }) if err != nil { return err } - d.SetId(resp.AssociationId) + d.SetId(*resp.NewAssociationID) log.Printf("[INFO] New main route table association ID: %s", d.Id()) return nil } func resourceAwsMainRouteTableAssociationDelete(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn vpcId := d.Get("vpc_id").(string) originalRouteTableId := d.Get("original_route_table_id").(string) @@ -113,12 +117,15 @@ func resourceAwsMainRouteTableAssociationDelete(d *schema.ResourceData, meta int vpcId, originalRouteTableId) - resp, err := ec2conn.ReassociateRouteTable(d.Id(), originalRouteTableId) + resp, err := ec2conn.ReplaceRouteTableAssociation(&ec2.ReplaceRouteTableAssociationRequest{ + AssociationID: aws.String(d.Id()), + RouteTableID: aws.String(originalRouteTableId), + }) if err != nil { return err } - log.Printf("[INFO] Resulting Association ID: %s", resp.AssociationId) + log.Printf("[INFO] Resulting Association ID: %s", *resp.NewAssociationID) return nil } @@ -130,7 +137,7 @@ func findMainRouteTableAssociation(ec2conn *ec2.EC2, vpcId string) (*ec2.RouteTa } for _, a := range mainRouteTable.Associations { - if a.Main { + if *a.Main { return &a, nil } } @@ -138,10 +145,17 @@ func findMainRouteTableAssociation(ec2conn *ec2.EC2, vpcId string) (*ec2.RouteTa } func findMainRouteTable(ec2conn *ec2.EC2, vpcId string) (*ec2.RouteTable, error) { - filter := ec2.NewFilter() - filter.Add("association.main", "true") - filter.Add("vpc-id", vpcId) - routeResp, err := ec2conn.DescribeRouteTables(nil, filter) + mainFilter := ec2.Filter{ + aws.String("association.main"), + []string{"true"}, + } + vpcFilter := ec2.Filter{ + aws.String("vpc-id"), + []string{vpcId}, + } + routeResp, err := ec2conn.DescribeRouteTables(&ec2.DescribeRouteTablesRequest{ + Filters: []ec2.Filter{mainFilter, vpcFilter}, + }) if err != nil { return nil, err } else if len(routeResp.RouteTables) != 1 { diff --git a/builtin/providers/aws/resource_aws_main_route_table_association_test.go b/builtin/providers/aws/resource_aws_main_route_table_association_test.go index 937014cae..76e3e4d72 100644 --- a/builtin/providers/aws/resource_aws_main_route_table_association_test.go +++ b/builtin/providers/aws/resource_aws_main_route_table_association_test.go @@ -65,15 +65,15 @@ func testAccCheckMainRouteTableAssociation( return fmt.Errorf("Not found: %s", vpcResource) } - conn := testAccProvider.Meta().(*AWSClient).ec2conn + conn := testAccProvider.Meta().(*AWSClient).awsEC2conn mainAssociation, err := findMainRouteTableAssociation(conn, vpc.Primary.ID) if err != nil { return err } - if mainAssociation.AssociationId != rs.Primary.ID { + if *mainAssociation.RouteTableAssociationID != rs.Primary.ID { return fmt.Errorf("Found wrong main association: %s", - mainAssociation.AssociationId) + *mainAssociation.RouteTableAssociationID) } return nil diff --git a/builtin/providers/aws/resource_aws_network_acl.go b/builtin/providers/aws/resource_aws_network_acl.go index efafd7ffe..a8f654db7 100644 --- a/builtin/providers/aws/resource_aws_network_acl.go +++ b/builtin/providers/aws/resource_aws_network_acl.go @@ -6,10 +6,11 @@ import ( "log" "time" + "github.com/hashicorp/aws-sdk-go/aws" + "github.com/hashicorp/aws-sdk-go/gen/ec2" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" - "github.com/mitchellh/goamz/ec2" ) func resourceAwsNetworkAcl() *schema.Resource { @@ -108,32 +109,34 @@ func resourceAwsNetworkAcl() *schema.Resource { func resourceAwsNetworkAclCreate(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn // Create the Network Acl - createOpts := &ec2.CreateNetworkAcl{ - VpcId: d.Get("vpc_id").(string), + createOpts := &ec2.CreateNetworkACLRequest{ + VPCID: aws.String(d.Get("vpc_id").(string)), } log.Printf("[DEBUG] Network Acl create config: %#v", createOpts) - resp, err := ec2conn.CreateNetworkAcl(createOpts) + resp, err := ec2conn.CreateNetworkACL(createOpts) if err != nil { return fmt.Errorf("Error creating network acl: %s", err) } // Get the ID and store it - networkAcl := &resp.NetworkAcl - d.SetId(networkAcl.NetworkAclId) - log.Printf("[INFO] Network Acl ID: %s", networkAcl.NetworkAclId) + networkAcl := resp.NetworkACL + d.SetId(*networkAcl.NetworkACLID) + log.Printf("[INFO] Network Acl ID: %s", *networkAcl.NetworkACLID) // Update rules and subnet association once acl is created return resourceAwsNetworkAclUpdate(d, meta) } func resourceAwsNetworkAclRead(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn - resp, err := ec2conn.NetworkAcls([]string{d.Id()}, ec2.NewFilter()) + resp, err := ec2conn.DescribeNetworkACLs(&ec2.DescribeNetworkACLsRequest{ + NetworkACLIDs: []string{d.Id()}, + }) if err != nil { return err @@ -142,29 +145,29 @@ func resourceAwsNetworkAclRead(d *schema.ResourceData, meta interface{}) error { return nil } - networkAcl := &resp.NetworkAcls[0] - var ingressEntries []ec2.NetworkAclEntry - var egressEntries []ec2.NetworkAclEntry + networkAcl := &resp.NetworkACLs[0] + var ingressEntries []ec2.NetworkACLEntry + var egressEntries []ec2.NetworkACLEntry // separate the ingress and egress rules - for _, e := range networkAcl.EntrySet { - if e.Egress == true { + for _, e := range networkAcl.Entries { + if *e.Egress == true { egressEntries = append(egressEntries, e) } else { ingressEntries = append(ingressEntries, e) } } - d.Set("vpc_id", networkAcl.VpcId) + d.Set("vpc_id", networkAcl.VPCID) d.Set("ingress", ingressEntries) d.Set("egress", egressEntries) - d.Set("tags", tagsToMap(networkAcl.Tags)) + d.Set("tags", tagsToMapSDK(networkAcl.Tags)) return nil } func resourceAwsNetworkAclUpdate(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn d.Partial(true) if d.HasChange("ingress") { @@ -190,13 +193,16 @@ func resourceAwsNetworkAclUpdate(d *schema.ResourceData, meta interface{}) error if err != nil { return fmt.Errorf("Failed to update acl %s with subnet %s: %s", d.Id(), newSubnet, err) } - _, err = ec2conn.ReplaceNetworkAclAssociation(association.NetworkAclAssociationId, d.Id()) + _, err = ec2conn.ReplaceNetworkACLAssociation(&ec2.ReplaceNetworkACLAssociationRequest{ + AssociationID: association.NetworkACLAssociationID, + NetworkACLID: aws.String(d.Id()), + }) if err != nil { return err } } - if err := setTags(ec2conn, d); err != nil { + if err := setTagsSDK(ec2conn, d); err != nil { return err } else { d.SetPartial("tags") @@ -226,7 +232,11 @@ func updateNetworkAclEntries(d *schema.ResourceData, entryType string, ec2conn * } for _, remove := range toBeDeleted { // Delete old Acl - _, err := ec2conn.DeleteNetworkAclEntry(d.Id(), remove.RuleNumber, remove.Egress) + err := ec2conn.DeleteNetworkACLEntry(&ec2.DeleteNetworkACLEntryRequest{ + NetworkACLID: aws.String(d.Id()), + RuleNumber: remove.RuleNumber, + Egress: remove.Egress, + }) if err != nil { return fmt.Errorf("Error deleting %s entry: %s", entryType, err) } @@ -238,7 +248,15 @@ func updateNetworkAclEntries(d *schema.ResourceData, entryType string, ec2conn * } for _, add := range toBeCreated { // Add new Acl entry - _, err := ec2conn.CreateNetworkAclEntry(d.Id(), &add) + err := ec2conn.CreateNetworkACLEntry(&ec2.CreateNetworkACLEntryRequest{ + NetworkACLID: aws.String(d.Id()), + CIDRBlock: add.CIDRBlock, + Egress: add.Egress, + PortRange: add.PortRange, + Protocol: add.Protocol, + RuleAction: add.RuleAction, + RuleNumber: add.RuleNumber, + }) if err != nil { return fmt.Errorf("Error creating %s entry: %s", entryType, err) } @@ -247,12 +265,15 @@ func updateNetworkAclEntries(d *schema.ResourceData, entryType string, ec2conn * } func resourceAwsNetworkAclDelete(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn log.Printf("[INFO] Deleting Network Acl: %s", d.Id()) return resource.Retry(5*time.Minute, func() error { - if _, err := ec2conn.DeleteNetworkAcl(d.Id()); err != nil { - ec2err := err.(*ec2.Error) + err := ec2conn.DeleteNetworkACL(&ec2.DeleteNetworkACLRequest{ + NetworkACLID: aws.String(d.Id()), + }) + if err != nil { + ec2err := err.(aws.APIError) switch ec2err.Code { case "InvalidNetworkAclID.NotFound": return nil @@ -267,7 +288,10 @@ func resourceAwsNetworkAclDelete(d *schema.ResourceData, meta interface{}) error if err != nil { return fmt.Errorf("Dependency violation: Cannot delete acl %s: %s", d.Id(), err) } - _, err = ec2conn.ReplaceNetworkAclAssociation(association.NetworkAclAssociationId, defaultAcl.NetworkAclId) + _, err = ec2conn.ReplaceNetworkACLAssociation(&ec2.ReplaceNetworkACLAssociationRequest{ + AssociationID: association.NetworkACLAssociationID, + NetworkACLID: defaultAcl.NetworkACLID, + }) return resource.RetryError{Err: err} default: // Any other error, we want to quit the retry loop immediately @@ -296,30 +320,43 @@ func resourceAwsNetworkAclEntryHash(v interface{}) int { return hashcode.String(buf.String()) } -func getDefaultNetworkAcl(vpc_id string, ec2conn *ec2.EC2) (defaultAcl *ec2.NetworkAcl, err error) { - filter := ec2.NewFilter() - filter.Add("default", "true") - filter.Add("vpc-id", vpc_id) - - resp, err := ec2conn.NetworkAcls([]string{}, filter) +func getDefaultNetworkAcl(vpc_id string, ec2conn *ec2.EC2) (defaultAcl *ec2.NetworkACL, err error) { + resp, err := ec2conn.DescribeNetworkACLs(&ec2.DescribeNetworkACLsRequest{ + NetworkACLIDs: []string{}, + Filters: []ec2.Filter{ + ec2.Filter{ + Name: aws.String("default"), + Values: []string{"true"}, + }, + ec2.Filter{ + Name: aws.String("vpc-id"), + Values: []string{vpc_id}, + }, + }, + }) if err != nil { return nil, err } - return &resp.NetworkAcls[0], nil + return &resp.NetworkACLs[0], nil } -func findNetworkAclAssociation(subnetId string, ec2conn *ec2.EC2) (networkAclAssociation *ec2.NetworkAclAssociation, err error) { - filter := ec2.NewFilter() - filter.Add("association.subnet-id", subnetId) - - resp, err := ec2conn.NetworkAcls([]string{}, filter) +func findNetworkAclAssociation(subnetId string, ec2conn *ec2.EC2) (networkAclAssociation *ec2.NetworkACLAssociation, err error) { + resp, err := ec2conn.DescribeNetworkACLs(&ec2.DescribeNetworkACLsRequest{ + NetworkACLIDs: []string{}, + Filters: []ec2.Filter{ + ec2.Filter{ + Name: aws.String("association.subnet-id"), + Values: []string{subnetId}, + }, + }, + }) if err != nil { return nil, err } - for _, association := range resp.NetworkAcls[0].AssociationSet { - if association.SubnetId == subnetId { + for _, association := range resp.NetworkACLs[0].Associations { + if *association.SubnetID == subnetId { return &association, nil } } diff --git a/builtin/providers/aws/resource_aws_network_acl_test.go b/builtin/providers/aws/resource_aws_network_acl_test.go index 939e8633e..a4183a1a1 100644 --- a/builtin/providers/aws/resource_aws_network_acl_test.go +++ b/builtin/providers/aws/resource_aws_network_acl_test.go @@ -4,15 +4,16 @@ import ( "fmt" "testing" + "github.com/hashicorp/aws-sdk-go/aws" + "github.com/hashicorp/aws-sdk-go/gen/ec2" "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/goamz/ec2" // "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/resource" // "github.com/hashicorp/terraform/helper/schema" ) func TestAccAWSNetworkAclsWithEgressAndIngressRules(t *testing.T) { - var networkAcl ec2.NetworkAcl + var networkAcl ec2.NetworkACL resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -24,29 +25,29 @@ func TestAccAWSNetworkAclsWithEgressAndIngressRules(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckAWSNetworkAclExists("aws_network_acl.bar", &networkAcl), resource.TestCheckResourceAttr( - "aws_network_acl.bar", "ingress.580214135.protocol", "tcp"), + "aws_network_acl.bar", "ingress.3409203205.protocol", "tcp"), resource.TestCheckResourceAttr( - "aws_network_acl.bar", "ingress.580214135.rule_no", "1"), + "aws_network_acl.bar", "ingress.3409203205.rule_no", "1"), resource.TestCheckResourceAttr( - "aws_network_acl.bar", "ingress.580214135.from_port", "80"), + "aws_network_acl.bar", "ingress.3409203205.from_port", "80"), resource.TestCheckResourceAttr( - "aws_network_acl.bar", "ingress.580214135.to_port", "80"), + "aws_network_acl.bar", "ingress.3409203205.to_port", "80"), resource.TestCheckResourceAttr( - "aws_network_acl.bar", "ingress.580214135.action", "allow"), + "aws_network_acl.bar", "ingress.3409203205.action", "allow"), resource.TestCheckResourceAttr( - "aws_network_acl.bar", "ingress.580214135.cidr_block", "10.3.10.3/18"), + "aws_network_acl.bar", "ingress.3409203205.cidr_block", "10.3.10.3/18"), resource.TestCheckResourceAttr( - "aws_network_acl.bar", "egress.1730430240.protocol", "tcp"), + "aws_network_acl.bar", "egress.2579689292.protocol", "tcp"), resource.TestCheckResourceAttr( - "aws_network_acl.bar", "egress.1730430240.rule_no", "2"), + "aws_network_acl.bar", "egress.2579689292.rule_no", "2"), resource.TestCheckResourceAttr( - "aws_network_acl.bar", "egress.1730430240.from_port", "443"), + "aws_network_acl.bar", "egress.2579689292.from_port", "443"), resource.TestCheckResourceAttr( - "aws_network_acl.bar", "egress.1730430240.to_port", "443"), + "aws_network_acl.bar", "egress.2579689292.to_port", "443"), resource.TestCheckResourceAttr( - "aws_network_acl.bar", "egress.1730430240.cidr_block", "10.3.2.3/18"), + "aws_network_acl.bar", "egress.2579689292.cidr_block", "10.3.2.3/18"), resource.TestCheckResourceAttr( - "aws_network_acl.bar", "egress.1730430240.action", "allow"), + "aws_network_acl.bar", "egress.2579689292.action", "allow"), ), }, }, @@ -54,7 +55,7 @@ func TestAccAWSNetworkAclsWithEgressAndIngressRules(t *testing.T) { } func TestAccAWSNetworkAclsOnlyIngressRules(t *testing.T) { - var networkAcl ec2.NetworkAcl + var networkAcl ec2.NetworkACL resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -67,17 +68,17 @@ func TestAccAWSNetworkAclsOnlyIngressRules(t *testing.T) { testAccCheckAWSNetworkAclExists("aws_network_acl.foos", &networkAcl), // testAccCheckSubnetAssociation("aws_network_acl.foos", "aws_subnet.blob"), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.3697634361.protocol", "tcp"), + "aws_network_acl.foos", "ingress.2750166237.protocol", "tcp"), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.3697634361.rule_no", "1"), + "aws_network_acl.foos", "ingress.2750166237.rule_no", "1"), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.3697634361.from_port", "0"), + "aws_network_acl.foos", "ingress.2750166237.from_port", "0"), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.3697634361.to_port", "22"), + "aws_network_acl.foos", "ingress.2750166237.to_port", "22"), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.3697634361.action", "deny"), + "aws_network_acl.foos", "ingress.2750166237.action", "deny"), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.3697634361.cidr_block", "10.2.2.3/18"), + "aws_network_acl.foos", "ingress.2750166237.cidr_block", "10.2.2.3/18"), ), }, }, @@ -85,7 +86,7 @@ func TestAccAWSNetworkAclsOnlyIngressRules(t *testing.T) { } func TestAccAWSNetworkAclsOnlyIngressRulesChange(t *testing.T) { - var networkAcl ec2.NetworkAcl + var networkAcl ec2.NetworkACL resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -98,21 +99,21 @@ func TestAccAWSNetworkAclsOnlyIngressRulesChange(t *testing.T) { testAccCheckAWSNetworkAclExists("aws_network_acl.foos", &networkAcl), testIngressRuleLength(&networkAcl, 2), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.3697634361.protocol", "tcp"), + "aws_network_acl.foos", "ingress.37211640.protocol", "tcp"), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.3697634361.rule_no", "1"), + "aws_network_acl.foos", "ingress.37211640.rule_no", "1"), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.3697634361.from_port", "0"), + "aws_network_acl.foos", "ingress.37211640.from_port", "0"), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.3697634361.to_port", "22"), + "aws_network_acl.foos", "ingress.37211640.to_port", "22"), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.3697634361.action", "deny"), + "aws_network_acl.foos", "ingress.37211640.action", "deny"), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.3697634361.cidr_block", "10.2.2.3/18"), + "aws_network_acl.foos", "ingress.37211640.cidr_block", "10.2.2.3/18"), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.2438803013.from_port", "443"), + "aws_network_acl.foos", "ingress.2750166237.from_port", "443"), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.2438803013.rule_no", "2"), + "aws_network_acl.foos", "ingress.2750166237.rule_no", "2"), ), }, resource.TestStep{ @@ -121,17 +122,17 @@ func TestAccAWSNetworkAclsOnlyIngressRulesChange(t *testing.T) { testAccCheckAWSNetworkAclExists("aws_network_acl.foos", &networkAcl), testIngressRuleLength(&networkAcl, 1), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.3697634361.protocol", "tcp"), + "aws_network_acl.foos", "ingress.37211640.protocol", "tcp"), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.3697634361.rule_no", "1"), + "aws_network_acl.foos", "ingress.37211640.rule_no", "1"), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.3697634361.from_port", "0"), + "aws_network_acl.foos", "ingress.37211640.from_port", "0"), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.3697634361.to_port", "22"), + "aws_network_acl.foos", "ingress.37211640.to_port", "22"), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.3697634361.action", "deny"), + "aws_network_acl.foos", "ingress.37211640.action", "deny"), resource.TestCheckResourceAttr( - "aws_network_acl.foos", "ingress.3697634361.cidr_block", "10.2.2.3/18"), + "aws_network_acl.foos", "ingress.37211640.cidr_block", "10.2.2.3/18"), ), }, }, @@ -139,7 +140,7 @@ func TestAccAWSNetworkAclsOnlyIngressRulesChange(t *testing.T) { } func TestAccAWSNetworkAclsOnlyEgressRules(t *testing.T) { - var networkAcl ec2.NetworkAcl + var networkAcl ec2.NetworkACL resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -150,14 +151,14 @@ func TestAccAWSNetworkAclsOnlyEgressRules(t *testing.T) { Config: testAccAWSNetworkAclEgressConfig, Check: resource.ComposeTestCheckFunc( testAccCheckAWSNetworkAclExists("aws_network_acl.bond", &networkAcl), - testAccCheckTags(&networkAcl.Tags, "foo", "bar"), + testAccCheckTagsSDK(&networkAcl.Tags, "foo", "bar"), ), }, }, }) } -func TestAccNetworkAcl_SubnetChange(t *testing.T) { +func TestAccAWSNetworkAcl_SubnetChange(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -183,7 +184,7 @@ func TestAccNetworkAcl_SubnetChange(t *testing.T) { } func testAccCheckAWSNetworkAclDestroy(s *terraform.State) error { - conn := testAccProvider.Meta().(*AWSClient).ec2conn + conn := testAccProvider.Meta().(*AWSClient).awsEC2conn for _, rs := range s.RootModule().Resources { if rs.Type != "aws_network" { @@ -191,16 +192,18 @@ func testAccCheckAWSNetworkAclDestroy(s *terraform.State) error { } // Retrieve the network acl - resp, err := conn.NetworkAcls([]string{rs.Primary.ID}, ec2.NewFilter()) + resp, err := conn.DescribeNetworkACLs(&ec2.DescribeNetworkACLsRequest{ + NetworkACLIDs: []string{rs.Primary.ID}, + }) if err == nil { - if len(resp.NetworkAcls) > 0 && resp.NetworkAcls[0].NetworkAclId == rs.Primary.ID { + if len(resp.NetworkACLs) > 0 && *resp.NetworkACLs[0].NetworkACLID == rs.Primary.ID { return fmt.Errorf("Network Acl (%s) still exists.", rs.Primary.ID) } return nil } - ec2err, ok := err.(*ec2.Error) + ec2err, ok := err.(aws.APIError) if !ok { return err } @@ -213,7 +216,7 @@ func testAccCheckAWSNetworkAclDestroy(s *terraform.State) error { return nil } -func testAccCheckAWSNetworkAclExists(n string, networkAcl *ec2.NetworkAcl) resource.TestCheckFunc { +func testAccCheckAWSNetworkAclExists(n string, networkAcl *ec2.NetworkACL) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -223,15 +226,17 @@ func testAccCheckAWSNetworkAclExists(n string, networkAcl *ec2.NetworkAcl) resou if rs.Primary.ID == "" { return fmt.Errorf("No Security Group is set") } - conn := testAccProvider.Meta().(*AWSClient).ec2conn + conn := testAccProvider.Meta().(*AWSClient).awsEC2conn - resp, err := conn.NetworkAcls([]string{rs.Primary.ID}, nil) + resp, err := conn.DescribeNetworkACLs(&ec2.DescribeNetworkACLsRequest{ + NetworkACLIDs: []string{rs.Primary.ID}, + }) if err != nil { return err } - if len(resp.NetworkAcls) > 0 && resp.NetworkAcls[0].NetworkAclId == rs.Primary.ID { - *networkAcl = resp.NetworkAcls[0] + if len(resp.NetworkACLs) > 0 && *resp.NetworkACLs[0].NetworkACLID == rs.Primary.ID { + *networkAcl = resp.NetworkACLs[0] return nil } @@ -239,11 +244,11 @@ func testAccCheckAWSNetworkAclExists(n string, networkAcl *ec2.NetworkAcl) resou } } -func testIngressRuleLength(networkAcl *ec2.NetworkAcl, length int) resource.TestCheckFunc { +func testIngressRuleLength(networkAcl *ec2.NetworkACL, length int) resource.TestCheckFunc { return func(s *terraform.State) error { - var ingressEntries []ec2.NetworkAclEntry - for _, e := range networkAcl.EntrySet { - if e.Egress == false { + var ingressEntries []ec2.NetworkACLEntry + for _, e := range networkAcl.Entries { + if *e.Egress == false { ingressEntries = append(ingressEntries, e) } } @@ -261,21 +266,26 @@ func testAccCheckSubnetIsAssociatedWithAcl(acl string, sub string) resource.Test networkAcl := s.RootModule().Resources[acl] subnet := s.RootModule().Resources[sub] - conn := testAccProvider.Meta().(*AWSClient).ec2conn - filter := ec2.NewFilter() - filter.Add("association.subnet-id", subnet.Primary.ID) - resp, err := conn.NetworkAcls([]string{networkAcl.Primary.ID}, filter) - + conn := testAccProvider.Meta().(*AWSClient).awsEC2conn + resp, err := conn.DescribeNetworkACLs(&ec2.DescribeNetworkACLsRequest{ + NetworkACLIDs: []string{networkAcl.Primary.ID}, + Filters: []ec2.Filter{ + ec2.Filter{ + Name: aws.String("association.subnet-id"), + Values: []string{subnet.Primary.ID}, + }, + }, + }) if err != nil { return err } - if len(resp.NetworkAcls) > 0 { + if len(resp.NetworkACLs) > 0 { return nil } - r, _ := conn.NetworkAcls([]string{}, ec2.NewFilter()) - fmt.Printf("\n\nall acls\n %#v\n\n", r.NetworkAcls) - conn.NetworkAcls([]string{}, filter) + // r, _ := conn.NetworkACLs([]string{}, ec2.NewFilter()) + // fmt.Printf("\n\nall acls\n %#v\n\n", r.NetworkAcls) + // conn.NetworkAcls([]string{}, filter) return fmt.Errorf("Network Acl %s is not associated with subnet %s", acl, sub) } @@ -286,15 +296,21 @@ func testAccCheckSubnetIsNotAssociatedWithAcl(acl string, subnet string) resourc networkAcl := s.RootModule().Resources[acl] subnet := s.RootModule().Resources[subnet] - conn := testAccProvider.Meta().(*AWSClient).ec2conn - filter := ec2.NewFilter() - filter.Add("association.subnet-id", subnet.Primary.ID) - resp, err := conn.NetworkAcls([]string{networkAcl.Primary.ID}, filter) + conn := testAccProvider.Meta().(*AWSClient).awsEC2conn + resp, err := conn.DescribeNetworkACLs(&ec2.DescribeNetworkACLsRequest{ + NetworkACLIDs: []string{networkAcl.Primary.ID}, + Filters: []ec2.Filter{ + ec2.Filter{ + Name: aws.String("association.subnet-id"), + Values: []string{subnet.Primary.ID}, + }, + }, + }) if err != nil { return err } - if len(resp.NetworkAcls) > 0 { + if len(resp.NetworkACLs) > 0 { return fmt.Errorf("Network Acl %s is still associated with subnet %s", acl, subnet) } return nil diff --git a/builtin/providers/aws/resource_aws_route53_record.go b/builtin/providers/aws/resource_aws_route53_record.go index 96c7608fe..fcd781c61 100644 --- a/builtin/providers/aws/resource_aws_route53_record.go +++ b/builtin/providers/aws/resource_aws_route53_record.go @@ -138,7 +138,7 @@ func resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) er Delay: 30 * time.Second, Pending: []string{"PENDING"}, Target: "INSYNC", - Timeout: 10 * time.Minute, + Timeout: 30 * time.Minute, MinTimeout: 5 * time.Second, Refresh: func() (result interface{}, state string, err error) { changeRequest := &route53.GetChangeRequest{ diff --git a/builtin/providers/aws/resource_aws_route_table.go b/builtin/providers/aws/resource_aws_route_table.go index 9d01218b0..0290d053f 100644 --- a/builtin/providers/aws/resource_aws_route_table.go +++ b/builtin/providers/aws/resource_aws_route_table.go @@ -6,10 +6,11 @@ import ( "log" "time" + "github.com/hashicorp/aws-sdk-go/aws" + "github.com/hashicorp/aws-sdk-go/gen/ec2" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" - "github.com/mitchellh/goamz/ec2" ) func resourceAwsRouteTable() *schema.Resource { @@ -61,11 +62,11 @@ func resourceAwsRouteTable() *schema.Resource { } func resourceAwsRouteTableCreate(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn // Create the routing table - createOpts := &ec2.CreateRouteTable{ - VpcId: d.Get("vpc_id").(string), + createOpts := &ec2.CreateRouteTableRequest{ + VPCID: aws.String(d.Get("vpc_id").(string)), } log.Printf("[DEBUG] RouteTable create config: %#v", createOpts) @@ -75,8 +76,8 @@ func resourceAwsRouteTableCreate(d *schema.ResourceData, meta interface{}) error } // Get the ID and store it - rt := &resp.RouteTable - d.SetId(rt.RouteTableId) + rt := resp.RouteTable + d.SetId(*rt.RouteTableID) log.Printf("[INFO] Route Table ID: %s", d.Id()) // Wait for the route table to become available @@ -99,7 +100,7 @@ func resourceAwsRouteTableCreate(d *schema.ResourceData, meta interface{}) error } func resourceAwsRouteTableRead(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn rtRaw, _, err := resourceAwsRouteTableStateRefreshFunc(ec2conn, d.Id())() if err != nil { @@ -110,40 +111,48 @@ func resourceAwsRouteTableRead(d *schema.ResourceData, meta interface{}) error { } rt := rtRaw.(*ec2.RouteTable) - d.Set("vpc_id", rt.VpcId) + d.Set("vpc_id", rt.VPCID) // Create an empty schema.Set to hold all routes route := &schema.Set{F: resourceAwsRouteTableHash} // Loop through the routes and add them to the set for _, r := range rt.Routes { - if r.GatewayId == "local" { + if r.GatewayID != nil && *r.GatewayID == "local" { continue } - if r.Origin == "EnableVgwRoutePropagation" { + if r.Origin != nil && *r.Origin == "EnableVgwRoutePropagation" { continue } m := make(map[string]interface{}) - m["cidr_block"] = r.DestinationCidrBlock - m["gateway_id"] = r.GatewayId - m["instance_id"] = r.InstanceId - m["vpc_peering_connection_id"] = r.VpcPeeringConnectionId + if r.DestinationCIDRBlock != nil { + m["cidr_block"] = *r.DestinationCIDRBlock + } + if r.GatewayID != nil { + m["gateway_id"] = *r.GatewayID + } + if r.InstanceID != nil { + m["instance_id"] = *r.InstanceID + } + if r.VPCPeeringConnectionID != nil { + m["vpc_peering_connection_id"] = *r.VPCPeeringConnectionID + } route.Add(m) } d.Set("route", route) // Tags - d.Set("tags", tagsToMap(rt.Tags)) + d.Set("tags", tagsToMapSDK(rt.Tags)) return nil } func resourceAwsRouteTableUpdate(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn // Check if the route set as a whole has changed if d.HasChange("route") { @@ -159,8 +168,10 @@ func resourceAwsRouteTableUpdate(d *schema.ResourceData, meta interface{}) error log.Printf( "[INFO] Deleting route from %s: %s", d.Id(), m["cidr_block"].(string)) - _, err := ec2conn.DeleteRoute( - d.Id(), m["cidr_block"].(string)) + err := ec2conn.DeleteRoute(&ec2.DeleteRouteRequest{ + RouteTableID: aws.String(d.Id()), + DestinationCIDRBlock: aws.String(m["cidr_block"].(string)), + }) if err != nil { return err } @@ -174,17 +185,16 @@ func resourceAwsRouteTableUpdate(d *schema.ResourceData, meta interface{}) error for _, route := range nrs.List() { m := route.(map[string]interface{}) - opts := ec2.CreateRoute{ - RouteTableId: d.Id(), - DestinationCidrBlock: m["cidr_block"].(string), - GatewayId: m["gateway_id"].(string), - InstanceId: m["instance_id"].(string), - VpcPeeringConnectionId: m["vpc_peering_connection_id"].(string), + opts := ec2.CreateRouteRequest{ + RouteTableID: aws.String(d.Id()), + DestinationCIDRBlock: aws.String(m["cidr_block"].(string)), + GatewayID: aws.String(m["gateway_id"].(string)), + InstanceID: aws.String(m["instance_id"].(string)), + VPCPeeringConnectionID: aws.String(m["vpc_peering_connection_id"].(string)), } log.Printf("[INFO] Creating route for %s: %#v", d.Id(), opts) - _, err := ec2conn.CreateRoute(&opts) - if err != nil { + if err := ec2conn.CreateRoute(&opts); err != nil { return err } @@ -193,7 +203,7 @@ func resourceAwsRouteTableUpdate(d *schema.ResourceData, meta interface{}) error } } - if err := setTags(ec2conn, d); err != nil { + if err := setTagsSDK(ec2conn, d); err != nil { return err } else { d.SetPartial("tags") @@ -203,7 +213,7 @@ func resourceAwsRouteTableUpdate(d *schema.ResourceData, meta interface{}) error } func resourceAwsRouteTableDelete(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn // First request the routing table since we'll have to disassociate // all the subnets first. @@ -218,16 +228,22 @@ func resourceAwsRouteTableDelete(d *schema.ResourceData, meta interface{}) error // Do all the disassociations for _, a := range rt.Associations { - log.Printf("[INFO] Disassociating association: %s", a.AssociationId) - if _, err := ec2conn.DisassociateRouteTable(a.AssociationId); err != nil { + log.Printf("[INFO] Disassociating association: %s", *a.RouteTableAssociationID) + err := ec2conn.DisassociateRouteTable(&ec2.DisassociateRouteTableRequest{ + AssociationID: a.RouteTableAssociationID, + }) + if err != nil { return err } } // Delete the route table log.Printf("[INFO] Deleting Route Table: %s", d.Id()) - if _, err := ec2conn.DeleteRouteTable(d.Id()); err != nil { - ec2err, ok := err.(*ec2.Error) + err = ec2conn.DeleteRouteTable(&ec2.DeleteRouteTableRequest{ + RouteTableID: aws.String(d.Id()), + }) + if err != nil { + ec2err, ok := err.(aws.APIError) if ok && ec2err.Code == "InvalidRouteTableID.NotFound" { return nil } @@ -279,9 +295,11 @@ func resourceAwsRouteTableHash(v interface{}) int { // a RouteTable. func resourceAwsRouteTableStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - resp, err := conn.DescribeRouteTables([]string{id}, ec2.NewFilter()) + resp, err := conn.DescribeRouteTables(&ec2.DescribeRouteTablesRequest{ + RouteTableIDs: []string{id}, + }) if err != nil { - if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidRouteTableID.NotFound" { + if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidRouteTableID.NotFound" { resp = nil } else { log.Printf("Error on RouteTableStateRefresh: %s", err) diff --git a/builtin/providers/aws/resource_aws_route_table_association.go b/builtin/providers/aws/resource_aws_route_table_association.go index 846836008..a9a614f7f 100644 --- a/builtin/providers/aws/resource_aws_route_table_association.go +++ b/builtin/providers/aws/resource_aws_route_table_association.go @@ -4,8 +4,9 @@ import ( "fmt" "log" + "github.com/hashicorp/aws-sdk-go/aws" + "github.com/hashicorp/aws-sdk-go/gen/ec2" "github.com/hashicorp/terraform/helper/schema" - "github.com/mitchellh/goamz/ec2" ) func resourceAwsRouteTableAssociation() *schema.Resource { @@ -31,30 +32,31 @@ func resourceAwsRouteTableAssociation() *schema.Resource { } func resourceAwsRouteTableAssociationCreate(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn log.Printf( "[INFO] Creating route table association: %s => %s", d.Get("subnet_id").(string), d.Get("route_table_id").(string)) - resp, err := ec2conn.AssociateRouteTable( - d.Get("route_table_id").(string), - d.Get("subnet_id").(string)) + resp, err := ec2conn.AssociateRouteTable(&ec2.AssociateRouteTableRequest{ + RouteTableID: aws.String(d.Get("route_table_id").(string)), + SubnetID: aws.String(d.Get("subnet_id").(string)), + }) if err != nil { return err } // Set the ID and return - d.SetId(resp.AssociationId) + d.SetId(*resp.AssociationID) log.Printf("[INFO] Association ID: %s", d.Id()) return nil } func resourceAwsRouteTableAssociationRead(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn // Get the routing table that this association belongs to rtRaw, _, err := resourceAwsRouteTableStateRefreshFunc( @@ -70,9 +72,9 @@ func resourceAwsRouteTableAssociationRead(d *schema.ResourceData, meta interface // Inspect that the association exists found := false for _, a := range rt.Associations { - if a.AssociationId == d.Id() { + if *a.RouteTableAssociationID == d.Id() { found = true - d.Set("subnet_id", a.SubnetId) + d.Set("subnet_id", *a.SubnetID) break } } @@ -86,19 +88,21 @@ func resourceAwsRouteTableAssociationRead(d *schema.ResourceData, meta interface } func resourceAwsRouteTableAssociationUpdate(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn log.Printf( "[INFO] Creating route table association: %s => %s", d.Get("subnet_id").(string), d.Get("route_table_id").(string)) - resp, err := ec2conn.ReassociateRouteTable( - d.Id(), - d.Get("route_table_id").(string)) + req := &ec2.ReplaceRouteTableAssociationRequest{ + AssociationID: aws.String(d.Id()), + RouteTableID: aws.String(d.Get("route_table_id").(string)), + } + resp, err := ec2conn.ReplaceRouteTableAssociation(req) if err != nil { - ec2err, ok := err.(*ec2.Error) + ec2err, ok := err.(aws.APIError) if ok && ec2err.Code == "InvalidAssociationID.NotFound" { // Not found, so just create a new one return resourceAwsRouteTableAssociationCreate(d, meta) @@ -108,18 +112,21 @@ func resourceAwsRouteTableAssociationUpdate(d *schema.ResourceData, meta interfa } // Update the ID - d.SetId(resp.AssociationId) + d.SetId(*resp.NewAssociationID) log.Printf("[INFO] Association ID: %s", d.Id()) return nil } func resourceAwsRouteTableAssociationDelete(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn log.Printf("[INFO] Deleting route table association: %s", d.Id()) - if _, err := ec2conn.DisassociateRouteTable(d.Id()); err != nil { - ec2err, ok := err.(*ec2.Error) + err := ec2conn.DisassociateRouteTable(&ec2.DisassociateRouteTableRequest{ + AssociationID: aws.String(d.Id()), + }) + if err != nil { + ec2err, ok := err.(aws.APIError) if ok && ec2err.Code == "InvalidAssociationID.NotFound" { return nil } diff --git a/builtin/providers/aws/resource_aws_route_table_association_test.go b/builtin/providers/aws/resource_aws_route_table_association_test.go index 079fb41f8..8c4246aba 100644 --- a/builtin/providers/aws/resource_aws_route_table_association_test.go +++ b/builtin/providers/aws/resource_aws_route_table_association_test.go @@ -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 TestAccAWSRouteTableAssociation(t *testing.T) { @@ -37,7 +38,7 @@ func TestAccAWSRouteTableAssociation(t *testing.T) { } func testAccCheckRouteTableAssociationDestroy(s *terraform.State) error { - conn := testAccProvider.Meta().(*AWSClient).ec2conn + conn := testAccProvider.Meta().(*AWSClient).awsEC2conn for _, rs := range s.RootModule().Resources { if rs.Type != "aws_route_table_association" { @@ -45,11 +46,12 @@ func testAccCheckRouteTableAssociationDestroy(s *terraform.State) error { } // Try to find the resource - resp, err := conn.DescribeRouteTables( - []string{rs.Primary.Attributes["route_table_Id"]}, ec2.NewFilter()) + resp, err := conn.DescribeRouteTables(&ec2.DescribeRouteTablesRequest{ + RouteTableIDs: []string{rs.Primary.Attributes["route_table_id"]}, + }) if err != nil { // Verify the error is what we want - ec2err, ok := err.(*ec2.Error) + ec2err, ok := err.(aws.APIError) if !ok { return err } @@ -62,7 +64,7 @@ func testAccCheckRouteTableAssociationDestroy(s *terraform.State) error { rt := resp.RouteTables[0] if len(rt.Associations) > 0 { return fmt.Errorf( - "route table %s has associations", rt.RouteTableId) + "route table %s has associations", *rt.RouteTableID) } } @@ -81,9 +83,10 @@ func testAccCheckRouteTableAssociationExists(n string, v *ec2.RouteTable) resour return fmt.Errorf("No ID is set") } - conn := testAccProvider.Meta().(*AWSClient).ec2conn - resp, err := conn.DescribeRouteTables( - []string{rs.Primary.Attributes["route_table_id"]}, ec2.NewFilter()) + conn := testAccProvider.Meta().(*AWSClient).awsEC2conn + resp, err := conn.DescribeRouteTables(&ec2.DescribeRouteTablesRequest{ + RouteTableIDs: []string{rs.Primary.Attributes["route_table_id"]}, + }) if err != nil { return err } diff --git a/builtin/providers/aws/resource_aws_route_table_test.go b/builtin/providers/aws/resource_aws_route_table_test.go index 2f4dfab2e..5e6f90026 100644 --- a/builtin/providers/aws/resource_aws_route_table_test.go +++ b/builtin/providers/aws/resource_aws_route_table_test.go @@ -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 TestAccAWSRouteTable_normal(t *testing.T) { @@ -19,7 +20,7 @@ func TestAccAWSRouteTable_normal(t *testing.T) { routes := make(map[string]ec2.Route) for _, r := range v.Routes { - routes[r.DestinationCidrBlock] = r + routes[*r.DestinationCIDRBlock] = r } if _, ok := routes["10.1.0.0/16"]; !ok { @@ -39,7 +40,7 @@ func TestAccAWSRouteTable_normal(t *testing.T) { routes := make(map[string]ec2.Route) for _, r := range v.Routes { - routes[r.DestinationCidrBlock] = r + routes[*r.DestinationCIDRBlock] = r } if _, ok := routes["10.1.0.0/16"]; !ok { @@ -91,7 +92,7 @@ func TestAccAWSRouteTable_instance(t *testing.T) { routes := make(map[string]ec2.Route) for _, r := range v.Routes { - routes[r.DestinationCidrBlock] = r + routes[*r.DestinationCIDRBlock] = r } if _, ok := routes["10.1.0.0/16"]; !ok { @@ -133,7 +134,7 @@ func TestAccAWSRouteTable_tags(t *testing.T) { Config: testAccRouteTableConfigTags, Check: resource.ComposeTestCheckFunc( testAccCheckRouteTableExists("aws_route_table.foo", &route_table), - testAccCheckTags(&route_table.Tags, "foo", "bar"), + testAccCheckTagsSDK(&route_table.Tags, "foo", "bar"), ), }, @@ -141,8 +142,8 @@ func TestAccAWSRouteTable_tags(t *testing.T) { Config: testAccRouteTableConfigTagsUpdate, Check: resource.ComposeTestCheckFunc( testAccCheckRouteTableExists("aws_route_table.foo", &route_table), - testAccCheckTags(&route_table.Tags, "foo", ""), - testAccCheckTags(&route_table.Tags, "bar", "baz"), + testAccCheckTagsSDK(&route_table.Tags, "foo", ""), + testAccCheckTagsSDK(&route_table.Tags, "bar", "baz"), ), }, }, @@ -150,7 +151,7 @@ func TestAccAWSRouteTable_tags(t *testing.T) { } func testAccCheckRouteTableDestroy(s *terraform.State) error { - conn := testAccProvider.Meta().(*AWSClient).ec2conn + conn := testAccProvider.Meta().(*AWSClient).awsEC2conn for _, rs := range s.RootModule().Resources { if rs.Type != "aws_route_table" { @@ -158,8 +159,9 @@ func testAccCheckRouteTableDestroy(s *terraform.State) error { } // Try to find the resource - resp, err := conn.DescribeRouteTables( - []string{rs.Primary.ID}, ec2.NewFilter()) + resp, err := conn.DescribeRouteTables(&ec2.DescribeRouteTablesRequest{ + RouteTableIDs: []string{rs.Primary.ID}, + }) if err == nil { if len(resp.RouteTables) > 0 { return fmt.Errorf("still exist.") @@ -169,7 +171,7 @@ func testAccCheckRouteTableDestroy(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 } @@ -192,9 +194,10 @@ func testAccCheckRouteTableExists(n string, v *ec2.RouteTable) resource.TestChec return fmt.Errorf("No ID is set") } - conn := testAccProvider.Meta().(*AWSClient).ec2conn - resp, err := conn.DescribeRouteTables( - []string{rs.Primary.ID}, ec2.NewFilter()) + conn := testAccProvider.Meta().(*AWSClient).awsEC2conn + resp, err := conn.DescribeRouteTables(&ec2.DescribeRouteTablesRequest{ + RouteTableIDs: []string{rs.Primary.ID}, + }) if err != nil { return err } @@ -208,7 +211,10 @@ func testAccCheckRouteTableExists(n string, v *ec2.RouteTable) resource.TestChec } } -func TestAccAWSRouteTable_vpcPeering(t *testing.T) { +// TODO: re-enable this test. +// VPC Peering connections are prefixed with pcx +// Right now there is no VPC Peering resource +func _TestAccAWSRouteTable_vpcPeering(t *testing.T) { var v ec2.RouteTable testCheck := func(*terraform.State) error { @@ -218,7 +224,7 @@ func TestAccAWSRouteTable_vpcPeering(t *testing.T) { routes := make(map[string]ec2.Route) for _, r := range v.Routes { - routes[r.DestinationCidrBlock] = r + routes[*r.DestinationCIDRBlock] = r } if _, ok := routes["10.1.0.0/16"]; !ok { @@ -345,6 +351,9 @@ resource "aws_route_table" "foo" { } ` +// TODO: re-enable this test. +// VPC Peering connections are prefixed with pcx +// Right now there is no VPC Peering resource const testAccRouteTableVpcPeeringConfig = ` resource "aws_vpc" "foo" { cidr_block = "10.1.0.0/16" @@ -359,7 +368,7 @@ resource "aws_route_table" "foo" { route { cidr_block = "10.2.0.0/16" - vpc_peering_connection_id = "vpc-12345" + vpc_peering_connection_id = "pcx-12345" } } ` diff --git a/builtin/providers/aws/resource_aws_security_group.go b/builtin/providers/aws/resource_aws_security_group.go index 451f1816f..b7addd9aa 100644 --- a/builtin/providers/aws/resource_aws_security_group.go +++ b/builtin/providers/aws/resource_aws_security_group.go @@ -7,10 +7,11 @@ import ( "sort" "time" + "github.com/hashicorp/aws-sdk-go/aws" + "github.com/hashicorp/aws-sdk-go/gen/ec2" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" - "github.com/mitchellh/goamz/ec2" ) func resourceAwsSecurityGroup() *schema.Resource { @@ -141,18 +142,18 @@ func resourceAwsSecurityGroup() *schema.Resource { } func resourceAwsSecurityGroupCreate(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn - securityGroupOpts := ec2.SecurityGroup{ - Name: d.Get("name").(string), + securityGroupOpts := &ec2.CreateSecurityGroupRequest{ + GroupName: aws.String(d.Get("name").(string)), } if v := d.Get("vpc_id"); v != nil { - securityGroupOpts.VpcId = v.(string) + securityGroupOpts.VPCID = aws.String(v.(string)) } if v := d.Get("description"); v != nil { - securityGroupOpts.Description = v.(string) + securityGroupOpts.Description = aws.String(v.(string)) } log.Printf( @@ -162,7 +163,7 @@ func resourceAwsSecurityGroupCreate(d *schema.ResourceData, meta interface{}) er return fmt.Errorf("Error creating Security Group: %s", err) } - d.SetId(createResp.Id) + d.SetId(*createResp.GroupID) log.Printf("[INFO] Security Group ID: %s", d.Id()) @@ -186,7 +187,7 @@ func resourceAwsSecurityGroupCreate(d *schema.ResourceData, meta interface{}) er } func resourceAwsSecurityGroupRead(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn sgRaw, _, err := SGStateRefreshFunc(ec2conn, d.Id())() if err != nil { @@ -197,24 +198,23 @@ func resourceAwsSecurityGroupRead(d *schema.ResourceData, meta interface{}) erro return nil } - sg := sgRaw.(*ec2.SecurityGroupInfo) + sg := sgRaw.(ec2.SecurityGroup) - ingressRules := resourceAwsSecurityGroupIPPermGather(d, sg.IPPerms) - egressRules := resourceAwsSecurityGroupIPPermGather(d, sg.IPPermsEgress) + ingressRules := resourceAwsSecurityGroupIPPermGather(d, sg.IPPermissions) + egressRules := resourceAwsSecurityGroupIPPermGather(d, sg.IPPermissionsEgress) d.Set("description", sg.Description) - d.Set("name", sg.Name) - d.Set("vpc_id", sg.VpcId) - d.Set("owner_id", sg.OwnerId) + d.Set("name", sg.GroupName) + d.Set("vpc_id", sg.VPCID) + d.Set("owner_id", sg.OwnerID) d.Set("ingress", ingressRules) d.Set("egress", egressRules) - d.Set("tags", tagsToMap(sg.Tags)) - + d.Set("tags", tagsToMapSDK(sg.Tags)) return nil } func resourceAwsSecurityGroupUpdate(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn sgRaw, _, err := SGStateRefreshFunc(ec2conn, d.Id())() if err != nil { @@ -224,7 +224,8 @@ func resourceAwsSecurityGroupUpdate(d *schema.ResourceData, meta interface{}) er d.SetId("") return nil } - group := sgRaw.(*ec2.SecurityGroupInfo).SecurityGroup + + group := sgRaw.(ec2.SecurityGroup) err = resourceAwsSecurityGroupUpdateRules(d, "ingress", meta, group) if err != nil { @@ -238,7 +239,7 @@ func resourceAwsSecurityGroupUpdate(d *schema.ResourceData, meta interface{}) er } } - if err := setTags(ec2conn, d); err != nil { + if err := setTagsSDK(ec2conn, d); err != nil { return err } @@ -248,14 +249,16 @@ func resourceAwsSecurityGroupUpdate(d *schema.ResourceData, meta interface{}) er } func resourceAwsSecurityGroupDelete(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn log.Printf("[DEBUG] Security Group destroy: %v", d.Id()) return resource.Retry(5*time.Minute, func() error { - _, err := ec2conn.DeleteSecurityGroup(ec2.SecurityGroup{Id: d.Id()}) + err := ec2conn.DeleteSecurityGroup(&ec2.DeleteSecurityGroupRequest{ + GroupID: aws.String(d.Id()), + }) if err != nil { - ec2err, ok := err.(*ec2.Error) + ec2err, ok := err.(aws.APIError) if !ok { return err } @@ -313,34 +316,45 @@ func resourceAwsSecurityGroupRuleHash(v interface{}) int { return hashcode.String(buf.String()) } -func resourceAwsSecurityGroupIPPermGather(d *schema.ResourceData, permissions []ec2.IPPerm) []map[string]interface{} { +func resourceAwsSecurityGroupIPPermGather(d *schema.ResourceData, permissions []ec2.IPPermission) []map[string]interface{} { ruleMap := make(map[string]map[string]interface{}) for _, perm := range permissions { - k := fmt.Sprintf("%s-%d-%d", perm.Protocol, perm.FromPort, perm.ToPort) + var fromPort, toPort int + if v := perm.FromPort; v != nil { + fromPort = *v + } + if v := perm.ToPort; v != nil { + toPort = *v + } + + k := fmt.Sprintf("%s-%d-%d", *perm.IPProtocol, fromPort, toPort) m, ok := ruleMap[k] if !ok { m = make(map[string]interface{}) ruleMap[k] = m } - m["from_port"] = perm.FromPort - m["to_port"] = perm.ToPort - m["protocol"] = perm.Protocol + m["from_port"] = fromPort + m["to_port"] = toPort + m["protocol"] = *perm.IPProtocol - if len(perm.SourceIPs) > 0 { + if len(perm.IPRanges) > 0 { raw, ok := m["cidr_blocks"] if !ok { - raw = make([]string, 0, len(perm.SourceIPs)) + raw = make([]string, 0, len(perm.IPRanges)) } list := raw.([]string) - list = append(list, perm.SourceIPs...) + for _, ip := range perm.IPRanges { + list = append(list, *ip.CIDRIP) + } + m["cidr_blocks"] = list } var groups []string - if len(perm.SourceGroups) > 0 { - groups = flattenSecurityGroups(perm.SourceGroups) + if len(perm.UserIDGroupPairs) > 0 { + groups = flattenSecurityGroupsSDK(perm.UserIDGroupPairs) } for i, id := range groups { if id == d.Id() { @@ -364,7 +378,6 @@ func resourceAwsSecurityGroupIPPermGather(d *schema.ResourceData, permissions [] for _, m := range ruleMap { rules = append(rules, m) } - return rules } @@ -383,6 +396,7 @@ func resourceAwsSecurityGroupUpdateRules( os := o.(*schema.Set) ns := n.(*schema.Set) + // TODO: re-munge this when test is updated remove := expandIPPerms(d.Id(), os.Difference(ns).List()) add := expandIPPerms(d.Id(), ns.Difference(os).List()) @@ -396,34 +410,53 @@ func resourceAwsSecurityGroupUpdateRules( // not have service issues. if len(remove) > 0 || len(add) > 0 { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn + var err error if len(remove) > 0 { - // Revoke the old rules - revoke := ec2conn.RevokeSecurityGroup + log.Printf("[DEBUG] Revoking security group %#v %s rule: %#v", + group, ruleset, remove) + if ruleset == "egress" { - revoke = ec2conn.RevokeSecurityGroupEgress + req := &ec2.RevokeSecurityGroupEgressRequest{ + GroupID: group.GroupID, + IPPermissions: remove, + } + err = ec2conn.RevokeSecurityGroupEgress(req) + } else { + req := &ec2.RevokeSecurityGroupIngressRequest{ + GroupID: group.GroupID, + IPPermissions: remove, + } + err = ec2conn.RevokeSecurityGroupIngress(req) } - log.Printf("[DEBUG] Revoking security group %s %s rule: %#v", - group, ruleset, remove) - if _, err := revoke(group, remove); err != nil { + if err != nil { return fmt.Errorf( - "Error revoking security group %s rules: %s", + "Error authorizing security group %s rules: %s", ruleset, err) } } if len(add) > 0 { + log.Printf("[DEBUG] Authorizing security group %#v %s rule: %#v", + group, ruleset, add) // Authorize the new rules - authorize := ec2conn.AuthorizeSecurityGroup if ruleset == "egress" { - authorize = ec2conn.AuthorizeSecurityGroupEgress + req := &ec2.AuthorizeSecurityGroupEgressRequest{ + GroupID: group.GroupID, + IPPermissions: add, + } + err = ec2conn.AuthorizeSecurityGroupEgress(req) + } else { + req := &ec2.AuthorizeSecurityGroupIngressRequest{ + GroupID: group.GroupID, + IPPermissions: add, + } + err = ec2conn.AuthorizeSecurityGroupIngress(req) } - log.Printf("[DEBUG] Authorizing security group %s %s rule: %#v", - group, ruleset, add) - if _, err := authorize(group, add); err != nil { + if err != nil { return fmt.Errorf( "Error authorizing security group %s rules: %s", ruleset, err) @@ -431,7 +464,6 @@ func resourceAwsSecurityGroupUpdateRules( } } } - return nil } @@ -439,10 +471,12 @@ func resourceAwsSecurityGroupUpdateRules( // a security group. func SGStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - sgs := []ec2.SecurityGroup{ec2.SecurityGroup{Id: id}} - resp, err := conn.SecurityGroups(sgs, nil) + req := &ec2.DescribeSecurityGroupsRequest{ + GroupIDs: []string{id}, + } + resp, err := conn.DescribeSecurityGroups(req) if err != nil { - if ec2err, ok := err.(*ec2.Error); ok { + if ec2err, ok := err.(aws.APIError); ok { if ec2err.Code == "InvalidSecurityGroupID.NotFound" || ec2err.Code == "InvalidGroup.NotFound" { resp = nil @@ -460,7 +494,7 @@ func SGStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc { return nil, "", nil } - group := &resp.Groups[0] + group := resp.SecurityGroups[0] return group, "exists", nil } } diff --git a/builtin/providers/aws/resource_aws_security_group_test.go b/builtin/providers/aws/resource_aws_security_group_test.go index d31f9754b..c292c80d6 100644 --- a/builtin/providers/aws/resource_aws_security_group_test.go +++ b/builtin/providers/aws/resource_aws_security_group_test.go @@ -2,16 +2,18 @@ package aws import ( "fmt" + "log" "reflect" "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 TestAccAWSSecurityGroup_normal(t *testing.T) { - var group ec2.SecurityGroupInfo + var group ec2.SecurityGroup resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -44,7 +46,7 @@ func TestAccAWSSecurityGroup_normal(t *testing.T) { } func TestAccAWSSecurityGroup_self(t *testing.T) { - var group ec2.SecurityGroupInfo + var group ec2.SecurityGroup checkSelf := func(s *terraform.State) (err error) { defer func() { @@ -53,7 +55,7 @@ func TestAccAWSSecurityGroup_self(t *testing.T) { } }() - if group.IPPerms[0].SourceGroups[0].Id != group.Id { + if *group.IPPermissions[0].UserIDGroupPairs[0].GroupID != *group.GroupID { return fmt.Errorf("bad: %#v", group) } @@ -89,10 +91,10 @@ func TestAccAWSSecurityGroup_self(t *testing.T) { } func TestAccAWSSecurityGroup_vpc(t *testing.T) { - var group ec2.SecurityGroupInfo + var group ec2.SecurityGroup testCheck := func(*terraform.State) error { - if group.VpcId == "" { + if *group.VPCID == "" { return fmt.Errorf("should have vpc ID") } @@ -141,7 +143,7 @@ func TestAccAWSSecurityGroup_vpc(t *testing.T) { } func TestAccAWSSecurityGroup_MultiIngress(t *testing.T) { - var group ec2.SecurityGroupInfo + var group ec2.SecurityGroup resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -159,7 +161,7 @@ func TestAccAWSSecurityGroup_MultiIngress(t *testing.T) { } func TestAccAWSSecurityGroup_Change(t *testing.T) { - var group ec2.SecurityGroupInfo + var group ec2.SecurityGroup resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -184,30 +186,27 @@ func TestAccAWSSecurityGroup_Change(t *testing.T) { } func testAccCheckAWSSecurityGroupDestroy(s *terraform.State) error { - conn := testAccProvider.Meta().(*AWSClient).ec2conn + conn := testAccProvider.Meta().(*AWSClient).awsEC2conn for _, rs := range s.RootModule().Resources { if rs.Type != "aws_security_group" { continue } - sgs := []ec2.SecurityGroup{ - ec2.SecurityGroup{ - Id: rs.Primary.ID, - }, - } - // Retrieve our group - resp, err := conn.SecurityGroups(sgs, nil) + req := &ec2.DescribeSecurityGroupsRequest{ + GroupIDs: []string{rs.Primary.ID}, + } + resp, err := conn.DescribeSecurityGroups(req) if err == nil { - if len(resp.Groups) > 0 && resp.Groups[0].Id == rs.Primary.ID { + if len(resp.SecurityGroups) > 0 && *resp.SecurityGroups[0].GroupID == rs.Primary.ID { return fmt.Errorf("Security Group (%s) still exists.", rs.Primary.ID) } return nil } - ec2err, ok := err.(*ec2.Error) + ec2err, ok := err.(aws.APIError) if !ok { return err } @@ -220,7 +219,7 @@ func testAccCheckAWSSecurityGroupDestroy(s *terraform.State) error { return nil } -func testAccCheckAWSSecurityGroupExists(n string, group *ec2.SecurityGroupInfo) resource.TestCheckFunc { +func testAccCheckAWSSecurityGroupExists(n string, group *ec2.SecurityGroup) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -231,20 +230,19 @@ func testAccCheckAWSSecurityGroupExists(n string, group *ec2.SecurityGroupInfo) return fmt.Errorf("No Security Group is set") } - conn := testAccProvider.Meta().(*AWSClient).ec2conn - sgs := []ec2.SecurityGroup{ - ec2.SecurityGroup{ - Id: rs.Primary.ID, - }, + conn := testAccProvider.Meta().(*AWSClient).awsEC2conn + req := &ec2.DescribeSecurityGroupsRequest{ + GroupIDs: []string{rs.Primary.ID}, } - resp, err := conn.SecurityGroups(sgs, nil) + resp, err := conn.DescribeSecurityGroups(req) if err != nil { return err } - if len(resp.Groups) > 0 && resp.Groups[0].Id == rs.Primary.ID { + if len(resp.SecurityGroups) > 0 && *resp.SecurityGroups[0].GroupID == rs.Primary.ID { - *group = resp.Groups[0] + log.Printf("\n==\n===\nfound group\n===\n==\n") + *group = resp.SecurityGroups[0] return nil } @@ -253,32 +251,32 @@ func testAccCheckAWSSecurityGroupExists(n string, group *ec2.SecurityGroupInfo) } } -func testAccCheckAWSSecurityGroupAttributes(group *ec2.SecurityGroupInfo) resource.TestCheckFunc { +func testAccCheckAWSSecurityGroupAttributes(group *ec2.SecurityGroup) resource.TestCheckFunc { return func(s *terraform.State) error { - p := ec2.IPPerm{ - FromPort: 80, - ToPort: 8000, - Protocol: "tcp", - SourceIPs: []string{"10.0.0.0/8"}, + p := ec2.IPPermission{ + FromPort: aws.Integer(80), + ToPort: aws.Integer(8000), + IPProtocol: aws.String("tcp"), + IPRanges: []ec2.IPRange{ec2.IPRange{aws.String("10.0.0.0/8")}}, } - if group.Name != "terraform_acceptance_test_example" { - return fmt.Errorf("Bad name: %s", group.Name) + if *group.GroupName != "terraform_acceptance_test_example" { + return fmt.Errorf("Bad name: %s", *group.GroupName) } - if group.Description != "Used in the terraform acceptance tests" { - return fmt.Errorf("Bad description: %s", group.Description) + if *group.Description != "Used in the terraform acceptance tests" { + return fmt.Errorf("Bad description: %s", *group.Description) } - if len(group.IPPerms) == 0 { + if len(group.IPPermissions) == 0 { return fmt.Errorf("No IPPerms") } // Compare our ingress - if !reflect.DeepEqual(group.IPPerms[0], p) { + if !reflect.DeepEqual(group.IPPermissions[0], p) { return fmt.Errorf( "Got:\n\n%#v\n\nExpected:\n\n%#v\n", - group.IPPerms[0], + group.IPPermissions[0], p) } @@ -287,7 +285,7 @@ func testAccCheckAWSSecurityGroupAttributes(group *ec2.SecurityGroupInfo) resour } func TestAccAWSSecurityGroup_tags(t *testing.T) { - var group ec2.SecurityGroupInfo + var group ec2.SecurityGroup resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -298,7 +296,7 @@ func TestAccAWSSecurityGroup_tags(t *testing.T) { Config: testAccAWSSecurityGroupConfigTags, Check: resource.ComposeTestCheckFunc( testAccCheckAWSSecurityGroupExists("aws_security_group.foo", &group), - testAccCheckTags(&group.Tags, "foo", "bar"), + testAccCheckTagsSDK(&group.Tags, "foo", "bar"), ), }, @@ -306,56 +304,56 @@ func TestAccAWSSecurityGroup_tags(t *testing.T) { Config: testAccAWSSecurityGroupConfigTagsUpdate, Check: resource.ComposeTestCheckFunc( testAccCheckAWSSecurityGroupExists("aws_security_group.foo", &group), - testAccCheckTags(&group.Tags, "foo", ""), - testAccCheckTags(&group.Tags, "bar", "baz"), + testAccCheckTagsSDK(&group.Tags, "foo", ""), + testAccCheckTagsSDK(&group.Tags, "bar", "baz"), ), }, }, }) } -func testAccCheckAWSSecurityGroupAttributesChanged(group *ec2.SecurityGroupInfo) resource.TestCheckFunc { +func testAccCheckAWSSecurityGroupAttributesChanged(group *ec2.SecurityGroup) resource.TestCheckFunc { return func(s *terraform.State) error { - p := []ec2.IPPerm{ - ec2.IPPerm{ - FromPort: 80, - ToPort: 9000, - Protocol: "tcp", - SourceIPs: []string{"10.0.0.0/8"}, + p := []ec2.IPPermission{ + ec2.IPPermission{ + FromPort: aws.Integer(80), + ToPort: aws.Integer(9000), + IPProtocol: aws.String("tcp"), + IPRanges: []ec2.IPRange{ec2.IPRange{aws.String("10.0.0.0/8")}}, }, - ec2.IPPerm{ - FromPort: 80, - ToPort: 8000, - Protocol: "tcp", - SourceIPs: []string{"0.0.0.0/0", "10.0.0.0/8"}, + ec2.IPPermission{ + FromPort: aws.Integer(80), + ToPort: aws.Integer(8000), + IPProtocol: aws.String("tcp"), + IPRanges: []ec2.IPRange{ec2.IPRange{aws.String("0.0.0.0/0")}, ec2.IPRange{aws.String("10.0.0.0/8")}}, }, } - if group.Name != "terraform_acceptance_test_example" { - return fmt.Errorf("Bad name: %s", group.Name) + if *group.GroupName != "terraform_acceptance_test_example" { + return fmt.Errorf("Bad name: %s", *group.GroupName) } - if group.Description != "Used in the terraform acceptance tests" { - return fmt.Errorf("Bad description: %s", group.Description) + if *group.Description != "Used in the terraform acceptance tests" { + return fmt.Errorf("Bad description: %s", *group.Description) } // Compare our ingress - if len(group.IPPerms) != 2 { + if len(group.IPPermissions) != 2 { return fmt.Errorf( "Got:\n\n%#v\n\nExpected:\n\n%#v\n", - group.IPPerms, + group.IPPermissions, p) } - if group.IPPerms[0].ToPort == 8000 { - group.IPPerms[1], group.IPPerms[0] = - group.IPPerms[0], group.IPPerms[1] + if *group.IPPermissions[0].ToPort == 8000 { + group.IPPermissions[1], group.IPPermissions[0] = + group.IPPermissions[0], group.IPPermissions[1] } - if !reflect.DeepEqual(group.IPPerms, p) { + if !reflect.DeepEqual(group.IPPermissions, p) { return fmt.Errorf( "Got:\n\n%#v\n\nExpected:\n\n%#v\n", - group.IPPerms, + group.IPPermissions, p) } diff --git a/builtin/providers/aws/resource_aws_subnet.go b/builtin/providers/aws/resource_aws_subnet.go index fd7ac2e06..e09fb8bc4 100644 --- a/builtin/providers/aws/resource_aws_subnet.go +++ b/builtin/providers/aws/resource_aws_subnet.go @@ -68,10 +68,10 @@ 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) + 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", diff --git a/builtin/providers/aws/resource_aws_subnet_test.go b/builtin/providers/aws/resource_aws_subnet_test.go index d06b5b31e..77dfeccf0 100644 --- a/builtin/providers/aws/resource_aws_subnet_test.go +++ b/builtin/providers/aws/resource_aws_subnet_test.go @@ -15,11 +15,11 @@ func TestAccAWSSubnet(t *testing.T) { testCheck := func(*terraform.State) error { if *v.CIDRBlock != "10.1.1.0/24" { - return fmt.Errorf("bad cidr: %s", v.CIDRBlock) + return fmt.Errorf("bad cidr: %s", *v.CIDRBlock) } if *v.MapPublicIPOnLaunch != true { - return fmt.Errorf("bad MapPublicIpOnLaunch: %t", v.MapPublicIPOnLaunch) + return fmt.Errorf("bad MapPublicIpOnLaunch: %t", *v.MapPublicIPOnLaunch) } return nil diff --git a/builtin/providers/aws/resource_aws_vpc_peering_connection.go b/builtin/providers/aws/resource_aws_vpc_peering_connection.go index a8316c114..06f50f01c 100644 --- a/builtin/providers/aws/resource_aws_vpc_peering_connection.go +++ b/builtin/providers/aws/resource_aws_vpc_peering_connection.go @@ -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 resourceAwsVpcPeeringConnection() *schema.Resource { @@ -19,9 +20,10 @@ func resourceAwsVpcPeeringConnection() *schema.Resource { Schema: map[string]*schema.Schema{ "peer_owner_id": &schema.Schema{ - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: schema.EnvDefaultFunc("AWS_ACCOUNT_ID", nil), }, "peer_vpc_id": &schema.Schema{ Type: schema.TypeString, @@ -39,23 +41,23 @@ func resourceAwsVpcPeeringConnection() *schema.Resource { } func resourceAwsVpcPeeringCreate(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn // Create the vpc peering connection - createOpts := &ec2.CreateVpcPeeringConnection{ - PeerOwnerId: d.Get("peer_owner_id").(string), - PeerVpcId: d.Get("peer_vpc_id").(string), - VpcId: d.Get("vpc_id").(string), + createOpts := &ec2.CreateVPCPeeringConnectionRequest{ + PeerOwnerID: aws.String(d.Get("peer_owner_id").(string)), + PeerVPCID: aws.String(d.Get("peer_vpc_id").(string)), + VPCID: aws.String(d.Get("vpc_id").(string)), } log.Printf("[DEBUG] VpcPeeringCreate create config: %#v", createOpts) - resp, err := ec2conn.CreateVpcPeeringConnection(createOpts) + resp, err := ec2conn.CreateVPCPeeringConnection(createOpts) if err != nil { return fmt.Errorf("Error creating vpc peering connection: %s", err) } // Get the ID and store it - rt := &resp.VpcPeeringConnection - d.SetId(rt.VpcPeeringConnectionId) + rt := resp.VPCPeeringConnection + d.SetId(*rt.VPCPeeringConnectionID) log.Printf("[INFO] Vpc Peering Connection ID: %s", d.Id()) // Wait for the vpc peering connection to become available @@ -78,7 +80,7 @@ func resourceAwsVpcPeeringCreate(d *schema.ResourceData, meta interface{}) error } func resourceAwsVpcPeeringRead(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn pcRaw, _, err := resourceAwsVpcPeeringConnectionStateRefreshFunc(ec2conn, d.Id())() if err != nil { return err @@ -88,20 +90,20 @@ func resourceAwsVpcPeeringRead(d *schema.ResourceData, meta interface{}) error { return nil } - pc := pcRaw.(*ec2.VpcPeeringConnection) + pc := pcRaw.(*ec2.VPCPeeringConnection) - d.Set("peer_owner_id", pc.AccepterVpcInfo.OwnerId) - d.Set("peer_vpc_id", pc.AccepterVpcInfo.VpcId) - d.Set("vpc_id", pc.RequesterVpcInfo.VpcId) - d.Set("tags", tagsToMap(pc.Tags)) + d.Set("peer_owner_id", pc.AccepterVPCInfo.OwnerID) + d.Set("peer_vpc_id", pc.AccepterVPCInfo.VPCID) + d.Set("vpc_id", pc.RequesterVPCInfo.VPCID) + d.Set("tags", tagsToMapSDK(pc.Tags)) return nil } func resourceAwsVpcPeeringUpdate(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn - if err := setTags(ec2conn, d); err != nil { + if err := setTagsSDK(ec2conn, d); err != nil { return err } else { d.SetPartial("tags") @@ -111,9 +113,12 @@ func resourceAwsVpcPeeringUpdate(d *schema.ResourceData, meta interface{}) error } func resourceAwsVpcPeeringDelete(d *schema.ResourceData, meta interface{}) error { - ec2conn := meta.(*AWSClient).ec2conn + ec2conn := meta.(*AWSClient).awsEC2conn - _, err := ec2conn.DeleteVpcPeeringConnection(d.Id()) + _, err := ec2conn.DeleteVPCPeeringConnection( + &ec2.DeleteVPCPeeringConnectionRequest{ + VPCPeeringConnectionID: aws.String(d.Id()), + }) return err } @@ -122,9 +127,11 @@ func resourceAwsVpcPeeringDelete(d *schema.ResourceData, meta interface{}) error func resourceAwsVpcPeeringConnectionStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - resp, err := conn.DescribeVpcPeeringConnection([]string{id}, ec2.NewFilter()) + resp, err := conn.DescribeVPCPeeringConnections(&ec2.DescribeVPCPeeringConnectionsRequest{ + VPCPeeringConnectionIDs: []string{id}, + }) if err != nil { - if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidVpcPeeringConnectionID.NotFound" { + if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidVpcPeeringConnectionID.NotFound" { resp = nil } else { log.Printf("Error on VpcPeeringConnectionStateRefresh: %s", err) @@ -138,7 +145,7 @@ func resourceAwsVpcPeeringConnectionStateRefreshFunc(conn *ec2.EC2, id string) r return nil, "", nil } - pc := &resp.VpcPeeringConnections[0] + pc := &resp.VPCPeeringConnections[0] return pc, "ready", nil } diff --git a/builtin/providers/aws/resource_aws_vpc_peering_connection_test.go b/builtin/providers/aws/resource_aws_vpc_peering_connection_test.go index 2b4b71e33..307dcb7d9 100644 --- a/builtin/providers/aws/resource_aws_vpc_peering_connection_test.go +++ b/builtin/providers/aws/resource_aws_vpc_peering_connection_test.go @@ -4,9 +4,9 @@ import ( "fmt" "testing" + "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 TestAccAWSVPCPeeringConnection_normal(t *testing.T) { @@ -28,17 +28,20 @@ func TestAccAWSVPCPeeringConnection_normal(t *testing.T) { } func testAccCheckAWSVpcPeeringConnectionDestroy(s *terraform.State) error { - conn := testAccProvider.Meta().(*AWSClient).ec2conn + conn := testAccProvider.Meta().(*AWSClient).awsEC2conn for _, rs := range s.RootModule().Resources { if rs.Type != "aws_vpc_peering_connection" { continue } - describe, err := conn.DescribeVpcPeeringConnection([]string{rs.Primary.ID}, ec2.NewFilter()) + describe, err := conn.DescribeVPCPeeringConnections( + &ec2.DescribeVPCPeeringConnectionsRequest{ + VPCPeeringConnectionIDs: []string{rs.Primary.ID}, + }) if err == nil { - if len(describe.VpcPeeringConnections) != 0 { + if len(describe.VPCPeeringConnections) != 0 { return fmt.Errorf("vpc peering connection still exists") } } @@ -68,11 +71,10 @@ resource "aws_vpc" "foo" { } resource "aws_vpc" "bar" { - cidr_block = "10.0.1.0/16" + cidr_block = "10.1.0.0/16" } resource "aws_vpc_peering_connection" "foo" { - peer_owner_id = "12345" vpc_id = "${aws_vpc.foo.id}" peer_vpc_id = "${aws_vpc.bar.id}" } diff --git a/builtin/providers/aws/structure.go b/builtin/providers/aws/structure.go index 7d4793d3d..910f748e6 100644 --- a/builtin/providers/aws/structure.go +++ b/builtin/providers/aws/structure.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/hashicorp/aws-sdk-go/aws" + awsEC2 "github.com/hashicorp/aws-sdk-go/gen/ec2" "github.com/hashicorp/aws-sdk-go/gen/elb" "github.com/hashicorp/aws-sdk-go/gen/rds" "github.com/hashicorp/terraform/helper/schema" @@ -39,15 +40,15 @@ func expandListeners(configured []interface{}) ([]elb.Listener, error) { // Takes the result of flatmap.Expand for an array of ingress/egress // security group rules and returns EC2 API compatible objects -func expandIPPerms(id string, configured []interface{}) []ec2.IPPerm { - perms := make([]ec2.IPPerm, len(configured)) +func expandIPPerms(id string, configured []interface{}) []awsEC2.IPPermission { + perms := make([]awsEC2.IPPermission, len(configured)) for i, mRaw := range configured { - var perm ec2.IPPerm + var perm awsEC2.IPPermission m := mRaw.(map[string]interface{}) - perm.FromPort = m["from_port"].(int) - perm.ToPort = m["to_port"].(int) - perm.Protocol = m["protocol"].(string) + perm.FromPort = aws.Integer(m["from_port"].(int)) + perm.ToPort = aws.Integer(m["to_port"].(int)) + perm.IPProtocol = aws.String(m["protocol"].(string)) var groups []string if raw, ok := m["security_groups"]; ok { @@ -61,25 +62,25 @@ func expandIPPerms(id string, configured []interface{}) []ec2.IPPerm { } if len(groups) > 0 { - perm.SourceGroups = make([]ec2.UserSecurityGroup, len(groups)) + perm.UserIDGroupPairs = make([]awsEC2.UserIDGroupPair, len(groups)) for i, name := range groups { ownerId, id := "", name if items := strings.Split(id, "/"); len(items) > 1 { ownerId, id = items[0], items[1] } - perm.SourceGroups[i] = ec2.UserSecurityGroup{ - Id: id, - OwnerId: ownerId, + perm.UserIDGroupPairs[i] = awsEC2.UserIDGroupPair{ + GroupID: aws.String(id), + UserID: aws.String(ownerId), } } } if raw, ok := m["cidr_blocks"]; ok { list := raw.([]interface{}) - perm.SourceIPs = make([]string, len(list)) + perm.IPRanges = make([]awsEC2.IPRange, len(list)) for i, v := range list { - perm.SourceIPs[i] = v.(string) + perm.IPRanges[i] = awsEC2.IPRange{aws.String(v.(string))} } } @@ -111,31 +112,6 @@ func expandParameters(configured []interface{}) ([]rds.Parameter, error) { return parameters, nil } -// Flattens an array of ipPerms into a list of primitives that -// flatmap.Flatten() can handle -func flattenIPPerms(list []ec2.IPPerm) []map[string]interface{} { - result := make([]map[string]interface{}, 0, len(list)) - - for _, perm := range list { - n := make(map[string]interface{}) - n["from_port"] = perm.FromPort - n["protocol"] = perm.Protocol - n["to_port"] = perm.ToPort - - if len(perm.SourceIPs) > 0 { - n["cidr_blocks"] = perm.SourceIPs - } - - if v := flattenSecurityGroups(perm.SourceGroups); len(v) > 0 { - n["security_groups"] = v - } - - result = append(result, n) - } - - return result -} - // Flattens a health check into something that flatmap.Flatten() // can handle func flattenHealthCheck(check *elb.HealthCheck) []map[string]interface{} { @@ -162,6 +138,15 @@ func flattenSecurityGroups(list []ec2.UserSecurityGroup) []string { return result } +// Flattens an array of UserSecurityGroups into a []string +func flattenSecurityGroupsSDK(list []awsEC2.UserIDGroupPair) []string { + result := make([]string, 0, len(list)) + for _, g := range list { + result = append(result, *g.GroupID) + } + return result +} + // Flattens an array of Instances into a []string func flattenInstances(list []elb.Instance) []string { result := make([]string, 0, len(list)) diff --git a/builtin/providers/aws/structure_test.go b/builtin/providers/aws/structure_test.go index f3a8bcc72..d5e470341 100644 --- a/builtin/providers/aws/structure_test.go +++ b/builtin/providers/aws/structure_test.go @@ -5,12 +5,12 @@ import ( "testing" "github.com/hashicorp/aws-sdk-go/aws" + awsEC2 "github.com/hashicorp/aws-sdk-go/gen/ec2" "github.com/hashicorp/aws-sdk-go/gen/elb" "github.com/hashicorp/aws-sdk-go/gen/rds" "github.com/hashicorp/terraform/flatmap" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/schema" - "github.com/mitchellh/goamz/ec2" ) // Returns test configuration @@ -61,120 +61,58 @@ func TestExpandIPPerms(t *testing.T) { } perms := expandIPPerms("foo", expanded) - expected := []ec2.IPPerm{ - ec2.IPPerm{ - Protocol: "icmp", - FromPort: 1, - ToPort: -1, - SourceIPs: []string{"0.0.0.0/0"}, - SourceGroups: []ec2.UserSecurityGroup{ - ec2.UserSecurityGroup{ - OwnerId: "foo", - Id: "sg-22222", + expected := []awsEC2.IPPermission{ + awsEC2.IPPermission{ + IPProtocol: aws.String("icmp"), + FromPort: aws.Integer(1), + ToPort: aws.Integer(-1), + IPRanges: []awsEC2.IPRange{awsEC2.IPRange{aws.String("0.0.0.0/0")}}, + UserIDGroupPairs: []awsEC2.UserIDGroupPair{ + awsEC2.UserIDGroupPair{ + UserID: aws.String("foo"), + GroupID: aws.String("sg-22222"), }, - ec2.UserSecurityGroup{ - Id: "sg-11111", + awsEC2.UserIDGroupPair{ + GroupID: aws.String("sg-22222"), }, }, }, - ec2.IPPerm{ - Protocol: "icmp", - FromPort: 1, - ToPort: -1, - SourceGroups: []ec2.UserSecurityGroup{ - ec2.UserSecurityGroup{ - Id: "foo", + awsEC2.IPPermission{ + IPProtocol: aws.String("icmp"), + FromPort: aws.Integer(1), + ToPort: aws.Integer(-1), + UserIDGroupPairs: []awsEC2.UserIDGroupPair{ + awsEC2.UserIDGroupPair{ + UserID: aws.String("foo"), }, }, }, } - if !reflect.DeepEqual(perms, expected) { + exp := expected[0] + perm := perms[0] + + if *exp.FromPort != *perm.FromPort { t.Fatalf( "Got:\n\n%#v\n\nExpected:\n\n%#v\n", - perms[0], - expected) + *perm.FromPort, + *exp.FromPort) } -} - -func TestFlattenIPPerms(t *testing.T) { - cases := []struct { - Input []ec2.IPPerm - Output []map[string]interface{} - }{ - { - Input: []ec2.IPPerm{ - ec2.IPPerm{ - Protocol: "icmp", - FromPort: 1, - ToPort: -1, - SourceIPs: []string{"0.0.0.0/0"}, - SourceGroups: []ec2.UserSecurityGroup{ - ec2.UserSecurityGroup{ - Id: "sg-11111", - }, - }, - }, - }, - - Output: []map[string]interface{}{ - map[string]interface{}{ - "protocol": "icmp", - "from_port": 1, - "to_port": -1, - "cidr_blocks": []string{"0.0.0.0/0"}, - "security_groups": []string{"sg-11111"}, - }, - }, - }, - - { - Input: []ec2.IPPerm{ - ec2.IPPerm{ - Protocol: "icmp", - FromPort: 1, - ToPort: -1, - SourceIPs: []string{"0.0.0.0/0"}, - SourceGroups: nil, - }, - }, - - Output: []map[string]interface{}{ - map[string]interface{}{ - "protocol": "icmp", - "from_port": 1, - "to_port": -1, - "cidr_blocks": []string{"0.0.0.0/0"}, - }, - }, - }, - { - Input: []ec2.IPPerm{ - ec2.IPPerm{ - Protocol: "icmp", - FromPort: 1, - ToPort: -1, - SourceIPs: nil, - }, - }, - - Output: []map[string]interface{}{ - map[string]interface{}{ - "protocol": "icmp", - "from_port": 1, - "to_port": -1, - }, - }, - }, + if *exp.IPRanges[0].CIDRIP != *perm.IPRanges[0].CIDRIP { + t.Fatalf( + "Got:\n\n%#v\n\nExpected:\n\n%#v\n", + *perm.IPRanges[0].CIDRIP, + *exp.IPRanges[0].CIDRIP) } - for _, tc := range cases { - output := flattenIPPerms(tc.Input) - if !reflect.DeepEqual(output, tc.Output) { - t.Fatalf("Input:\n\n%#v\n\nOutput:\n\n%#v", tc.Input, output) - } + if *exp.UserIDGroupPairs[0].UserID != *perm.UserIDGroupPairs[0].UserID { + t.Fatalf( + "Got:\n\n%#v\n\nExpected:\n\n%#v\n", + *perm.UserIDGroupPairs[0].UserID, + *exp.UserIDGroupPairs[0].UserID) } + } func TestExpandListeners(t *testing.T) { diff --git a/builtin/providers/cloudstack/provider.go b/builtin/providers/cloudstack/provider.go index a9913f6e8..8bea67ef5 100644 --- a/builtin/providers/cloudstack/provider.go +++ b/builtin/providers/cloudstack/provider.go @@ -30,22 +30,25 @@ func Provider() terraform.ResourceProvider { "timeout": &schema.Schema{ Type: schema.TypeInt, Required: true, - DefaultFunc: schema.EnvDefaultFunc("CLOUDSTACK_TIMEOUT", 180), + DefaultFunc: schema.EnvDefaultFunc("CLOUDSTACK_TIMEOUT", 300), }, }, ResourcesMap: map[string]*schema.Resource{ - "cloudstack_disk": resourceCloudStackDisk(), - "cloudstack_egress_firewall": resourceCloudStackEgressFirewall(), - "cloudstack_firewall": resourceCloudStackFirewall(), - "cloudstack_instance": resourceCloudStackInstance(), - "cloudstack_ipaddress": resourceCloudStackIPAddress(), - "cloudstack_network": resourceCloudStackNetwork(), - "cloudstack_network_acl": resourceCloudStackNetworkACL(), - "cloudstack_network_acl_rule": resourceCloudStackNetworkACLRule(), - "cloudstack_nic": resourceCloudStackNIC(), - "cloudstack_port_forward": resourceCloudStackPortForward(), - "cloudstack_vpc": resourceCloudStackVPC(), + "cloudstack_disk": resourceCloudStackDisk(), + "cloudstack_egress_firewall": resourceCloudStackEgressFirewall(), + "cloudstack_firewall": resourceCloudStackFirewall(), + "cloudstack_instance": resourceCloudStackInstance(), + "cloudstack_ipaddress": resourceCloudStackIPAddress(), + "cloudstack_network": resourceCloudStackNetwork(), + "cloudstack_network_acl": resourceCloudStackNetworkACL(), + "cloudstack_network_acl_rule": resourceCloudStackNetworkACLRule(), + "cloudstack_nic": resourceCloudStackNIC(), + "cloudstack_port_forward": resourceCloudStackPortForward(), + "cloudstack_vpc": resourceCloudStackVPC(), + "cloudstack_vpn_connection": resourceCloudStackVPNConnection(), + "cloudstack_vpn_customer_gateway": resourceCloudStackVPNCustomerGateway(), + "cloudstack_vpn_gateway": resourceCloudStackVPNGateway(), }, ConfigureFunc: providerConfigure, diff --git a/builtin/providers/cloudstack/provider_test.go b/builtin/providers/cloudstack/provider_test.go index a13839177..537a2664f 100644 --- a/builtin/providers/cloudstack/provider_test.go +++ b/builtin/providers/cloudstack/provider_test.go @@ -51,7 +51,8 @@ var CLOUDSTACK_NETWORK_1_OFFERING = "" var CLOUDSTACK_NETWORK_1_IPADDRESS = "" var CLOUDSTACK_NETWORK_2 = "" var CLOUDSTACK_NETWORK_2_IPADDRESS = "" -var CLOUDSTACK_VPC_CIDR = "" +var CLOUDSTACK_VPC_CIDR_1 = "" +var CLOUDSTACK_VPC_CIDR_2 = "" var CLOUDSTACK_VPC_OFFERING = "" var CLOUDSTACK_VPC_NETWORK_CIDR = "" var CLOUDSTACK_VPC_NETWORK_OFFERING = "" diff --git a/builtin/providers/cloudstack/resource_cloudstack_instance.go b/builtin/providers/cloudstack/resource_cloudstack_instance.go index 600001a27..f0e52b588 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_instance.go +++ b/builtin/providers/cloudstack/resource_cloudstack_instance.go @@ -95,18 +95,18 @@ func resourceCloudStackInstanceCreate(d *schema.ResourceData, meta interface{}) return e.Error() } - // Retrieve the template UUID - templateid, e := retrieveUUID(cs, "template", d.Get("template").(string)) - if e != nil { - return e.Error() - } - // Retrieve the zone object zone, _, err := cs.Zone.GetZoneByName(d.Get("zone").(string)) if err != nil { return err } + // Retrieve the template UUID + templateid, e := retrieveTemplateUUID(cs, zone.Id, d.Get("template").(string)) + if e != nil { + return e.Error() + } + // Create a new parameter struct p := cs.VirtualMachine.NewDeployVirtualMachineParams(serviceofferingid, templateid, zone.Id) diff --git a/builtin/providers/cloudstack/resource_cloudstack_ipaddress_test.go b/builtin/providers/cloudstack/resource_cloudstack_ipaddress_test.go index 5b1fc9a31..dfdeba209 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_ipaddress_test.go +++ b/builtin/providers/cloudstack/resource_cloudstack_ipaddress_test.go @@ -132,6 +132,6 @@ resource "cloudstack_vpc" "foobar" { resource "cloudstack_ipaddress" "foo" { vpc = "${cloudstack_vpc.foobar.name}" }`, - CLOUDSTACK_VPC_CIDR, + CLOUDSTACK_VPC_CIDR_1, CLOUDSTACK_VPC_OFFERING, CLOUDSTACK_ZONE) diff --git a/builtin/providers/cloudstack/resource_cloudstack_network_acl_rule_test.go b/builtin/providers/cloudstack/resource_cloudstack_network_acl_rule_test.go index 037b9d10b..dbceb8d8d 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_network_acl_rule_test.go +++ b/builtin/providers/cloudstack/resource_cloudstack_network_acl_rule_test.go @@ -196,7 +196,7 @@ resource "cloudstack_network_acl_rule" "foo" { traffic_type = "ingress" } }`, - CLOUDSTACK_VPC_CIDR, + CLOUDSTACK_VPC_CIDR_1, CLOUDSTACK_VPC_OFFERING, CLOUDSTACK_ZONE) @@ -233,6 +233,6 @@ resource "cloudstack_network_acl_rule" "foo" { traffic_type = "egress" } }`, - CLOUDSTACK_VPC_CIDR, + CLOUDSTACK_VPC_CIDR_1, CLOUDSTACK_VPC_OFFERING, CLOUDSTACK_ZONE) diff --git a/builtin/providers/cloudstack/resource_cloudstack_network_acl_test.go b/builtin/providers/cloudstack/resource_cloudstack_network_acl_test.go index e625d4c2d..9bf0bb0cf 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_network_acl_test.go +++ b/builtin/providers/cloudstack/resource_cloudstack_network_acl_test.go @@ -112,6 +112,6 @@ resource "cloudstack_network_acl" "foo" { description = "terraform-acl-text" vpc = "${cloudstack_vpc.foobar.name}" }`, - CLOUDSTACK_VPC_CIDR, + CLOUDSTACK_VPC_CIDR_1, CLOUDSTACK_VPC_OFFERING, CLOUDSTACK_ZONE) diff --git a/builtin/providers/cloudstack/resource_cloudstack_network_test.go b/builtin/providers/cloudstack/resource_cloudstack_network_test.go index 750761f02..d936f8cb0 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_network_test.go +++ b/builtin/providers/cloudstack/resource_cloudstack_network_test.go @@ -186,7 +186,7 @@ resource "cloudstack_network" "foo" { aclid = "${cloudstack_network_acl.foo.id}" zone = "${cloudstack_vpc.foobar.zone}" }`, - CLOUDSTACK_VPC_CIDR, + CLOUDSTACK_VPC_CIDR_1, CLOUDSTACK_VPC_OFFERING, CLOUDSTACK_ZONE, CLOUDSTACK_VPC_NETWORK_CIDR, diff --git a/builtin/providers/cloudstack/resource_cloudstack_vpc_test.go b/builtin/providers/cloudstack/resource_cloudstack_vpc_test.go index bf4e8f448..07861a091 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_vpc_test.go +++ b/builtin/providers/cloudstack/resource_cloudstack_vpc_test.go @@ -72,8 +72,8 @@ func testAccCheckCloudStackVPCAttributes( return fmt.Errorf("Bad display text: %s", vpc.Displaytext) } - if vpc.Cidr != CLOUDSTACK_VPC_CIDR { - return fmt.Errorf("Bad VPC offering: %s", vpc.Cidr) + if vpc.Cidr != CLOUDSTACK_VPC_CIDR_1 { + return fmt.Errorf("Bad VPC CIDR: %s", vpc.Cidr) } return nil @@ -113,6 +113,6 @@ resource "cloudstack_vpc" "foo" { vpc_offering = "%s" zone = "%s" }`, - CLOUDSTACK_VPC_CIDR, + CLOUDSTACK_VPC_CIDR_1, CLOUDSTACK_VPC_OFFERING, CLOUDSTACK_ZONE) diff --git a/builtin/providers/cloudstack/resource_cloudstack_vpn_connection.go b/builtin/providers/cloudstack/resource_cloudstack_vpn_connection.go new file mode 100644 index 000000000..b036890a5 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_vpn_connection.go @@ -0,0 +1,95 @@ +package cloudstack + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func resourceCloudStackVPNConnection() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackVPNConnectionCreate, + Read: resourceCloudStackVPNConnectionRead, + Delete: resourceCloudStackVPNConnectionDelete, + + Schema: map[string]*schema.Schema{ + "customergatewayid": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "vpngatewayid": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceCloudStackVPNConnectionCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.VPN.NewCreateVpnConnectionParams( + d.Get("customergatewayid").(string), + d.Get("vpngatewayid").(string), + ) + + // Create the new VPN Connection + v, err := cs.VPN.CreateVpnConnection(p) + if err != nil { + return fmt.Errorf("Error creating VPN Connection: %s", err) + } + + d.SetId(v.Id) + + return resourceCloudStackVPNConnectionRead(d, meta) +} + +func resourceCloudStackVPNConnectionRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the VPN Connection details + v, count, err := cs.VPN.GetVpnConnectionByID(d.Id()) + if err != nil { + if count == 0 { + log.Printf("[DEBUG] VPN Connection does no longer exist") + d.SetId("") + return nil + } + + return err + } + + d.Set("customergatewayid", v.S2scustomergatewayid) + d.Set("vpngatewayid", v.S2svpngatewayid) + + return nil +} + +func resourceCloudStackVPNConnectionDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.VPN.NewDeleteVpnConnectionParams(d.Id()) + + // Delete the VPN Connection + _, err := cs.VPN.DeleteVpnConnection(p) + if err != nil { + // This is a very poor way to be told the UUID does no longer exist :( + if strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", d.Id())) { + return nil + } + + return fmt.Errorf("Error deleting VPN Connection: %s", err) + } + + return nil +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_vpn_connection_test.go b/builtin/providers/cloudstack/resource_cloudstack_vpn_connection_test.go new file mode 100644 index 000000000..1b9d9920a --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_vpn_connection_test.go @@ -0,0 +1,142 @@ +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackVPNConnection_basic(t *testing.T) { + var vpnConnection cloudstack.VpnConnection + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackVPNConnectionDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackVPNConnection_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackVPNConnectionExists( + "cloudstack_vpn_connection.foo-bar", &vpnConnection), + testAccCheckCloudStackVPNConnectionExists( + "cloudstack_vpn_connection.bar-foo", &vpnConnection), + ), + }, + }, + }) +} + +func testAccCheckCloudStackVPNConnectionExists( + n string, vpnConnection *cloudstack.VpnConnection) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No VPN Connection ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + v, _, err := cs.VPN.GetVpnConnectionByID(rs.Primary.ID) + + if err != nil { + return err + } + + if v.Id != rs.Primary.ID { + return fmt.Errorf("VPN Connection not found") + } + + *vpnConnection = *v + + return nil + } +} + +func testAccCheckCloudStackVPNConnectionDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_vpn_connection" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No VPN Connection ID is set") + } + + p := cs.VPN.NewDeleteVpnConnectionParams(rs.Primary.ID) + _, err := cs.VPN.DeleteVpnConnection(p) + + if err != nil { + return fmt.Errorf( + "Error deleting VPN Connection (%s): %s", + rs.Primary.ID, err) + } + } + + return nil +} + +var testAccCloudStackVPNConnection_basic = fmt.Sprintf(` +resource "cloudstack_vpc" "foo" { + name = "terraform-vpc-foo" + cidr = "%s" + vpc_offering = "%s" + zone = "%s" +} + +resource "cloudstack_vpc" "bar" { + name = "terraform-vpc-bar" + cidr = "%s" + vpc_offering = "%s" + zone = "%s" +} + +resource "cloudstack_vpn_gateway" "foo" { + vpc = "${cloudstack_vpc.foo.name}" +} + +resource "cloudstack_vpn_gateway" "bar" { + vpc = "${cloudstack_vpc.bar.name}" +} + +resource "cloudstack_vpn_customer_gateway" "foo" { + name = "terraform-foo" + cidr = "${cloudstack_vpc.foo.cidr}" + esp_policy = "aes256-sha1" + gateway = "${cloudstack_vpn_gateway.foo.public_ip}" + ike_policy = "aes256-sha1" + ipsec_psk = "terraform" +} + +resource "cloudstack_vpn_customer_gateway" "bar" { + name = "terraform-bar" + cidr = "${cloudstack_vpc.bar.cidr}" + esp_policy = "aes256-sha1" + gateway = "${cloudstack_vpn_gateway.bar.public_ip}" + ike_policy = "aes256-sha1" + ipsec_psk = "terraform" +} + +resource "cloudstack_vpn_connection" "foo-bar" { + customergatewayid = "${cloudstack_vpn_customer_gateway.foo.id}" + vpngatewayid = "${cloudstack_vpn_gateway.bar.id}" +} + +resource "cloudstack_vpn_connection" "bar-foo" { + customergatewayid = "${cloudstack_vpn_customer_gateway.bar.id}" + vpngatewayid = "${cloudstack_vpn_gateway.foo.id}" +}`, + CLOUDSTACK_VPC_CIDR_1, + CLOUDSTACK_VPC_OFFERING, + CLOUDSTACK_ZONE, + CLOUDSTACK_VPC_CIDR_2, + CLOUDSTACK_VPC_OFFERING, + CLOUDSTACK_ZONE) diff --git a/builtin/providers/cloudstack/resource_cloudstack_vpn_customer_gateway.go b/builtin/providers/cloudstack/resource_cloudstack_vpn_customer_gateway.go new file mode 100644 index 000000000..90a03aeb0 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_vpn_customer_gateway.go @@ -0,0 +1,193 @@ +package cloudstack + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func resourceCloudStackVPNCustomerGateway() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackVPNCustomerGatewayCreate, + Read: resourceCloudStackVPNCustomerGatewayRead, + Update: resourceCloudStackVPNCustomerGatewayUpdate, + Delete: resourceCloudStackVPNCustomerGatewayDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "cidr": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "esp_policy": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "gateway": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "ike_policy": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "ipsec_psk": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "dpd": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + + "esp_lifetime": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + + "ike_lifetime": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + }, + } +} + +func resourceCloudStackVPNCustomerGatewayCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.VPN.NewCreateVpnCustomerGatewayParams( + d.Get("cidr").(string), + d.Get("esp_policy").(string), + d.Get("gateway").(string), + d.Get("ike_policy").(string), + d.Get("ipsec_psk").(string), + ) + + p.SetName(d.Get("name").(string)) + + if dpd, ok := d.GetOk("dpd"); ok { + p.SetDpd(dpd.(bool)) + } + + if esplifetime, ok := d.GetOk("esp_lifetime"); ok { + p.SetEsplifetime(esplifetime.(int)) + } + + if ikelifetime, ok := d.GetOk("ike_lifetime"); ok { + p.SetIkelifetime(ikelifetime.(int)) + } + + // Create the new VPN Customer Gateway + v, err := cs.VPN.CreateVpnCustomerGateway(p) + if err != nil { + return fmt.Errorf("Error creating VPN Customer Gateway %s: %s", d.Get("name").(string), err) + } + + d.SetId(v.Id) + + return resourceCloudStackVPNCustomerGatewayRead(d, meta) +} + +func resourceCloudStackVPNCustomerGatewayRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the VPN Customer Gateway details + v, count, err := cs.VPN.GetVpnCustomerGatewayByID(d.Id()) + if err != nil { + if count == 0 { + log.Printf( + "[DEBUG] VPN Customer Gateway %s does no longer exist", d.Get("name").(string)) + d.SetId("") + return nil + } + + return err + } + + d.Set("name", v.Name) + d.Set("cidr", v.Cidrlist) + d.Set("esp_policy", v.Esppolicy) + d.Set("gateway", v.Gateway) + d.Set("ike_policy", v.Ikepolicy) + d.Set("ipsec_psk", v.Ipsecpsk) + d.Set("dpd", v.Dpd) + d.Set("esp_lifetime", v.Esplifetime) + d.Set("ike_lifetime", v.Ikelifetime) + + return nil +} + +func resourceCloudStackVPNCustomerGatewayUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.VPN.NewUpdateVpnCustomerGatewayParams( + d.Get("cidr").(string), + d.Get("esp_policy").(string), + d.Get("gateway").(string), + d.Id(), + d.Get("ike_policy").(string), + d.Get("ipsec_psk").(string), + ) + + p.SetName(d.Get("name").(string)) + + if dpd, ok := d.GetOk("dpd"); ok { + p.SetDpd(dpd.(bool)) + } + + if esplifetime, ok := d.GetOk("esp_lifetime"); ok { + p.SetEsplifetime(esplifetime.(int)) + } + + if ikelifetime, ok := d.GetOk("ike_lifetime"); ok { + p.SetIkelifetime(ikelifetime.(int)) + } + + // Update the VPN Customer Gateway + _, err := cs.VPN.UpdateVpnCustomerGateway(p) + if err != nil { + return fmt.Errorf("Error updating VPN Customer Gateway %s: %s", d.Get("name").(string), err) + } + + return resourceCloudStackVPNCustomerGatewayRead(d, meta) +} + +func resourceCloudStackVPNCustomerGatewayDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.VPN.NewDeleteVpnCustomerGatewayParams(d.Id()) + + // Delete the VPN Customer Gateway + _, err := cs.VPN.DeleteVpnCustomerGateway(p) + if err != nil { + // This is a very poor way to be told the UUID does no longer exist :( + if strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", d.Id())) { + return nil + } + + return fmt.Errorf("Error deleting VPN Customer Gateway %s: %s", d.Get("name").(string), err) + } + + return nil +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_vpn_customer_gateway_test.go b/builtin/providers/cloudstack/resource_cloudstack_vpn_customer_gateway_test.go new file mode 100644 index 000000000..b468c76fe --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_vpn_customer_gateway_test.go @@ -0,0 +1,223 @@ +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackVPNCustomerGateway_basic(t *testing.T) { + var vpnCustomerGateway cloudstack.VpnCustomerGateway + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackVPNCustomerGatewayDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackVPNCustomerGateway_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackVPNCustomerGatewayExists( + "cloudstack_vpn_customer_gateway.foo", &vpnCustomerGateway), + testAccCheckCloudStackVPNCustomerGatewayAttributes(&vpnCustomerGateway), + resource.TestCheckResourceAttr( + "cloudstack_vpn_customer_gateway.foo", "name", "terraform-foo"), + resource.TestCheckResourceAttr( + "cloudstack_vpn_customer_gateway.bar", "name", "terraform-bar"), + resource.TestCheckResourceAttr( + "cloudstack_vpn_customer_gateway.foo", "ike_policy", "aes256-sha1"), + resource.TestCheckResourceAttr( + "cloudstack_vpn_customer_gateway.bar", "esp_policy", "aes256-sha1"), + ), + }, + }, + }) +} + +func TestAccCloudStackVPNCustomerGateway_update(t *testing.T) { + var vpnCustomerGateway cloudstack.VpnCustomerGateway + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackVPNCustomerGatewayDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackVPNCustomerGateway_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackVPNCustomerGatewayExists( + "cloudstack_vpn_customer_gateway.foo", &vpnCustomerGateway), + testAccCheckCloudStackVPNCustomerGatewayAttributes(&vpnCustomerGateway), + resource.TestCheckResourceAttr( + "cloudstack_vpn_customer_gateway.foo", "name", "terraform-foo"), + resource.TestCheckResourceAttr( + "cloudstack_vpn_customer_gateway.bar", "name", "terraform-bar"), + resource.TestCheckResourceAttr( + "cloudstack_vpn_customer_gateway.foo", "ike_policy", "aes256-sha1"), + resource.TestCheckResourceAttr( + "cloudstack_vpn_customer_gateway.bar", "esp_policy", "aes256-sha1"), + ), + }, + + resource.TestStep{ + Config: testAccCloudStackVPNCustomerGateway_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackVPNCustomerGatewayExists( + "cloudstack_vpn_customer_gateway.foo", &vpnCustomerGateway), + testAccCheckCloudStackVPNCustomerGatewayAttributes(&vpnCustomerGateway), + resource.TestCheckResourceAttr( + "cloudstack_vpn_customer_gateway.foo", "name", "terraform-foo-bar"), + resource.TestCheckResourceAttr( + "cloudstack_vpn_customer_gateway.bar", "name", "terraform-bar-foo"), + resource.TestCheckResourceAttr( + "cloudstack_vpn_customer_gateway.foo", "ike_policy", "3des-md5"), + resource.TestCheckResourceAttr( + "cloudstack_vpn_customer_gateway.bar", "esp_policy", "3des-md5"), + ), + }, + }, + }) +} + +func testAccCheckCloudStackVPNCustomerGatewayExists( + n string, vpnCustomerGateway *cloudstack.VpnCustomerGateway) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No VPN CustomerGateway ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + v, _, err := cs.VPN.GetVpnCustomerGatewayByID(rs.Primary.ID) + + if err != nil { + return err + } + + if v.Id != rs.Primary.ID { + return fmt.Errorf("VPN CustomerGateway not found") + } + + *vpnCustomerGateway = *v + + return nil + } +} + +func testAccCheckCloudStackVPNCustomerGatewayAttributes( + vpnCustomerGateway *cloudstack.VpnCustomerGateway) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if vpnCustomerGateway.Esppolicy != "aes256-sha1" { + return fmt.Errorf("Bad ESP policy: %s", vpnCustomerGateway.Esppolicy) + } + + if vpnCustomerGateway.Ikepolicy != "aes256-sha1" { + return fmt.Errorf("Bad IKE policy: %s", vpnCustomerGateway.Ikepolicy) + } + + if vpnCustomerGateway.Ipsecpsk != "terraform" { + return fmt.Errorf("Bad IPSEC pre-shared key: %s", vpnCustomerGateway.Ipsecpsk) + } + + return nil + } +} + +func testAccCheckCloudStackVPNCustomerGatewayDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_vpn_customer_gateway" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No VPN Customer Gateway ID is set") + } + + p := cs.VPN.NewDeleteVpnCustomerGatewayParams(rs.Primary.ID) + _, err := cs.VPN.DeleteVpnCustomerGateway(p) + + if err != nil { + return fmt.Errorf( + "Error deleting VPN Customer Gateway (%s): %s", + rs.Primary.ID, err) + } + } + + return nil +} + +var testAccCloudStackVPNCustomerGateway_basic = fmt.Sprintf(` +resource "cloudstack_vpc" "foo" { + name = "terraform-vpc-foo" + cidr = "%s" + vpc_offering = "%s" + zone = "%s" +} + +resource "cloudstack_vpc" "bar" { + name = "terraform-vpc-bar" + cidr = "%s" + vpc_offering = "%s" + zone = "%s" +} + +resource "cloudstack_vpn_gateway" "foo" { + vpc = "${cloudstack_vpc.foo.name}" +} + +resource "cloudstack_vpn_gateway" "bar" { + vpc = "${cloudstack_vpc.bar.name}" +} + +resource "cloudstack_vpn_customer_gateway" "foo" { + name = "terraform-foo" + cidr = "${cloudstack_vpc.foo.cidr}" + esp_policy = "aes256-sha1" + gateway = "${cloudstack_vpn_gateway.foo.public_ip}" + ike_policy = "aes256-sha1" + ipsec_psk = "terraform" +} + +resource "cloudstack_vpn_customer_gateway" "bar" { + name = "terraform-bar" + cidr = "${cloudstack_vpc.bar.cidr}" + esp_policy = "aes256-sha1" + gateway = "${cloudstack_vpn_gateway.bar.public_ip}" + ike_policy = "aes256-sha1" + ipsec_psk = "terraform" +}`, + CLOUDSTACK_VPC_CIDR_1, + CLOUDSTACK_VPC_OFFERING, + CLOUDSTACK_ZONE, + CLOUDSTACK_VPC_CIDR_2, + CLOUDSTACK_VPC_OFFERING, + CLOUDSTACK_ZONE) + +var testAccCloudStackVPNCustomerGateway_update = fmt.Sprintf(` +resource "cloudstack_vpn_customer_gateway" "foo" { + name = "terraform-foo-bar" + cidr = "${cloudstack_vpc.foo.cidr}" + esp_policy = "3des-md5" + gateway = "${cloudstack_vpn_gateway.foo.public_ip}" + ike_policy = "3des-md5" + ipsec_psk = "terraform" +} + +resource "cloudstack_vpn_customer_gateway" "bar" { + name = "terraform-bar-foo" + cidr = "${cloudstack_vpc.bar.cidr}" + esp_policy = "3des-md5" + gateway = "${cloudstack_vpn_gateway.bar.public_ip}" + ike_policy = "3des-md5" + ipsec_psk = "terraform" +}`) diff --git a/builtin/providers/cloudstack/resource_cloudstack_vpn_gateway.go b/builtin/providers/cloudstack/resource_cloudstack_vpn_gateway.go new file mode 100644 index 000000000..063c31777 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_vpn_gateway.go @@ -0,0 +1,97 @@ +package cloudstack + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func resourceCloudStackVPNGateway() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackVPNGatewayCreate, + Read: resourceCloudStackVPNGatewayRead, + Delete: resourceCloudStackVPNGatewayDelete, + + Schema: map[string]*schema.Schema{ + "vpc": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "public_ip": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceCloudStackVPNGatewayCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Retrieve the VPC UUID + vpcid, e := retrieveUUID(cs, "vpc", d.Get("vpc").(string)) + if e != nil { + return e.Error() + } + + // Create a new parameter struct + p := cs.VPN.NewCreateVpnGatewayParams(vpcid) + + // Create the new VPN Gateway + v, err := cs.VPN.CreateVpnGateway(p) + if err != nil { + return fmt.Errorf("Error creating VPN Gateway for VPC %s: %s", d.Get("vpc").(string), err) + } + + d.SetId(v.Id) + + return resourceCloudStackVPNGatewayRead(d, meta) +} + +func resourceCloudStackVPNGatewayRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the VPN Gateway details + v, count, err := cs.VPN.GetVpnGatewayByID(d.Id()) + if err != nil { + if count == 0 { + log.Printf( + "[DEBUG] VPN Gateway for VPC %s does no longer exist", d.Get("vpc").(string)) + d.SetId("") + return nil + } + + return err + } + + d.Set("public_ip", v.Publicip) + + return nil +} + +func resourceCloudStackVPNGatewayDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.VPN.NewDeleteVpnGatewayParams(d.Id()) + + // Delete the VPN Gateway + _, err := cs.VPN.DeleteVpnGateway(p) + if err != nil { + // This is a very poor way to be told the UUID does no longer exist :( + if strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", d.Id())) { + return nil + } + + return fmt.Errorf("Error deleting VPN Gateway for VPC %s: %s", d.Get("vpc").(string), err) + } + + return nil +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_vpn_gateway_test.go b/builtin/providers/cloudstack/resource_cloudstack_vpn_gateway_test.go new file mode 100644 index 000000000..db6c0085a --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_vpn_gateway_test.go @@ -0,0 +1,101 @@ +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackVPNGateway_basic(t *testing.T) { + var vpnGateway cloudstack.VpnGateway + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackVPNGatewayDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackVPNGateway_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackVPNGatewayExists( + "cloudstack_vpn_gateway.foo", &vpnGateway), + resource.TestCheckResourceAttr( + "cloudstack_vpn_gateway.foo", "vpc", "terraform-vpc"), + ), + }, + }, + }) +} + +func testAccCheckCloudStackVPNGatewayExists( + n string, vpnGateway *cloudstack.VpnGateway) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No VPN Gateway ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + v, _, err := cs.VPN.GetVpnGatewayByID(rs.Primary.ID) + + if err != nil { + return err + } + + if v.Id != rs.Primary.ID { + return fmt.Errorf("VPN Gateway not found") + } + + *vpnGateway = *v + + return nil + } +} + +func testAccCheckCloudStackVPNGatewayDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_vpn_gateway" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No VPN Gateway ID is set") + } + + p := cs.VPN.NewDeleteVpnGatewayParams(rs.Primary.ID) + _, err := cs.VPN.DeleteVpnGateway(p) + + if err != nil { + return fmt.Errorf( + "Error deleting VPN Gateway (%s): %s", + rs.Primary.ID, err) + } + } + + return nil +} + +var testAccCloudStackVPNGateway_basic = fmt.Sprintf(` +resource "cloudstack_vpc" "foo" { + name = "terraform-vpc" + display_text = "terraform-vpc-text" + cidr = "%s" + vpc_offering = "%s" + zone = "%s" +} + +resource "cloudstack_vpn_gateway" "foo" { + vpc = "${cloudstack_vpc.foo.name}" +}`, + CLOUDSTACK_VPC_CIDR_1, + CLOUDSTACK_VPC_OFFERING, + CLOUDSTACK_ZONE) diff --git a/builtin/providers/cloudstack/resources.go b/builtin/providers/cloudstack/resources.go index acef7b3da..76d38eb7c 100644 --- a/builtin/providers/cloudstack/resources.go +++ b/builtin/providers/cloudstack/resources.go @@ -40,8 +40,6 @@ func retrieveUUID(cs *cloudstack.CloudStackClient, name, value string) (uuid str uuid, err = cs.VPC.GetVPCOfferingID(value) case "vpc": uuid, err = cs.VPC.GetVPCID(value) - case "template": - uuid, err = cs.Template.GetTemplateID(value, "executable") case "network": uuid, err = cs.Network.GetNetworkID(value) case "zone": @@ -71,6 +69,22 @@ func retrieveUUID(cs *cloudstack.CloudStackClient, name, value string) (uuid str return uuid, nil } +func retrieveTemplateUUID(cs *cloudstack.CloudStackClient, zoneid, value string) (uuid string, e *retrieveError) { + // If the supplied value isn't a UUID, try to retrieve the UUID ourselves + if isUUID(value) { + return value, nil + } + + log.Printf("[DEBUG] Retrieving UUID of template: %s", value) + + uuid, err := cs.Template.GetTemplateID(value, "executable", zoneid) + if err != nil { + return uuid, &retrieveError{name: "template", value: value, err: err} + } + + return uuid, nil +} + func isUUID(s string) bool { re := regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`) return re.MatchString(s) diff --git a/builtin/providers/digitalocean/provider.go b/builtin/providers/digitalocean/provider.go index ecc7d67b8..080716e2e 100644 --- a/builtin/providers/digitalocean/provider.go +++ b/builtin/providers/digitalocean/provider.go @@ -21,6 +21,7 @@ func Provider() terraform.ResourceProvider { "digitalocean_domain": resourceDigitalOceanDomain(), "digitalocean_droplet": resourceDigitalOceanDroplet(), "digitalocean_record": resourceDigitalOceanRecord(), + "digitalocean_ssh_key": resourceDigitalOceanSSHKey(), }, ConfigureFunc: providerConfigure, diff --git a/builtin/providers/digitalocean/resource_digitalocean_ssh_key.go b/builtin/providers/digitalocean/resource_digitalocean_ssh_key.go new file mode 100644 index 000000000..c509d4725 --- /dev/null +++ b/builtin/providers/digitalocean/resource_digitalocean_ssh_key.go @@ -0,0 +1,114 @@ +package digitalocean + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/pearkes/digitalocean" +) + +func resourceDigitalOceanSSHKey() *schema.Resource { + return &schema.Resource{ + Create: resourceDigitalOceanSSHKeyCreate, + Read: resourceDigitalOceanSSHKeyRead, + Update: resourceDigitalOceanSSHKeyUpdate, + Delete: resourceDigitalOceanSSHKeyDelete, + + Schema: map[string]*schema.Schema{ + "id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "public_key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "fingerprint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceDigitalOceanSSHKeyCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*digitalocean.Client) + + // Build up our creation options + opts := &digitalocean.CreateSSHKey{ + Name: d.Get("name").(string), + PublicKey: d.Get("public_key").(string), + } + + log.Printf("[DEBUG] SSH Key create configuration: %#v", opts) + id, err := client.CreateSSHKey(opts) + if err != nil { + return fmt.Errorf("Error creating SSH Key: %s", err) + } + + d.SetId(id) + log.Printf("[INFO] SSH Key: %s", id) + + return resourceDigitalOceanSSHKeyRead(d, meta) +} + +func resourceDigitalOceanSSHKeyRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*digitalocean.Client) + + key, err := client.RetrieveSSHKey(d.Id()) + if err != nil { + // If the key is somehow already destroyed, mark as + // succesfully gone + if strings.Contains(err.Error(), "404 Not Found") { + d.SetId("") + return nil + } + + return fmt.Errorf("Error retrieving SSH key: %s", err) + } + + d.Set("name", key.Name) + d.Set("fingerprint", key.Fingerprint) + + return nil +} + +func resourceDigitalOceanSSHKeyUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*digitalocean.Client) + + var newName string + if v, ok := d.GetOk("name"); ok { + newName = v.(string) + } + + log.Printf("[DEBUG] SSH key update name: %#v", newName) + err := client.RenameSSHKey(d.Id(), newName) + if err != nil { + return fmt.Errorf("Failed to update SSH key: %s", err) + } + + return resourceDigitalOceanSSHKeyRead(d, meta) +} + +func resourceDigitalOceanSSHKeyDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*digitalocean.Client) + + log.Printf("[INFO] Deleting SSH key: %s", d.Id()) + err := client.DestroySSHKey(d.Id()) + if err != nil { + return fmt.Errorf("Error deleting SSH key: %s", err) + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/digitalocean/resource_digitalocean_ssh_key_test.go b/builtin/providers/digitalocean/resource_digitalocean_ssh_key_test.go new file mode 100644 index 000000000..d5c50e6f8 --- /dev/null +++ b/builtin/providers/digitalocean/resource_digitalocean_ssh_key_test.go @@ -0,0 +1,99 @@ +package digitalocean + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/pearkes/digitalocean" +) + +func TestAccDigitalOceanSSHKey_Basic(t *testing.T) { + var key digitalocean.SSHKey + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDigitalOceanSSHKeyDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckDigitalOceanSSHKeyConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanSSHKeyExists("digitalocean_ssh_key.foobar", &key), + testAccCheckDigitalOceanSSHKeyAttributes(&key), + resource.TestCheckResourceAttr( + "digitalocean_ssh_key.foobar", "name", "foobar"), + resource.TestCheckResourceAttr( + "digitalocean_ssh_key.foobar", "public_key", "abcdef"), + ), + }, + }, + }) +} + +func testAccCheckDigitalOceanSSHKeyDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*digitalocean.Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "digitalocean_ssh_key" { + continue + } + + // Try to find the key + _, err := client.RetrieveSSHKey(rs.Primary.ID) + + if err == nil { + fmt.Errorf("SSH key still exists") + } + } + + return nil +} + +func testAccCheckDigitalOceanSSHKeyAttributes(key *digitalocean.SSHKey) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if key.Name != "foobar" { + return fmt.Errorf("Bad name: %s", key.Name) + } + + return nil + } +} + +func testAccCheckDigitalOceanSSHKeyExists(n string, key *digitalocean.SSHKey) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Record ID is set") + } + + client := testAccProvider.Meta().(*digitalocean.Client) + + foundKey, err := client.RetrieveSSHKey(rs.Primary.ID) + + if err != nil { + return err + } + + if foundKey.Name != rs.Primary.ID { + return fmt.Errorf("Record not found") + } + + *key = foundKey + + return nil + } +} + +const testAccCheckDigitalOceanSSHKeyConfig_basic = ` +resource "digitalocean_ssh_key" "foobar" { + name = "foobar" + public_key = "abcdef" +}` diff --git a/builtin/providers/google/resource_compute_instance.go b/builtin/providers/google/resource_compute_instance.go index 91b7af2e1..3b3e86ded 100644 --- a/builtin/providers/google/resource_compute_instance.go +++ b/builtin/providers/google/resource_compute_instance.go @@ -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 diff --git a/command/cli_ui.go b/command/cli_ui.go index b9fc23556..d9016db03 100644 --- a/command/cli_ui.go +++ b/command/cli_ui.go @@ -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 diff --git a/command/command.go b/command/command.go index 7779a64bf..c42313b3b 100644 --- a/command/command.go +++ b/command/command.go @@ -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 false } return true diff --git a/command/init.go b/command/init.go index 643b34973..328c473d0 100644 --- a/command/init.go +++ b/command/init.go @@ -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, } diff --git a/command/meta.go b/command/meta.go index 4745ef6a1..7cf3ebe05 100644 --- a/command/meta.go +++ b/command/meta.go @@ -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, }, } diff --git a/command/remote.go b/command/remote.go index f304a0aa2..3e9e05f1b 100644 --- a/command/remote.go +++ b/command/remote.go @@ -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 [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" } diff --git a/command/remote_config.go b/command/remote_config.go new file mode 100644 index 000000000..cb95c4b94 --- /dev/null +++ b/command/remote_config.go @@ -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" +} diff --git a/command/pull.go b/command/remote_pull.go similarity index 90% rename from command/pull.go rename to command/remote_pull.go index e5976f2a5..3965f0d42 100644 --- a/command/pull.go +++ b/command/remote_pull.go @@ -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" } diff --git a/command/pull_test.go b/command/remote_pull_test.go similarity index 94% rename from command/pull_test.go rename to command/remote_pull_test.go index d0bd08b86..94b52ce2b 100644 --- a/command/pull_test.go +++ b/command/remote_pull_test.go @@ -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, diff --git a/command/push.go b/command/remote_push.go similarity index 91% rename from command/push.go rename to command/remote_push.go index a02553c21..259c82863 100644 --- a/command/push.go +++ b/command/remote_push.go @@ -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" } diff --git a/command/push_test.go b/command/remote_push_test.go similarity index 89% rename from command/push_test.go rename to command/remote_push_test.go index 59c1c834e..d92c3e8ab 100644 --- a/command/push_test.go +++ b/command/remote_push_test.go @@ -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, diff --git a/command/remote_test.go b/command/remote_test.go index be93eaf24..0452e3416 100644 --- a/command/remote_test.go +++ b/command/remote_test.go @@ -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, diff --git a/commands.go b/commands.go index 8c10a9a06..c585b7827 100644 --- a/commands.go +++ b/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, diff --git a/config/interpolate_funcs.go b/config/interpolate_funcs.go index d6d5e3779..8bb76c532 100644 --- a/config/interpolate_funcs.go +++ b/config/interpolate_funcs.go @@ -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 { diff --git a/config/interpolate_funcs_test.go b/config/interpolate_funcs_test.go index 73a793c0f..2061e6ad8 100644 --- a/config/interpolate_funcs_test.go +++ b/config/interpolate_funcs_test.go @@ -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{ diff --git a/config/lang/ast/ast.go b/config/lang/ast/ast.go index fc6c966b0..1d784c78a 100644 --- a/config/lang/ast/ast.go +++ b/config/lang/ast/ast.go @@ -48,7 +48,8 @@ type Type uint32 const ( TypeInvalid Type = 0 - TypeString Type = 1 << iota + TypeAny Type = 1 << iota + TypeString TypeInt TypeFloat ) diff --git a/config/lang/ast/type_string.go b/config/lang/ast/type_string.go index fd0e9e355..d9b5a2df4 100644 --- a/config/lang/ast/type_string.go +++ b/config/lang/ast/type_string.go @@ -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) } diff --git a/config/lang/check_types.go b/config/lang/check_types.go index f5cf16680..0396eb1f3 100644 --- a/config/lang/check_types.go +++ b/config/lang/check_types.go @@ -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 { diff --git a/helper/schema/resource.go b/helper/schema/resource.go index f0e0515cd..a19912eed 100644 --- a/helper/schema/resource.go +++ b/helper/schema/resource.go @@ -3,6 +3,7 @@ package schema import ( "errors" "fmt" + "strconv" "github.com/hashicorp/terraform/terraform" ) @@ -24,6 +25,31 @@ type Resource struct { // resource. Schema map[string]*Schema + // SchemaVersion is the version number for this resource's Schema + // definition. The current SchemaVersion stored in the state for each + // resource. Provider authors can increment this version number + // when Schema semantics change. If the State's SchemaVersion is less than + // the current SchemaVersion, the InstanceState is yielded to the + // MigrateState callback, where the provider can make whatever changes it + // needs to update the state to be compatible to the latest version of the + // Schema. + // + // When unset, SchemaVersion defaults to 0, so provider authors can start + // their Versioning at any integer >= 1 + SchemaVersion int + + // MigrateState is responsible for updating an InstanceState with an old + // version to the format expected by the current version of the Schema. + // + // It is called during Refresh if the State's stored SchemaVersion is less + // than the current SchemaVersion of the Resource. + // + // The function is yielded the state's stored SchemaVersion and a pointer to + // the InstanceState that needs updating, as well as the configured + // provider's configured meta interface{}, in case the migration process + // needs to make any remote API calls. + MigrateState StateMigrateFunc + // The functions below are the CRUD operations for this resource. // // The only optional operation is Update. If Update is not implemented, @@ -69,6 +95,10 @@ type DeleteFunc func(*ResourceData, interface{}) error // See Resource documentation. type ExistsFunc func(*ResourceData, interface{}) (bool, error) +// See Resource documentation. +type StateMigrateFunc func( + int, *terraform.InstanceState, interface{}) (*terraform.InstanceState, error) + // Apply creates, updates, and/or deletes a resource. func (r *Resource) Apply( s *terraform.InstanceState, @@ -158,6 +188,14 @@ func (r *Resource) Refresh( } } + needsMigration, stateSchemaVersion := r.checkSchemaVersion(s) + if needsMigration && r.MigrateState != nil { + s, err := r.MigrateState(stateSchemaVersion, s, meta) + if err != nil { + return s, err + } + } + data, err := schemaMap(r.Schema).Data(s, nil) if err != nil { return s, err @@ -169,6 +207,13 @@ func (r *Resource) Refresh( state = nil } + if state != nil && r.SchemaVersion > 0 { + if state.Meta == nil { + state.Meta = make(map[string]string) + } + state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion) + } + return state, err } @@ -189,3 +234,10 @@ func (r *Resource) InternalValidate() error { return schemaMap(r.Schema).InternalValidate() } + +// Determines if a given InstanceState needs to be migrated by checking the +// stored version number with the current SchemaVersion +func (r *Resource) checkSchemaVersion(is *terraform.InstanceState) (bool, int) { + stateSchemaVersion, _ := strconv.Atoi(is.Meta["schema_version"]) + return stateSchemaVersion < r.SchemaVersion, stateSchemaVersion +} diff --git a/helper/schema/resource_test.go b/helper/schema/resource_test.go index 0c71abddf..b1c42721f 100644 --- a/helper/schema/resource_test.go +++ b/helper/schema/resource_test.go @@ -3,6 +3,7 @@ package schema import ( "fmt" "reflect" + "strconv" "testing" "github.com/hashicorp/terraform/terraform" @@ -478,3 +479,218 @@ func TestResourceRefresh_noExists(t *testing.T) { t.Fatalf("should have no state") } } + +func TestResourceRefresh_needsMigration(t *testing.T) { + // Schema v2 it deals only in newfoo, which tracks foo as an int + r := &Resource{ + SchemaVersion: 2, + Schema: map[string]*Schema{ + "newfoo": &Schema{ + Type: TypeInt, + Optional: true, + }, + }, + } + + r.Read = func(d *ResourceData, m interface{}) error { + return d.Set("newfoo", d.Get("newfoo").(int)+1) + } + + r.MigrateState = func( + v int, + s *terraform.InstanceState, + meta interface{}) (*terraform.InstanceState, error) { + // Real state migration functions will probably switch on this value, + // but we'll just assert on it for now. + if v != 1 { + t.Fatalf("Expected StateSchemaVersion to be 1, got %d", v) + } + + if meta != 42 { + t.Fatal("Expected meta to be passed through to the migration function") + } + + oldfoo, err := strconv.ParseFloat(s.Attributes["oldfoo"], 64) + if err != nil { + t.Fatalf("err: %#v", err) + } + s.Attributes["newfoo"] = strconv.Itoa((int(oldfoo * 10))) + delete(s.Attributes, "oldfoo") + + return s, nil + } + + // State is v1 and deals in oldfoo, which tracked foo as a float at 1/10th + // the scale of newfoo + s := &terraform.InstanceState{ + ID: "bar", + Attributes: map[string]string{ + "oldfoo": "1.2", + }, + Meta: map[string]string{ + "schema_version": "1", + }, + } + + actual, err := r.Refresh(s, 42) + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := &terraform.InstanceState{ + ID: "bar", + Attributes: map[string]string{ + "id": "bar", + "newfoo": "13", + }, + Meta: map[string]string{ + "schema_version": "2", + }, + } + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad:\n\nexpected: %#v\ngot: %#v", expected, actual) + } +} + +func TestResourceRefresh_noMigrationNeeded(t *testing.T) { + r := &Resource{ + SchemaVersion: 2, + Schema: map[string]*Schema{ + "newfoo": &Schema{ + Type: TypeInt, + Optional: true, + }, + }, + } + + r.Read = func(d *ResourceData, m interface{}) error { + return d.Set("newfoo", d.Get("newfoo").(int)+1) + } + + r.MigrateState = func( + v int, + s *terraform.InstanceState, + meta interface{}) (*terraform.InstanceState, error) { + t.Fatal("Migrate function shouldn't be called!") + return nil, nil + } + + s := &terraform.InstanceState{ + ID: "bar", + Attributes: map[string]string{ + "newfoo": "12", + }, + Meta: map[string]string{ + "schema_version": "2", + }, + } + + actual, err := r.Refresh(s, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := &terraform.InstanceState{ + ID: "bar", + Attributes: map[string]string{ + "id": "bar", + "newfoo": "13", + }, + Meta: map[string]string{ + "schema_version": "2", + }, + } + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad:\n\nexpected: %#v\ngot: %#v", expected, actual) + } +} + +func TestResourceRefresh_stateSchemaVersionUnset(t *testing.T) { + r := &Resource{ + // Version 1 > Version 0 + SchemaVersion: 1, + Schema: map[string]*Schema{ + "newfoo": &Schema{ + Type: TypeInt, + Optional: true, + }, + }, + } + + r.Read = func(d *ResourceData, m interface{}) error { + return d.Set("newfoo", d.Get("newfoo").(int)+1) + } + + r.MigrateState = func( + v int, + s *terraform.InstanceState, + meta interface{}) (*terraform.InstanceState, error) { + s.Attributes["newfoo"] = s.Attributes["oldfoo"] + return s, nil + } + + s := &terraform.InstanceState{ + ID: "bar", + Attributes: map[string]string{ + "oldfoo": "12", + }, + } + + actual, err := r.Refresh(s, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := &terraform.InstanceState{ + ID: "bar", + Attributes: map[string]string{ + "id": "bar", + "newfoo": "13", + }, + Meta: map[string]string{ + "schema_version": "1", + }, + } + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad:\n\nexpected: %#v\ngot: %#v", expected, actual) + } +} + +func TestResourceRefresh_migrateStateErr(t *testing.T) { + r := &Resource{ + SchemaVersion: 2, + Schema: map[string]*Schema{ + "newfoo": &Schema{ + Type: TypeInt, + Optional: true, + }, + }, + } + + r.Read = func(d *ResourceData, m interface{}) error { + t.Fatal("Read should never be called!") + return nil + } + + r.MigrateState = func( + v int, + s *terraform.InstanceState, + meta interface{}) (*terraform.InstanceState, error) { + return s, fmt.Errorf("triggering an error") + } + + s := &terraform.InstanceState{ + ID: "bar", + Attributes: map[string]string{ + "oldfoo": "12", + }, + } + + _, err := r.Refresh(s, nil) + if err == nil { + t.Fatal("expected error, but got none!") + } +} diff --git a/helper/schema/schema.go b/helper/schema/schema.go index e05acd395..33c8b8310 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -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. diff --git a/helper/schema/schema_test.go b/helper/schema/schema_test.go index 81f38aa76..2c9e89f63 100644 --- a/helper/schema/schema_test.go +++ b/helper/schema/schema_test.go @@ -2583,15 +2583,15 @@ func TestSchemaMap_InternalValidate(t *testing.T) { } func TestSchemaMap_Validate(t *testing.T) { - cases := []struct { - Schema map[string]*Schema - Config map[string]interface{} - Vars map[string]string - Warn bool - Err bool + cases := map[string]struct { + Schema map[string]*Schema + Config map[string]interface{} + Vars map[string]string + 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) + } } } } diff --git a/helper/ssh/communicator.go b/helper/ssh/communicator.go index 186fd4824..817f37368 100644 --- a/helper/ssh/communicator.go +++ b/helper/ssh/communicator.go @@ -14,7 +14,7 @@ import ( "sync" "time" - "code.google.com/p/go.crypto/ssh" + "golang.org/x/crypto/ssh" ) // RemoteCmd represents a remote command being prepared or run. diff --git a/helper/ssh/communicator_test.go b/helper/ssh/communicator_test.go index 2e16e1482..0e80c9415 100644 --- a/helper/ssh/communicator_test.go +++ b/helper/ssh/communicator_test.go @@ -4,7 +4,7 @@ package ssh import ( "bytes" - "code.google.com/p/go.crypto/ssh" + "golang.org/x/crypto/ssh" "fmt" "net" "testing" diff --git a/helper/ssh/password.go b/helper/ssh/password.go index 934bcd01f..8db6f82da 100644 --- a/helper/ssh/password.go +++ b/helper/ssh/password.go @@ -1,7 +1,7 @@ package ssh import ( - "code.google.com/p/go.crypto/ssh" + "golang.org/x/crypto/ssh" "log" ) diff --git a/helper/ssh/password_test.go b/helper/ssh/password_test.go index e74b46e06..6e3e0a257 100644 --- a/helper/ssh/password_test.go +++ b/helper/ssh/password_test.go @@ -1,7 +1,7 @@ package ssh import ( - "code.google.com/p/go.crypto/ssh" + "golang.org/x/crypto/ssh" "reflect" "testing" ) diff --git a/helper/ssh/provisioner.go b/helper/ssh/provisioner.go index baebbd9b6..2d60d8934 100644 --- a/helper/ssh/provisioner.go +++ b/helper/ssh/provisioner.go @@ -7,7 +7,7 @@ import ( "log" "time" - "code.google.com/p/go.crypto/ssh" + "golang.org/x/crypto/ssh" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/go-homedir" "github.com/mitchellh/mapstructure" diff --git a/scripts/build.sh b/scripts/build.sh index bf6074068..4433d8f9b 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -19,9 +19,11 @@ GIT_DIRTY=$(test -n "`git status --porcelain`" && echo "+CHANGES" || true) XC_ARCH=${XC_ARCH:-"386 amd64 arm"} XC_OS=${XC_OS:-linux darwin windows freebsd openbsd} -# Install dependencies -echo "==> Getting dependencies..." -go get ./... +# Install dependencies unless running in quick mode +if [ "${TF_QUICKDEV}x" == "x" ]; then + echo "==> Getting dependencies..." + go get ./... +fi # Delete the old dir echo "==> Removing old directory..." diff --git a/state/remote/file.go b/state/remote/file.go new file mode 100644 index 000000000..f3cbdb45e --- /dev/null +++ b/state/remote/file.go @@ -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) +} diff --git a/state/remote/file_test.go b/state/remote/file_test.go new file mode 100644 index 000000000..352d787db --- /dev/null +++ b/state/remote/file_test.go @@ -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) +} diff --git a/state/remote/remote.go b/state/remote/remote.go index fe730531e..19632a9fd 100644 --- a/state/remote/remote.go +++ b/state/remote/remote.go @@ -39,4 +39,7 @@ var BuiltinClients = map[string]Factory{ "atlas": atlasFactory, "consul": consulFactory, "http": httpFactory, + + // This is used for development purposes only. + "_local": fileFactory, } diff --git a/terraform/context_test.go b/terraform/context_test.go index aaeda9159..9050d4b96 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -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 diff --git a/terraform/eval_error.go b/terraform/eval_error.go new file mode 100644 index 000000000..470f798b7 --- /dev/null +++ b/terraform/eval_error.go @@ -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 +} diff --git a/terraform/eval_if.go b/terraform/eval_if.go index c96e13229..d6b46a1f2 100644 --- a/terraform/eval_if.go +++ b/terraform/eval_if.go @@ -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 diff --git a/terraform/eval_state.go b/terraform/eval_state.go index 161752eb4..cea738661 100644 --- a/terraform/eval_state.go +++ b/terraform/eval_state.go @@ -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 + Name string + 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 + Name string + ResourceType string + Dependencies []string + State **InstanceState } -// 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 } diff --git a/terraform/eval_state_test.go b/terraform/eval_state_test.go index b2783da9a..3e5265f4a 100644 --- a/terraform/eval_state_test.go +++ b/terraform/eval_state_test.go @@ -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 = + 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 = + Deposed ID 1 = i-abc123 + `) +} diff --git a/terraform/graph_config_node.go b/terraform/graph_config_node.go index 07e53ef09..625992f3f 100644 --- a/terraform/graph_config_node.go +++ b/terraform/graph_config_node.go @@ -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{ - State: state, - View: n.Resource.Id(), - Deposed: n.Resource.Lifecycle.CreateBeforeDestroy, - DeposedInclude: true, - }) - } + steps = append(steps, &DeposedTransformer{ + State: state, + View: n.Resource.Id(), + }) 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, + State: state, + View: n.Resource.Id(), }) } diff --git a/terraform/state.go b/terraform/state.go index 85492f31c..3dbad7ae6 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -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() { @@ -794,6 +832,11 @@ type InstanceState struct { // that is necessary for the Terraform run to complete, but is not // persisted to a state file. Ephemeral EphemeralState `json:"-"` + + // Meta is a simple K/V map that is persisted to the State but otherwise + // ignored by Terraform core. It's meant to be used for accounting by + // external client code. + Meta map[string]string `json:"meta,omitempty"` } func (i *InstanceState) init() { diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 4070475c0..94664791f 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -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 = ` diff --git a/terraform/test-fixtures/apply-multi-depose-create-before-destroy/main.tf b/terraform/test-fixtures/apply-multi-depose-create-before-destroy/main.tf new file mode 100644 index 000000000..ac7ba4b9b --- /dev/null +++ b/terraform/test-fixtures/apply-multi-depose-create-before-destroy/main.tf @@ -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 + } +} diff --git a/terraform/transform_deposed.go b/terraform/transform_deposed.go new file mode 100644 index 000000000..b7c7e3f06 --- /dev/null +++ b/terraform/transform_deposed.go @@ -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 +} diff --git a/terraform/transform_resource.go b/terraform/transform_resource.go index 4b1985450..8c2a00c78 100644 --- a/terraform/transform_resource.go +++ b/terraform/transform_resource.go @@ -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{ - Name: n.stateId(), - ResourceType: n.Resource.Type, - Dependencies: n.DependentOn(), - State: &state, - Tainted: &tainted, - TaintedIndex: -1, - TaintedClearPrimary: !n.Resource.Lifecycle.CreateBeforeDestroy, + &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, + 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{ - Name: n.stateId(), - Output: &state, - Tainted: n.Resource.Lifecycle.CreateBeforeDestroy, - TaintedIndex: -1, + &EvalIf{ + If: func(ctx EvalContext) (bool, error) { + return n.Resource.Lifecycle.CreateBeforeDestroy, nil + }, + Then: &EvalReadStateTainted{ + Name: n.stateId(), + Output: &state, + Index: -1, + }, + Else: &EvalReadState{ + Name: n.stateId(), + Output: &state, + }, }, &EvalRequireState{ State: &state, diff --git a/terraform/transform_tainted.go b/terraform/transform_tainted.go index 1e1b2cb31..c88516ade 100644 --- a/terraform/transform_tainted.go +++ b/terraform/transform_tainted.go @@ -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,11 +87,10 @@ func (n *graphNodeTaintedResource) EvalTree() EvalNode { Name: n.ProvidedBy()[0], Output: &provider, }, - &EvalReadState{ - Name: n.ResourceName, - Tainted: true, - TaintedIndex: n.Index, - Output: &state, + &EvalReadStateTainted{ + Name: n.ResourceName, + Index: n.Index, + Output: &state, }, &EvalRefresh{ Info: info, @@ -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,11 +118,10 @@ func (n *graphNodeTaintedResource) EvalTree() EvalNode { Name: n.ProvidedBy()[0], Output: &provider, }, - &EvalReadState{ - Name: n.ResourceName, - Tainted: true, - TaintedIndex: n.Index, - Output: &state, + &EvalReadStateTainted{ + Name: n.ResourceName, + Index: n.Index, + Output: &state, }, &EvalDiffDestroy{ Info: info, @@ -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{}, }, diff --git a/website/Gemfile b/website/Gemfile index 936305b5e..850fc4b6c 100644 --- a/website/Gemfile +++ b/website/Gemfile @@ -1,3 +1,3 @@ source 'https://rubygems.org' -gem 'middleman-hashicorp', github: 'hashicorp/middleman-hashicorp' +gem 'middleman-hashicorp', git: 'https://github.com/hashicorp/middleman-hashicorp' diff --git a/website/Gemfile.lock b/website/Gemfile.lock index e956a6aa6..a55579e6d 100644 --- a/website/Gemfile.lock +++ b/website/Gemfile.lock @@ -1,19 +1,19 @@ GIT - remote: git://github.com/hashicorp/middleman-hashicorp.git - revision: 30c15f93fb501041cff97c490b60ddc96c8314c9 + remote: https://github.com/hashicorp/middleman-hashicorp + revision: 783fe9517dd02badb85e5ddfeda4d8e35bbd05a8 specs: middleman-hashicorp (0.1.0) - bootstrap-sass (~> 3.2) + bootstrap-sass (~> 3.3) builder (~> 3.2) less (~> 2.6) middleman (~> 3.3) - middleman-livereload (~> 3.3) + middleman-livereload (~> 3.4) middleman-minify-html (~> 3.4) middleman-syntax (~> 2.0) - rack-contrib (~> 1.1) + rack-contrib (~> 1.2) rack-rewrite (~> 1.5) rack-ssl-enforcer (~> 0.2) - redcarpet (~> 3.1) + redcarpet (~> 3.2) therubyracer (~> 0.12) thin (~> 1.6) @@ -26,7 +26,7 @@ GEM minitest (~> 5.1) thread_safe (~> 0.1) tzinfo (~> 1.1) - autoprefixer-rails (5.0.0.2) + autoprefixer-rails (5.1.7) execjs json bootstrap-sass (3.3.3) @@ -35,11 +35,11 @@ GEM builder (3.2.2) celluloid (0.16.0) timers (~> 4.0.0) - chunky_png (1.3.3) + chunky_png (1.3.4) coffee-script (2.3.0) coffee-script-source execjs - coffee-script-source (1.8.0) + coffee-script-source (1.9.1) commonjs (0.2.7) compass (1.0.3) chunky_png (~> 1.2) @@ -58,8 +58,8 @@ GEM eventmachine (>= 0.12.9) http_parser.rb (~> 0.6.0) erubis (2.7.0) - eventmachine (1.0.4) - execjs (2.2.2) + eventmachine (1.0.7) + execjs (2.4.0) ffi (1.9.6) haml (4.0.6) tilt @@ -69,9 +69,9 @@ GEM uber (~> 0.0.4) htmlcompressor (0.1.2) http_parser.rb (0.6.0) - i18n (0.6.11) + i18n (0.7.0) json (1.8.2) - kramdown (1.5.0) + kramdown (1.6.0) less (2.6.0) commonjs (~> 0.2.7) libv8 (3.16.14.7) @@ -79,23 +79,23 @@ GEM celluloid (>= 0.15.2) rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) - middleman (3.3.7) + middleman (3.3.10) coffee-script (~> 2.2) compass (>= 1.0.0, < 2.0.0) compass-import-once (= 1.0.5) execjs (~> 2.0) haml (>= 4.0.5) kramdown (~> 1.2) - middleman-core (= 3.3.7) + middleman-core (= 3.3.10) middleman-sprockets (>= 3.1.2) sass (>= 3.4.0, < 4.0) uglifier (~> 2.5) - middleman-core (3.3.7) + middleman-core (3.3.10) activesupport (~> 4.1.0) bundler (~> 1.1) erubis hooks (~> 0.3) - i18n (~> 0.6.9) + i18n (~> 0.7.0) listen (>= 2.7.9, < 3.0) padrino-helpers (~> 0.12.3) rack (>= 1.4.5, < 2.0) @@ -109,7 +109,7 @@ GEM middleman-minify-html (3.4.0) htmlcompressor (~> 0.1.0) middleman-core (>= 3.2) - middleman-sprockets (3.4.1) + middleman-sprockets (3.4.2) middleman-core (>= 3.3) sprockets (~> 2.12.1) sprockets-helpers (~> 1.1.0) @@ -118,12 +118,12 @@ GEM middleman-core (~> 3.2) rouge (~> 1.0) minitest (5.5.1) - multi_json (1.10.1) - padrino-helpers (0.12.4) + multi_json (1.11.0) + padrino-helpers (0.12.5) i18n (~> 0.6, >= 0.6.7) - padrino-support (= 0.12.4) + padrino-support (= 0.12.5) tilt (~> 1.4.1) - padrino-support (0.12.4) + padrino-support (0.12.5) activesupport (>= 3.1) rack (1.6.0) rack-contrib (1.2.0) @@ -139,8 +139,8 @@ GEM ffi (>= 0.5.0) redcarpet (3.2.2) ref (1.0.5) - rouge (1.7.7) - sass (3.4.10) + rouge (1.8.0) + sass (3.4.13) sprockets (2.12.3) hike (~> 1.2) multi_json (~> 1.0) @@ -166,7 +166,7 @@ GEM tzinfo (1.2.2) thread_safe (~> 0.1) uber (0.0.13) - uglifier (2.7.0) + uglifier (2.7.1) execjs (>= 0.3.0) json (>= 1.8.0) diff --git a/website/source/docs/commands/index.html.markdown b/website/source/docs/commands/index.html.markdown index aa79c29c9..d0156f212 100644 --- a/website/source/docs/commands/index.html.markdown +++ b/website/source/docs/commands/index.html.markdown @@ -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 ``` diff --git a/website/source/docs/commands/pull.html.markdown b/website/source/docs/commands/pull.html.markdown deleted file mode 100644 index 385534480..000000000 --- a/website/source/docs/commands/pull.html.markdown +++ /dev/null @@ -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. - diff --git a/website/source/docs/commands/push.html.markdown b/website/source/docs/commands/push.html.markdown deleted file mode 100644 index 1a592b192..000000000 --- a/website/source/docs/commands/push.html.markdown +++ /dev/null @@ -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. - diff --git a/website/source/docs/commands/remote-config.html.markdown b/website/source/docs/commands/remote-config.html.markdown new file mode 100644 index 000000000..3fd6c9b17 --- /dev/null +++ b/website/source/docs/commands/remote-config.html.markdown @@ -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. + diff --git a/website/source/docs/commands/remote-pull.html.markdown b/website/source/docs/commands/remote-pull.html.markdown new file mode 100644 index 000000000..db74689fe --- /dev/null +++ b/website/source/docs/commands/remote-pull.html.markdown @@ -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. + diff --git a/website/source/docs/commands/remote-push.html.markdown b/website/source/docs/commands/remote-push.html.markdown new file mode 100644 index 000000000..fa0cc4c0c --- /dev/null +++ b/website/source/docs/commands/remote-push.html.markdown @@ -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. + diff --git a/website/source/docs/commands/remote.html.markdown b/website/source/docs/commands/remote.html.markdown index cc9c4ff47..3bc96c802 100644 --- a/website/source/docs/commands/remote.html.markdown +++ b/website/source/docs/commands/remote.html.markdown @@ -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). diff --git a/website/source/docs/configuration/interpolation.html.md b/website/source/docs/configuration/interpolation.html.md index 70a73f826..15f39ddf1 100644 --- a/website/source/docs/configuration/interpolation.html.md +++ b/website/source/docs/configuration/interpolation.html.md @@ -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)` diff --git a/website/source/docs/providers/aws/r/db_instance.html.markdown b/website/source/docs/providers/aws/r/db_instance.html.markdown index 7727b9dd6..4b2253115 100644 --- a/website/source/docs/providers/aws/r/db_instance.html.markdown +++ b/website/source/docs/providers/aws/r/db_instance.html.markdown @@ -61,6 +61,7 @@ The following arguments are supported: Only used for [DB Instances on the _EC2-Classic_ Platform](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_VPC.html#USER_VPC.FindDefaultVPC). * `db_subnet_group_name` - (Optional) Name of DB subnet group * `parameter_group_name` - (Optional) Name of the DB parameter group to associate. +* `storage_encrypted` - (Optional) Specifies whether the DB instance is encrypted. The Default is `false` if not specified. ## Attributes Reference @@ -82,4 +83,5 @@ The following attributes are exported: * `port` - The database port * `status` - The RDS instance status * `username` - The master username for the database +* `storage_encrypted` - Specifies whether the DB instance is encrypted diff --git a/website/source/docs/providers/aws/r/instance.html.markdown b/website/source/docs/providers/aws/r/instance.html.markdown index fc959fa2a..94f042af3 100644 --- a/website/source/docs/providers/aws/r/instance.html.markdown +++ b/website/source/docs/providers/aws/r/instance.html.markdown @@ -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 diff --git a/website/source/docs/providers/cloudstack/r/egress_firewall.html.markdown b/website/source/docs/providers/cloudstack/r/egress_firewall.html.markdown index 17fa20927..b905bc0e9 100644 --- a/website/source/docs/providers/cloudstack/r/egress_firewall.html.markdown +++ b/website/source/docs/providers/cloudstack/r/egress_firewall.html.markdown @@ -58,4 +58,4 @@ The `rule` block supports: The following attributes are exported: -* `ID` - The network ID for which the egress firewall rules are created. +* `id` - The network ID for which the egress firewall rules are created. diff --git a/website/source/docs/providers/cloudstack/r/firewall.html.markdown b/website/source/docs/providers/cloudstack/r/firewall.html.markdown index 1c659e6bf..8b8aa0089 100644 --- a/website/source/docs/providers/cloudstack/r/firewall.html.markdown +++ b/website/source/docs/providers/cloudstack/r/firewall.html.markdown @@ -58,4 +58,4 @@ The `rule` block supports: The following attributes are exported: -* `ID` - The IP address ID for which the firewall rules are created. +* `id` - The IP address ID for which the firewall rules are created. diff --git a/website/source/docs/providers/cloudstack/r/network_acl_rule.html.markdown b/website/source/docs/providers/cloudstack/r/network_acl_rule.html.markdown index fb6b0891f..f82b8f446 100644 --- a/website/source/docs/providers/cloudstack/r/network_acl_rule.html.markdown +++ b/website/source/docs/providers/cloudstack/r/network_acl_rule.html.markdown @@ -66,4 +66,4 @@ The `rule` block supports: The following attributes are exported: -* `ID` - The ACL ID for which the rules are created. +* `id` - The ACL ID for which the rules are created. diff --git a/website/source/docs/providers/cloudstack/r/vpn_connection.html.markdown b/website/source/docs/providers/cloudstack/r/vpn_connection.html.markdown new file mode 100644 index 000000000..3ecf17cbc --- /dev/null +++ b/website/source/docs/providers/cloudstack/r/vpn_connection.html.markdown @@ -0,0 +1,38 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_vpn_connection" +sidebar_current: "docs-cloudstack-resource-vpn-connection" +description: |- + Creates a site to site VPN connection. +--- + +# cloudstack\_vpn\_connection + +Creates a site to site VPN connection. + +## Example Usage + +Basic usage: + +``` +resource "cloudstack_vpn_connection" "default" { + customergatewayid = "xxx" + vpngatewayid = "xxx" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `customergatewayid` - (Required) The Customer Gateway ID to connect. + Changing this forces a new resource to be created. + +* `vpngatewayid` - (Required) The VPN Gateway ID to connect. + Changing this forces a new resource to be created. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the VPN Connection. diff --git a/website/source/docs/providers/cloudstack/r/vpn_customer_gateway.html.markdown b/website/source/docs/providers/cloudstack/r/vpn_customer_gateway.html.markdown new file mode 100644 index 000000000..84183b8d6 --- /dev/null +++ b/website/source/docs/providers/cloudstack/r/vpn_customer_gateway.html.markdown @@ -0,0 +1,59 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_vpn_customer_gateway" +sidebar_current: "docs-cloudstack-resource-vpn-customer-gateway" +description: |- + Creates a site to site VPN local customer gateway. +--- + +# cloudstack\_vpn\_customer\_gateway + +Creates a site to site VPN local customer gateway. + +## Example Usage + +Basic usage: + +``` +resource "cloudstack_vpn_customer_gateway" "default" { + name = "test-vpc" + cidr = "10.0.0.0/8" + esp_policy = "aes256-sha1" + gateway = "192.168.0.1" + ike_policy = "aes256-sha1" + ipsec_psk = "terraform" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the VPN Customer Gateway. + +* `cidr` - (Required) The CIDR block that needs to be routed through this gateway. + +* `esp_policy` - (Required) The ESP policy to use for this VPN Customer Gateway. + +* `gateway` - (Required) The public IP address of the related VPN Gateway. + +* `ike_policy` - (Required) The IKE policy to use for this VPN Customer Gateway. + +* `ipsec_psk` - (Required) The IPSEC pre-shared key used for this gateway. + +* `dpd` - (Optional) If DPD is enabled for the related VPN connection (defaults false) + +* `esp_lifetime` - (Optional) The ESP lifetime of phase 2 VPN connection to this + VPN Customer Gateway in seconds (defaults 86400) + +* `ike_lifetime` - (Optional) The IKE lifetime of phase 2 VPN connection to this + VPN Customer Gateway in seconds (defaults 86400) + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the VPN Customer Gateway. +* `dpd` - Enable or disable DPD is enabled for the related VPN connection. +* `esp_lifetime` - The ESP lifetime of phase 2 VPN connection to this VPN Customer Gateway. +* `ike_lifetime` - The IKE lifetime of phase 2 VPN connection to this VPN Customer Gateway. diff --git a/website/source/docs/providers/cloudstack/r/vpn_gateway.html.markdown b/website/source/docs/providers/cloudstack/r/vpn_gateway.html.markdown new file mode 100644 index 000000000..10aabd796 --- /dev/null +++ b/website/source/docs/providers/cloudstack/r/vpn_gateway.html.markdown @@ -0,0 +1,35 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_vpn_gateway" +sidebar_current: "docs-cloudstack-resource-vpn-gateway" +description: |- + Creates a site to site VPN local gateway. +--- + +# cloudstack\_vpn\_gateway + +Creates a site to site VPN local gateway. + +## Example Usage + +Basic usage: + +``` +resource "cloudstack_vpn_gateway" "default" { + vpc = "test-vpc" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `vpc` - (Required) The name of the VPC for which to create the VPN Gateway. + Changing this forces a new resource to be created. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the VPN Gateway. +* `public_ip` - The public IP address associated with the VPN Gateway. diff --git a/website/source/docs/providers/do/r/ssh_key.html.markdown b/website/source/docs/providers/do/r/ssh_key.html.markdown new file mode 100644 index 000000000..7a8033519 --- /dev/null +++ b/website/source/docs/providers/do/r/ssh_key.html.markdown @@ -0,0 +1,41 @@ +--- +layout: "digitalocean" +page_title: "DigitalOcean: digitalocean_ssh_key" +sidebar_current: "docs-do-resource-ssh-key" +description: |- + Provides a DigitalOcean SSH key resource. +--- + +# digitalocean\_ssh_key + +Provides a DigitalOcean SSH key resource to allow you manage SSH +keys for Droplet access. Keys created with this resource +can be referenced in your droplet configuration via their ID or +fingerprint. + +## Example Usage + +``` +# Create a new SSH key +resource "digitalocean_ssh_key" "default" { + name = "Terraform Example" + public_key = "${file("/Users/terraform/.ssh/id_rsa.pub")}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the SSH key for identification +* `public_key` - (Required) The public key. If this is a file, it +can be read using the file interpolation function + +## Attributes Reference + +The following attributes are exported: + +* `id` - The unique ID of the key +* `name` - The name of the SSH key +* `public_key` - The text of the public key +* `fingerprint` - The fingerprint of the SSH key diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 030192dfd..0bcff9cd8 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -95,6 +95,10 @@ > aws_vpc + + > + aws_vpc_peering + diff --git a/website/source/layouts/cloudstack.erb b/website/source/layouts/cloudstack.erb index a0e137aae..ee1ae6587 100644 --- a/website/source/layouts/cloudstack.erb +++ b/website/source/layouts/cloudstack.erb @@ -1,66 +1,78 @@ <% wrap_layout :inner do %> - <% content_for :sidebar do %> - + <% end %> + + <%= yield %> +<% end %> diff --git a/website/source/layouts/digitalocean.erb b/website/source/layouts/digitalocean.erb index cfffcd85f..c67d6395b 100644 --- a/website/source/layouts/digitalocean.erb +++ b/website/source/layouts/digitalocean.erb @@ -23,6 +23,10 @@ > digitalocean_record + + + > + digitalocean_ssh_key diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 2a313ba9a..fa8bce84a 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -79,14 +79,6 @@ plan - > - pull - - - > - push - - > refresh