github.com/inge4pres/terraform@v0.7.5-0.20160930053151-bd083f84f376/builtin/providers/aws/resource_aws_eip.go (about)

     1  package aws
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"net"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/aws/aws-sdk-go/aws"
    11  	"github.com/aws/aws-sdk-go/aws/awserr"
    12  	"github.com/aws/aws-sdk-go/service/ec2"
    13  	"github.com/hashicorp/terraform/helper/resource"
    14  	"github.com/hashicorp/terraform/helper/schema"
    15  )
    16  
    17  func resourceAwsEip() *schema.Resource {
    18  	return &schema.Resource{
    19  		Create: resourceAwsEipCreate,
    20  		Read:   resourceAwsEipRead,
    21  		Update: resourceAwsEipUpdate,
    22  		Delete: resourceAwsEipDelete,
    23  		Importer: &schema.ResourceImporter{
    24  			State: schema.ImportStatePassthrough,
    25  		},
    26  
    27  		Schema: map[string]*schema.Schema{
    28  			"vpc": &schema.Schema{
    29  				Type:     schema.TypeBool,
    30  				Optional: true,
    31  				ForceNew: true,
    32  			},
    33  
    34  			"instance": &schema.Schema{
    35  				Type:     schema.TypeString,
    36  				Optional: true,
    37  				Computed: true,
    38  			},
    39  
    40  			"network_interface": &schema.Schema{
    41  				Type:     schema.TypeString,
    42  				Optional: true,
    43  				Computed: true,
    44  			},
    45  
    46  			"allocation_id": &schema.Schema{
    47  				Type:     schema.TypeString,
    48  				Computed: true,
    49  			},
    50  
    51  			"association_id": &schema.Schema{
    52  				Type:     schema.TypeString,
    53  				Computed: true,
    54  			},
    55  
    56  			"domain": &schema.Schema{
    57  				Type:     schema.TypeString,
    58  				Computed: true,
    59  			},
    60  
    61  			"public_ip": &schema.Schema{
    62  				Type:     schema.TypeString,
    63  				Computed: true,
    64  			},
    65  
    66  			"private_ip": &schema.Schema{
    67  				Type:     schema.TypeString,
    68  				Computed: true,
    69  			},
    70  
    71  			"associate_with_private_ip": &schema.Schema{
    72  				Type:     schema.TypeString,
    73  				Optional: true,
    74  			},
    75  		},
    76  	}
    77  }
    78  
    79  func resourceAwsEipCreate(d *schema.ResourceData, meta interface{}) error {
    80  	ec2conn := meta.(*AWSClient).ec2conn
    81  
    82  	// By default, we're not in a VPC
    83  	domainOpt := ""
    84  	if v := d.Get("vpc"); v != nil && v.(bool) {
    85  		domainOpt = "vpc"
    86  	}
    87  
    88  	allocOpts := &ec2.AllocateAddressInput{
    89  		Domain: aws.String(domainOpt),
    90  	}
    91  
    92  	log.Printf("[DEBUG] EIP create configuration: %#v", allocOpts)
    93  	allocResp, err := ec2conn.AllocateAddress(allocOpts)
    94  	if err != nil {
    95  		return fmt.Errorf("Error creating EIP: %s", err)
    96  	}
    97  
    98  	// The domain tells us if we're in a VPC or not
    99  	d.Set("domain", allocResp.Domain)
   100  
   101  	// Assign the eips (unique) allocation id for use later
   102  	// the EIP api has a conditional unique ID (really), so
   103  	// if we're in a VPC we need to save the ID as such, otherwise
   104  	// it defaults to using the public IP
   105  	log.Printf("[DEBUG] EIP Allocate: %#v", allocResp)
   106  	if d.Get("domain").(string) == "vpc" {
   107  		d.SetId(*allocResp.AllocationId)
   108  	} else {
   109  		d.SetId(*allocResp.PublicIp)
   110  	}
   111  
   112  	log.Printf("[INFO] EIP ID: %s (domain: %v)", d.Id(), *allocResp.Domain)
   113  	return resourceAwsEipUpdate(d, meta)
   114  }
   115  
   116  func resourceAwsEipRead(d *schema.ResourceData, meta interface{}) error {
   117  	ec2conn := meta.(*AWSClient).ec2conn
   118  
   119  	domain := resourceAwsEipDomain(d)
   120  	id := d.Id()
   121  
   122  	req := &ec2.DescribeAddressesInput{}
   123  
   124  	if domain == "vpc" {
   125  		req.AllocationIds = []*string{aws.String(id)}
   126  	} else {
   127  		req.PublicIps = []*string{aws.String(id)}
   128  	}
   129  
   130  	log.Printf(
   131  		"[DEBUG] EIP describe configuration: %s (domain: %s)",
   132  		req, domain)
   133  
   134  	describeAddresses, err := ec2conn.DescribeAddresses(req)
   135  	if err != nil {
   136  		if ec2err, ok := err.(awserr.Error); ok && (ec2err.Code() == "InvalidAllocationID.NotFound" || ec2err.Code() == "InvalidAddress.NotFound") {
   137  			d.SetId("")
   138  			return nil
   139  		}
   140  
   141  		return fmt.Errorf("Error retrieving EIP: %s", err)
   142  	}
   143  
   144  	// Verify AWS returned our EIP
   145  	if len(describeAddresses.Addresses) != 1 ||
   146  		domain == "vpc" && *describeAddresses.Addresses[0].AllocationId != id ||
   147  		*describeAddresses.Addresses[0].PublicIp != id {
   148  		if err != nil {
   149  			return fmt.Errorf("Unable to find EIP: %#v", describeAddresses.Addresses)
   150  		}
   151  	}
   152  
   153  	address := describeAddresses.Addresses[0]
   154  
   155  	d.Set("association_id", address.AssociationId)
   156  	if address.InstanceId != nil {
   157  		d.Set("instance", address.InstanceId)
   158  	} else {
   159  		d.Set("instance", "")
   160  	}
   161  	if address.NetworkInterfaceId != nil {
   162  		d.Set("network_interface", address.NetworkInterfaceId)
   163  	} else {
   164  		d.Set("network_interface", "")
   165  	}
   166  	d.Set("private_ip", address.PrivateIpAddress)
   167  	d.Set("public_ip", address.PublicIp)
   168  
   169  	// On import (domain never set, which it must've been if we created),
   170  	// set the 'vpc' attribute depending on if we're in a VPC.
   171  	if _, ok := d.GetOk("domain"); !ok {
   172  		d.Set("vpc", *address.Domain == "vpc")
   173  	}
   174  
   175  	d.Set("domain", address.Domain)
   176  
   177  	// Force ID to be an Allocation ID if we're on a VPC
   178  	// This allows users to import the EIP based on the IP if they are in a VPC
   179  	if *address.Domain == "vpc" && net.ParseIP(id) != nil {
   180  		log.Printf("[DEBUG] Re-assigning EIP ID (%s) to it's Allocation ID (%s)", d.Id(), *address.AllocationId)
   181  		d.SetId(*address.AllocationId)
   182  	}
   183  
   184  	return nil
   185  }
   186  
   187  func resourceAwsEipUpdate(d *schema.ResourceData, meta interface{}) error {
   188  	ec2conn := meta.(*AWSClient).ec2conn
   189  
   190  	domain := resourceAwsEipDomain(d)
   191  
   192  	// Associate to instance or interface if specified
   193  	v_instance, ok_instance := d.GetOk("instance")
   194  	v_interface, ok_interface := d.GetOk("network_interface")
   195  
   196  	if ok_instance || ok_interface {
   197  		instanceId := v_instance.(string)
   198  		networkInterfaceId := v_interface.(string)
   199  
   200  		assocOpts := &ec2.AssociateAddressInput{
   201  			InstanceId: aws.String(instanceId),
   202  			PublicIp:   aws.String(d.Id()),
   203  		}
   204  
   205  		// more unique ID conditionals
   206  		if domain == "vpc" {
   207  			var privateIpAddress *string
   208  			if v := d.Get("associate_with_private_ip").(string); v != "" {
   209  				privateIpAddress = aws.String(v)
   210  			}
   211  			assocOpts = &ec2.AssociateAddressInput{
   212  				NetworkInterfaceId: aws.String(networkInterfaceId),
   213  				InstanceId:         aws.String(instanceId),
   214  				AllocationId:       aws.String(d.Id()),
   215  				PrivateIpAddress:   privateIpAddress,
   216  			}
   217  		}
   218  
   219  		log.Printf("[DEBUG] EIP associate configuration: %s (domain: %s)", assocOpts, domain)
   220  
   221  		err := resource.Retry(1*time.Minute, func() *resource.RetryError {
   222  			_, err := ec2conn.AssociateAddress(assocOpts)
   223  			if err != nil {
   224  				if awsErr, ok := err.(awserr.Error); ok {
   225  					if awsErr.Code() == "InvalidAllocationID.NotFound" {
   226  						return resource.RetryableError(awsErr)
   227  					}
   228  				}
   229  				return resource.NonRetryableError(err)
   230  			}
   231  			return nil
   232  		})
   233  		if err != nil {
   234  			// Prevent saving instance if association failed
   235  			// e.g. missing internet gateway in VPC
   236  			d.Set("instance", "")
   237  			d.Set("network_interface", "")
   238  			return fmt.Errorf("Failure associating EIP: %s", err)
   239  		}
   240  	}
   241  
   242  	return resourceAwsEipRead(d, meta)
   243  }
   244  
   245  func resourceAwsEipDelete(d *schema.ResourceData, meta interface{}) error {
   246  	ec2conn := meta.(*AWSClient).ec2conn
   247  
   248  	if err := resourceAwsEipRead(d, meta); err != nil {
   249  		return err
   250  	}
   251  	if d.Id() == "" {
   252  		// This might happen from the read
   253  		return nil
   254  	}
   255  
   256  	// If we are attached to an instance or interface, detach first.
   257  	if d.Get("instance").(string) != "" || d.Get("association_id").(string) != "" {
   258  		log.Printf("[DEBUG] Disassociating EIP: %s", d.Id())
   259  		var err error
   260  		switch resourceAwsEipDomain(d) {
   261  		case "vpc":
   262  			_, err = ec2conn.DisassociateAddress(&ec2.DisassociateAddressInput{
   263  				AssociationId: aws.String(d.Get("association_id").(string)),
   264  			})
   265  		case "standard":
   266  			_, err = ec2conn.DisassociateAddress(&ec2.DisassociateAddressInput{
   267  				PublicIp: aws.String(d.Get("public_ip").(string)),
   268  			})
   269  		}
   270  
   271  		if err != nil {
   272  			// First check if the association ID is not found. If this
   273  			// is the case, then it was already disassociated somehow,
   274  			// and that is okay. The most commmon reason for this is that
   275  			// the instance or ENI it was attached it was destroyed.
   276  			if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidAssociationID.NotFound" {
   277  				err = nil
   278  			}
   279  		}
   280  
   281  		if err != nil {
   282  			return err
   283  		}
   284  	}
   285  
   286  	domain := resourceAwsEipDomain(d)
   287  	return resource.Retry(3*time.Minute, func() *resource.RetryError {
   288  		var err error
   289  		switch domain {
   290  		case "vpc":
   291  			log.Printf(
   292  				"[DEBUG] EIP release (destroy) address allocation: %v",
   293  				d.Id())
   294  			_, err = ec2conn.ReleaseAddress(&ec2.ReleaseAddressInput{
   295  				AllocationId: aws.String(d.Id()),
   296  			})
   297  		case "standard":
   298  			log.Printf("[DEBUG] EIP release (destroy) address: %v", d.Id())
   299  			_, err = ec2conn.ReleaseAddress(&ec2.ReleaseAddressInput{
   300  				PublicIp: aws.String(d.Id()),
   301  			})
   302  		}
   303  
   304  		if err == nil {
   305  			return nil
   306  		}
   307  		if _, ok := err.(awserr.Error); !ok {
   308  			return resource.NonRetryableError(err)
   309  		}
   310  
   311  		return resource.RetryableError(err)
   312  	})
   313  }
   314  
   315  func resourceAwsEipDomain(d *schema.ResourceData) string {
   316  	if v, ok := d.GetOk("domain"); ok {
   317  		return v.(string)
   318  	} else if strings.Contains(d.Id(), "eipalloc") {
   319  		// We have to do this for backwards compatibility since TF 0.1
   320  		// didn't have the "domain" computed attribute.
   321  		return "vpc"
   322  	}
   323  
   324  	return "standard"
   325  }