diff --git a/builtin/providers/aws/resource_aws_instance.go b/builtin/providers/aws/resource_aws_instance.go index ee62d305e..54584402f 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 { @@ -253,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 := "" @@ -261,38 +261,87 @@ 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 + subnet := false + var subnetID string + if v, ok := d.GetOk("subnet_id"); ok { + subnet = true + subnetID = v.(string) + } + + if subnet && 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 } } @@ -311,24 +360,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 { - runOpts.BlockDevices[i].IOPS = int64(v) + if v, ok := bd["iops"].(int); ok && v > 0 { + runOpts.BlockDeviceMappings[i].EBS.IOPS = aws.Integer(v) } } } @@ -341,21 +393,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, @@ -365,16 +417,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 { @@ -386,13 +440,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 } @@ -410,28 +466,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() { @@ -448,24 +509,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 } @@ -473,28 +536,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 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["iops"] = vol.IOPS + blockDevice["snapshot_id"] = *vol.SnapshotID + blockDevice["encrypted"] = *vol.Encrypted + if vol.IOPS != nil { + blockDevice["iops"] = *vol.IOPS + } nonRootBlockDevices = append(nonRootBlockDevices, blockDevice) } d.Set("block_device", nonRootBlockDevices) @@ -504,21 +564,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") @@ -528,10 +592,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) } @@ -563,9 +630,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 { @@ -581,7 +650,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 e25d23542..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. @@ -147,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 @@ -206,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{ @@ -218,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, "#", ""), ), }, @@ -228,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 @@ -265,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 @@ -295,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" { @@ -303,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.") @@ -314,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 } @@ -337,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 }