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 }