From af29a61748d4989670b1f6ef6c087e7749b5fc74 Mon Sep 17 00:00:00 2001 From: Paul Stack Date: Mon, 9 May 2016 18:40:45 +0100 Subject: [PATCH] provider/aws: Change `aws_elastic_ip_association` to have computed parameters (#6552) * New top level AWS resource aws_eip_association * Add documentation for aws_eip_association * Add tests for aws_eip_association * provider/aws: Change `aws_elastic_ip_association` to have computed parameters The AWS API was send ing more parameters than we had set. Therefore, Terraform was showing constant changes when plans were being formed --- builtin/providers/aws/provider.go | 1 + .../aws/resource_aws_eip_association.go | 161 ++++++++++++++++++ .../aws/resource_aws_eip_association_test.go | 152 +++++++++++++++++ .../aws/r/eip_association.html.markdown | 67 ++++++++ website/source/layouts/aws.erb | 4 + 5 files changed, 385 insertions(+) create mode 100644 builtin/providers/aws/resource_aws_eip_association.go create mode 100644 builtin/providers/aws/resource_aws_eip_association_test.go create mode 100644 website/source/docs/providers/aws/r/eip_association.html.markdown diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 4448f6de7..df737555b 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -162,6 +162,7 @@ func Provider() terraform.ResourceProvider { "aws_efs_file_system": resourceAwsEfsFileSystem(), "aws_efs_mount_target": resourceAwsEfsMountTarget(), "aws_eip": resourceAwsEip(), + "aws_eip_association": resourceAwsEipAssociation(), "aws_elasticache_cluster": resourceAwsElasticacheCluster(), "aws_elasticache_parameter_group": resourceAwsElasticacheParameterGroup(), "aws_elasticache_security_group": resourceAwsElasticacheSecurityGroup(), diff --git a/builtin/providers/aws/resource_aws_eip_association.go b/builtin/providers/aws/resource_aws_eip_association.go new file mode 100644 index 000000000..ce31cf710 --- /dev/null +++ b/builtin/providers/aws/resource_aws_eip_association.go @@ -0,0 +1,161 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsEipAssociation() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsEipAssociationCreate, + Read: resourceAwsEipAssociationRead, + Delete: resourceAwsEipAssociationDelete, + + Schema: map[string]*schema.Schema{ + "allocation_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "allow_reassociation": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + + "instance_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "network_interface_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "private_ip_address": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "public_ip": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsEipAssociationCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + request := &ec2.AssociateAddressInput{} + + if v, ok := d.GetOk("allocation_id"); ok { + request.AllocationId = aws.String(v.(string)) + } + if v, ok := d.GetOk("allow_reassociation"); ok { + request.AllowReassociation = aws.Bool(v.(bool)) + } + if v, ok := d.GetOk("instance_id"); ok { + request.InstanceId = aws.String(v.(string)) + } + if v, ok := d.GetOk("network_interface_id"); ok { + request.NetworkInterfaceId = aws.String(v.(string)) + } + if v, ok := d.GetOk("private_ip_address"); ok { + request.PrivateIpAddress = aws.String(v.(string)) + } + if v, ok := d.GetOk("public_ip"); ok { + request.PublicIp = aws.String(v.(string)) + } + + log.Printf("[DEBUG] EIP association configuration: %#v", request) + + resp, err := conn.AssociateAddress(request) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + return fmt.Errorf("[WARN] Error attaching EIP, message: \"%s\", code: \"%s\"", + awsErr.Message(), awsErr.Code()) + } + return err + } + + d.SetId(*resp.AssociationId) + + return resourceAwsEipAssociationRead(d, meta) +} + +func resourceAwsEipAssociationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + request := &ec2.DescribeAddressesInput{ + Filters: []*ec2.Filter{ + &ec2.Filter{ + Name: aws.String("association-id"), + Values: []*string{aws.String(d.Id())}, + }, + }, + } + + response, err := conn.DescribeAddresses(request) + if err != nil { + return fmt.Errorf("Error reading EC2 Elastic IP %s: %#v", d.Get("allocation_id").(string), err) + } + + if response.Addresses == nil || len(response.Addresses) == 0 { + return fmt.Errorf("Unable to find EIP Association: %s", d.Id()) + } + + return readAwsEipAssociation(d, response.Addresses[0]) +} + +func resourceAwsEipAssociationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + opts := &ec2.DisassociateAddressInput{ + AssociationId: aws.String(d.Id()), + } + + _, err := conn.DisassociateAddress(opts) + if err != nil { + return fmt.Errorf("Error deleting Elastic IP association: %s", err) + } + + return nil +} + +func readAwsEipAssociation(d *schema.ResourceData, address *ec2.Address) error { + if err := d.Set("allocation_id", address.AllocationId); err != nil { + return err + } + if err := d.Set("instance_id", address.InstanceId); err != nil { + return err + } + if err := d.Set("network_interface_id", address.NetworkInterfaceId); err != nil { + return err + } + if err := d.Set("private_ip_address", address.PrivateIpAddress); err != nil { + return err + } + if err := d.Set("public_ip", address.PublicIp); err != nil { + return err + } + + return nil +} diff --git a/builtin/providers/aws/resource_aws_eip_association_test.go b/builtin/providers/aws/resource_aws_eip_association_test.go new file mode 100644 index 000000000..ef0e7f1fa --- /dev/null +++ b/builtin/providers/aws/resource_aws_eip_association_test.go @@ -0,0 +1,152 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSEIPAssociation_basic(t *testing.T) { + var a ec2.Address + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEIPAssociationDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSEIPAssociationConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEIPExists( + "aws_eip.bar.0", &a), + testAccCheckAWSEIPAssociationExists( + "aws_eip_association.by_allocation_id", &a), + testAccCheckAWSEIPExists( + "aws_eip.bar.1", &a), + testAccCheckAWSEIPAssociationExists( + "aws_eip_association.by_public_ip", &a), + testAccCheckAWSEIPExists( + "aws_eip.bar.2", &a), + testAccCheckAWSEIPAssociationExists( + "aws_eip_association.to_eni", &a), + ), + }, + }, + }) +} + +func testAccCheckAWSEIPAssociationExists(name string, res *ec2.Address) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No EIP Association ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + request := &ec2.DescribeAddressesInput{ + Filters: []*ec2.Filter{ + &ec2.Filter{ + Name: aws.String("association-id"), + Values: []*string{res.AssociationId}, + }, + }, + } + describe, err := conn.DescribeAddresses(request) + if err != nil { + return err + } + + if len(describe.Addresses) != 1 || + *describe.Addresses[0].AssociationId != *res.AssociationId { + return fmt.Errorf("EIP Association not found") + } + + return nil + } +} + +func testAccCheckAWSEIPAssociationDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_eip_association" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No EIP Association ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + request := &ec2.DescribeAddressesInput{ + Filters: []*ec2.Filter{ + &ec2.Filter{ + Name: aws.String("association-id"), + Values: []*string{aws.String(rs.Primary.ID)}, + }, + }, + } + describe, err := conn.DescribeAddresses(request) + if err != nil { + return err + } + + if len(describe.Addresses) > 0 { + return fmt.Errorf("EIP Association still exists") + } + } + return nil +} + +const testAccAWSEIPAssociationConfig = ` +resource "aws_vpc" "main" { + cidr_block = "192.168.0.0/24" +} +resource "aws_subnet" "sub" { + vpc_id = "${aws_vpc.main.id}" + cidr_block = "192.168.0.0/25" + availability_zone = "us-west-2a" +} +resource "aws_internet_gateway" "igw" { + vpc_id = "${aws_vpc.main.id}" +} +resource "aws_instance" "foo" { + count = 2 + ami = "ami-21f78e11" + availability_zone = "us-west-2a" + instance_type = "t1.micro" + subnet_id = "${aws_subnet.sub.id}" +} +resource "aws_eip" "bar" { + count = 3 + vpc = true +} +resource "aws_eip_association" "by_allocation_id" { + allocation_id = "${aws_eip.bar.0.id}" + instance_id = "${aws_instance.foo.0.id}" +} +resource "aws_eip_association" "by_public_ip" { + public_ip = "${aws_eip.bar.1.public_ip}" + instance_id = "${aws_instance.foo.1.id}" +} +resource "aws_eip_association" "to_eni" { + allocation_id = "${aws_eip.bar.2.id}" + network_interface_id = "${aws_network_interface.baz.id}" +} +resource "aws_network_interface" "baz" { + subnet_id = "${aws_subnet.sub.id}" + private_ips = ["192.168.0.10"] + attachment { + instance = "${aws_instance.foo.0.id}" + device_index = 1 + } +} +` diff --git a/website/source/docs/providers/aws/r/eip_association.html.markdown b/website/source/docs/providers/aws/r/eip_association.html.markdown new file mode 100644 index 000000000..308db8cee --- /dev/null +++ b/website/source/docs/providers/aws/r/eip_association.html.markdown @@ -0,0 +1,67 @@ +--- +layout: "aws" +page_title: "AWS: aws_eip_association" +sidebar_current: "docs-aws-resource-eip-association" +description: |- + Provides an AWS EIP Association +--- + +# aws\_eip\_association + +Provides an AWS EIP Association as a top level resource, to associate and +disassociate Elastic IPs from AWS Instances and Network Interfaces. + +~> **NOTE:** `aws_eip_association` is useful in scenarios where EIPs are either +pre-existing or distributed to customers or users and therefore cannot be changed. + +## Example Usage + +``` +resource "aws_eip_association" "eip_assoc" { + instance_id = "${aws_instance.web.id}" + allocation_id = "${aws_eip.example.allocation_id}" +} + +resource "aws_instance" "web" { + ami = "ami-21f78e11" + availability_zone = "us-west-2a" + instance_type = "t1.micro" + tags { + Name = "HelloWorld" + } +} + +resource "aws_eip" "example" { + vpc = true +} +``` + +## Argument Reference + +The following arguments are supported: + +* `allocation_id` - (Optional) The allocation ID. This is required for EC2-VPC. +* `allow_reassociation` - (Optional, Boolean) Whether to allow an Elastic IP to +be re-associated. Defaults to `true` in VPC. +* `instance_id` - (Optional) The ID of the instance. This is required for +EC2-Classic. For EC2-VPC, you can specify either the instance ID or the +network interface ID, but not both. The operation fails if you specify an +instance ID unless exactly one network interface is attached. +* `network_interface_id` - (Optional) The ID of the network interface. If the +instance has more than one network interface, you must specify a network +interface ID. +* `private_ip_address` - (Optional) The primary or secondary private IP address +to associate with the Elastic IP address. If no private IP address is +specified, the Elastic IP address is associated with the primary private IP +address. +* `public_ip` - (Optional) The Elastic IP address. This is required for EC2-Classic. + +## Attributes Reference + +* `association_id` - The ID that represents the association of the Elastic IP +address with an instance. +* `allocation_id` - As above +* `instance_id` - As above +* `network_interface_id` - As above +* `private_ip_address` - As above +* `public_ip` - As above diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 49b4f638f..b274eea98 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -202,6 +202,10 @@ aws_ebs_volume + > + aws_eip_association + + > aws_eip