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