github.com/andresvia/terraform@v0.6.15-0.20160412045437-d51c75946785/builtin/providers/fastly/resource_fastly_service_v1.go (about) 1 package fastly 2 3 import ( 4 "errors" 5 "fmt" 6 "log" 7 "time" 8 9 "github.com/hashicorp/terraform/helper/schema" 10 gofastly "github.com/sethvargo/go-fastly" 11 ) 12 13 var fastlyNoServiceFoundErr = errors.New("No matching Fastly Service found") 14 15 func resourceServiceV1() *schema.Resource { 16 return &schema.Resource{ 17 Create: resourceServiceV1Create, 18 Read: resourceServiceV1Read, 19 Update: resourceServiceV1Update, 20 Delete: resourceServiceV1Delete, 21 22 Schema: map[string]*schema.Schema{ 23 "name": &schema.Schema{ 24 Type: schema.TypeString, 25 Required: true, 26 Description: "Unique name for this Service", 27 }, 28 29 // Active Version represents the currently activated version in Fastly. In 30 // Terraform, we abstract this number away from the users and manage 31 // creating and activating. It's used internally, but also exported for 32 // users to see. 33 "active_version": &schema.Schema{ 34 Type: schema.TypeString, 35 Computed: true, 36 }, 37 38 "domain": &schema.Schema{ 39 Type: schema.TypeSet, 40 Required: true, 41 Elem: &schema.Resource{ 42 Schema: map[string]*schema.Schema{ 43 "name": &schema.Schema{ 44 Type: schema.TypeString, 45 Required: true, 46 Description: "The domain that this Service will respond to", 47 }, 48 49 "comment": &schema.Schema{ 50 Type: schema.TypeString, 51 Optional: true, 52 }, 53 }, 54 }, 55 }, 56 57 "default_ttl": &schema.Schema{ 58 Type: schema.TypeInt, 59 Optional: true, 60 Default: 3600, 61 Description: "The default Time-to-live (TTL) for the version", 62 }, 63 64 "default_host": &schema.Schema{ 65 Type: schema.TypeString, 66 Optional: true, 67 Computed: true, 68 Description: "The default hostname for the version", 69 }, 70 71 "backend": &schema.Schema{ 72 Type: schema.TypeSet, 73 Required: true, 74 Elem: &schema.Resource{ 75 Schema: map[string]*schema.Schema{ 76 // required fields 77 "name": &schema.Schema{ 78 Type: schema.TypeString, 79 Required: true, 80 Description: "A name for this Backend", 81 }, 82 "address": &schema.Schema{ 83 Type: schema.TypeString, 84 Required: true, 85 Description: "An IPv4, hostname, or IPv6 address for the Backend", 86 }, 87 // Optional fields, defaults where they exist 88 "auto_loadbalance": &schema.Schema{ 89 Type: schema.TypeBool, 90 Optional: true, 91 Default: true, 92 Description: "Should this Backend be load balanced", 93 }, 94 "between_bytes_timeout": &schema.Schema{ 95 Type: schema.TypeInt, 96 Optional: true, 97 Default: 10000, 98 Description: "How long to wait between bytes in milliseconds", 99 }, 100 "connect_timeout": &schema.Schema{ 101 Type: schema.TypeInt, 102 Optional: true, 103 Default: 1000, 104 Description: "How long to wait for a timeout in milliseconds", 105 }, 106 "error_threshold": &schema.Schema{ 107 Type: schema.TypeInt, 108 Optional: true, 109 Default: 0, 110 Description: "Number of errors to allow before the Backend is marked as down", 111 }, 112 "first_byte_timeout": &schema.Schema{ 113 Type: schema.TypeInt, 114 Optional: true, 115 Default: 15000, 116 Description: "How long to wait for the first bytes in milliseconds", 117 }, 118 "max_conn": &schema.Schema{ 119 Type: schema.TypeInt, 120 Optional: true, 121 Default: 200, 122 Description: "Maximum number of connections for this Backend", 123 }, 124 "port": &schema.Schema{ 125 Type: schema.TypeInt, 126 Optional: true, 127 Default: 80, 128 Description: "The port number Backend responds on. Default 80", 129 }, 130 "ssl_check_cert": &schema.Schema{ 131 Type: schema.TypeBool, 132 Optional: true, 133 Default: true, 134 Description: "Be strict on checking SSL certs", 135 }, 136 // UseSSL is something we want to support in the future, but 137 // requires SSL setup we don't yet have 138 // TODO: Provide all SSL fields from https://docs.fastly.com/api/config#backend 139 // "use_ssl": &schema.Schema{ 140 // Type: schema.TypeBool, 141 // Optional: true, 142 // Default: false, 143 // Description: "Whether or not to use SSL to reach the Backend", 144 // }, 145 "weight": &schema.Schema{ 146 Type: schema.TypeInt, 147 Optional: true, 148 Default: 100, 149 Description: "How long to wait for the first bytes in milliseconds", 150 }, 151 }, 152 }, 153 }, 154 155 "force_destroy": &schema.Schema{ 156 Type: schema.TypeBool, 157 Optional: true, 158 }, 159 }, 160 } 161 } 162 163 func resourceServiceV1Create(d *schema.ResourceData, meta interface{}) error { 164 conn := meta.(*FastlyClient).conn 165 service, err := conn.CreateService(&gofastly.CreateServiceInput{ 166 Name: d.Get("name").(string), 167 Comment: "Managed by Terraform", 168 }) 169 170 if err != nil { 171 return err 172 } 173 174 d.SetId(service.ID) 175 return resourceServiceV1Update(d, meta) 176 } 177 178 func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error { 179 conn := meta.(*FastlyClient).conn 180 181 // Update Name. No new verions is required for this 182 if d.HasChange("name") { 183 _, err := conn.UpdateService(&gofastly.UpdateServiceInput{ 184 ID: d.Id(), 185 Name: d.Get("name").(string), 186 }) 187 if err != nil { 188 return err 189 } 190 } 191 192 // Once activated, Versions are locked and become immutable. This is true for 193 // versions that are no longer active. For Domains, Backends, DefaultHost and 194 // DefaultTTL, a new Version must be created first, and updates posted to that 195 // Version. Loop these attributes and determine if we need to create a new version first 196 var needsChange bool 197 for _, v := range []string{"domain", "backend", "default_host", "default_ttl"} { 198 if d.HasChange(v) { 199 needsChange = true 200 } 201 } 202 203 if needsChange { 204 latestVersion := d.Get("active_version").(string) 205 if latestVersion == "" { 206 // If the service was just created, there is an empty Version 1 available 207 // that is unlocked and can be updated 208 latestVersion = "1" 209 } else { 210 // Clone the latest version, giving us an unlocked version we can modify 211 log.Printf("[DEBUG] Creating clone of version (%s) for updates", latestVersion) 212 newVersion, err := conn.CloneVersion(&gofastly.CloneVersionInput{ 213 Service: d.Id(), 214 Version: latestVersion, 215 }) 216 if err != nil { 217 return err 218 } 219 220 // The new version number is named "Number", but it's actually a string 221 latestVersion = newVersion.Number 222 223 // New versions are not immediately found in the API, or are not 224 // immediately mutable, so we need to sleep a few and let Fastly ready 225 // itself. Typically, 7 seconds is enough 226 log.Printf("[DEBUG] Sleeping 7 seconds to allow Fastly Version to be available") 227 time.Sleep(7 * time.Second) 228 } 229 230 // update general settings 231 if d.HasChange("default_host") || d.HasChange("default_ttl") { 232 opts := gofastly.UpdateSettingsInput{ 233 Service: d.Id(), 234 Version: latestVersion, 235 // default_ttl has the same default value of 3600 that is provided by 236 // the Fastly API, so it's safe to include here 237 DefaultTTL: uint(d.Get("default_ttl").(int)), 238 } 239 240 if attr, ok := d.GetOk("default_host"); ok { 241 opts.DefaultHost = attr.(string) 242 } 243 244 log.Printf("[DEBUG] Update Settings opts: %#v", opts) 245 _, err := conn.UpdateSettings(&opts) 246 if err != nil { 247 return err 248 } 249 } 250 251 // Find differences in domains 252 if d.HasChange("domain") { 253 // Note: we don't utilize the PUT endpoint to update a Domain, we simply 254 // destroy it and create a new one. This is how Terraform works with nested 255 // sub resources, we only get the full diff not a partial set item diff. 256 // Because this is done on a new version of the configuration, this is 257 // considered safe 258 od, nd := d.GetChange("domain") 259 if od == nil { 260 od = new(schema.Set) 261 } 262 if nd == nil { 263 nd = new(schema.Set) 264 } 265 266 ods := od.(*schema.Set) 267 nds := nd.(*schema.Set) 268 269 remove := ods.Difference(nds).List() 270 add := nds.Difference(ods).List() 271 272 // Delete removed domains 273 for _, dRaw := range remove { 274 df := dRaw.(map[string]interface{}) 275 opts := gofastly.DeleteDomainInput{ 276 Service: d.Id(), 277 Version: latestVersion, 278 Name: df["name"].(string), 279 } 280 281 log.Printf("[DEBUG] Fastly Domain Removal opts: %#v", opts) 282 err := conn.DeleteDomain(&opts) 283 if err != nil { 284 return err 285 } 286 } 287 288 // POST new Domains 289 for _, dRaw := range add { 290 df := dRaw.(map[string]interface{}) 291 opts := gofastly.CreateDomainInput{ 292 Service: d.Id(), 293 Version: latestVersion, 294 Name: df["name"].(string), 295 } 296 297 if v, ok := df["comment"]; ok { 298 opts.Comment = v.(string) 299 } 300 301 log.Printf("[DEBUG] Fastly Domain Addition opts: %#v", opts) 302 _, err := conn.CreateDomain(&opts) 303 if err != nil { 304 return err 305 } 306 } 307 } 308 309 // find difference in backends 310 if d.HasChange("backend") { 311 // POST new Backends 312 // Note: we don't utilize the PUT endpoint to update a Backend, we simply 313 // destroy it and create a new one. This is how Terraform works with nested 314 // sub resources, we only get the full diff not a partial set item diff. 315 // Because this is done on a new version of the configuration, this is 316 // considered safe 317 ob, nb := d.GetChange("backend") 318 if ob == nil { 319 ob = new(schema.Set) 320 } 321 if nb == nil { 322 nb = new(schema.Set) 323 } 324 325 obs := ob.(*schema.Set) 326 nbs := nb.(*schema.Set) 327 removeBackends := obs.Difference(nbs).List() 328 addBackends := nbs.Difference(obs).List() 329 330 // DELETE old Backends 331 for _, bRaw := range removeBackends { 332 bf := bRaw.(map[string]interface{}) 333 opts := gofastly.DeleteBackendInput{ 334 Service: d.Id(), 335 Version: latestVersion, 336 Name: bf["name"].(string), 337 } 338 339 log.Printf("[DEBUG] Fastly Backend Removal opts: %#v", opts) 340 err := conn.DeleteBackend(&opts) 341 if err != nil { 342 return err 343 } 344 } 345 346 for _, dRaw := range addBackends { 347 df := dRaw.(map[string]interface{}) 348 opts := gofastly.CreateBackendInput{ 349 Service: d.Id(), 350 Version: latestVersion, 351 Name: df["name"].(string), 352 Address: df["address"].(string), 353 AutoLoadbalance: df["auto_loadbalance"].(bool), 354 SSLCheckCert: df["ssl_check_cert"].(bool), 355 Port: uint(df["port"].(int)), 356 BetweenBytesTimeout: uint(df["between_bytes_timeout"].(int)), 357 ConnectTimeout: uint(df["connect_timeout"].(int)), 358 ErrorThreshold: uint(df["error_threshold"].(int)), 359 FirstByteTimeout: uint(df["first_byte_timeout"].(int)), 360 MaxConn: uint(df["max_conn"].(int)), 361 Weight: uint(df["weight"].(int)), 362 } 363 364 log.Printf("[DEBUG] Create Backend Opts: %#v", opts) 365 _, err := conn.CreateBackend(&opts) 366 if err != nil { 367 return err 368 } 369 } 370 } 371 372 // validate version 373 log.Printf("[DEBUG] Validating Fastly Service (%s), Version (%s)", d.Id(), latestVersion) 374 valid, msg, err := conn.ValidateVersion(&gofastly.ValidateVersionInput{ 375 Service: d.Id(), 376 Version: latestVersion, 377 }) 378 379 if err != nil { 380 return fmt.Errorf("[ERR] Error checking validation: %s", err) 381 } 382 383 if !valid { 384 return fmt.Errorf("[ERR] Invalid configuration for Fastly Service (%s): %s", d.Id(), msg) 385 } 386 387 log.Printf("[DEBUG] Activating Fastly Service (%s), Version (%s)", d.Id(), latestVersion) 388 _, err = conn.ActivateVersion(&gofastly.ActivateVersionInput{ 389 Service: d.Id(), 390 Version: latestVersion, 391 }) 392 if err != nil { 393 return fmt.Errorf("[ERR] Error activating version (%s): %s", latestVersion, err) 394 } 395 396 // Only if the version is valid and activated do we set the active_version. 397 // This prevents us from getting stuck in cloning an invalid version 398 d.Set("active_version", latestVersion) 399 } 400 401 return resourceServiceV1Read(d, meta) 402 } 403 404 func resourceServiceV1Read(d *schema.ResourceData, meta interface{}) error { 405 conn := meta.(*FastlyClient).conn 406 407 // Find the Service. Discard the service because we need the ServiceDetails, 408 // not just a Service record 409 _, err := findService(d.Id(), meta) 410 if err != nil { 411 switch err { 412 case fastlyNoServiceFoundErr: 413 log.Printf("[WARN] %s for ID (%s)", err, d.Id()) 414 d.SetId("") 415 return nil 416 default: 417 return err 418 } 419 } 420 421 s, err := conn.GetServiceDetails(&gofastly.GetServiceInput{ 422 ID: d.Id(), 423 }) 424 425 if err != nil { 426 return err 427 } 428 429 d.Set("name", s.Name) 430 d.Set("active_version", s.ActiveVersion.Number) 431 432 // If CreateService succeeds, but initial updates to the Service fail, we'll 433 // have an empty ActiveService version (no version is active, so we can't 434 // query for information on it) 435 if s.ActiveVersion.Number != "" { 436 settingsOpts := gofastly.GetSettingsInput{ 437 Service: d.Id(), 438 Version: s.ActiveVersion.Number, 439 } 440 if settings, err := conn.GetSettings(&settingsOpts); err == nil { 441 d.Set("default_host", settings.DefaultHost) 442 d.Set("default_ttl", settings.DefaultTTL) 443 } else { 444 return fmt.Errorf("[ERR] Error looking up Version settings for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 445 } 446 447 // TODO: update go-fastly to support an ActiveVersion struct, which contains 448 // domain and backend info in the response. Here we do 2 additional queries 449 // to find out that info 450 domainList, err := conn.ListDomains(&gofastly.ListDomainsInput{ 451 Service: d.Id(), 452 Version: s.ActiveVersion.Number, 453 }) 454 455 if err != nil { 456 return fmt.Errorf("[ERR] Error looking up Domains for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 457 } 458 459 // Refresh Domains 460 dl := flattenDomains(domainList) 461 462 if err := d.Set("domain", dl); err != nil { 463 log.Printf("[WARN] Error setting Domains for (%s): %s", d.Id(), err) 464 } 465 466 // Refresh Backends 467 backendList, err := conn.ListBackends(&gofastly.ListBackendsInput{ 468 Service: d.Id(), 469 Version: s.ActiveVersion.Number, 470 }) 471 472 if err != nil { 473 return fmt.Errorf("[ERR] Error looking up Backends for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 474 } 475 476 bl := flattenBackends(backendList) 477 478 if err := d.Set("backend", bl); err != nil { 479 log.Printf("[WARN] Error setting Backends for (%s): %s", d.Id(), err) 480 } 481 } else { 482 log.Printf("[DEBUG] Active Version for Service (%s) is empty, no state to refresh", d.Id()) 483 } 484 485 return nil 486 } 487 488 func resourceServiceV1Delete(d *schema.ResourceData, meta interface{}) error { 489 conn := meta.(*FastlyClient).conn 490 491 // Fastly will fail to delete any service with an Active Version. 492 // If `force_destroy` is given, we deactivate the active version and then send 493 // the DELETE call 494 if d.Get("force_destroy").(bool) { 495 s, err := conn.GetServiceDetails(&gofastly.GetServiceInput{ 496 ID: d.Id(), 497 }) 498 499 if err != nil { 500 return err 501 } 502 503 if s.ActiveVersion.Number != "" { 504 _, err := conn.DeactivateVersion(&gofastly.DeactivateVersionInput{ 505 Service: d.Id(), 506 Version: s.ActiveVersion.Number, 507 }) 508 if err != nil { 509 return err 510 } 511 } 512 } 513 514 err := conn.DeleteService(&gofastly.DeleteServiceInput{ 515 ID: d.Id(), 516 }) 517 518 if err != nil { 519 return err 520 } 521 522 _, err = findService(d.Id(), meta) 523 if err != nil { 524 switch err { 525 // we expect no records to be found here 526 case fastlyNoServiceFoundErr: 527 d.SetId("") 528 return nil 529 default: 530 return err 531 } 532 } 533 534 // findService above returned something and nil error, but shouldn't have 535 return fmt.Errorf("[WARN] Tried deleting Service (%s), but was still found", d.Id()) 536 537 } 538 539 func flattenDomains(list []*gofastly.Domain) []map[string]interface{} { 540 dl := make([]map[string]interface{}, 0, len(list)) 541 542 for _, d := range list { 543 dl = append(dl, map[string]interface{}{ 544 "name": d.Name, 545 "comment": d.Comment, 546 }) 547 } 548 549 return dl 550 } 551 552 func flattenBackends(backendList []*gofastly.Backend) []map[string]interface{} { 553 var bl []map[string]interface{} 554 for _, b := range backendList { 555 // Convert Backend to a map for saving to state. 556 nb := map[string]interface{}{ 557 "name": b.Name, 558 "address": b.Address, 559 "auto_loadbalance": b.AutoLoadbalance, 560 "between_bytes_timeout": int(b.BetweenBytesTimeout), 561 "connect_timeout": int(b.ConnectTimeout), 562 "error_threshold": int(b.ErrorThreshold), 563 "first_byte_timeout": int(b.FirstByteTimeout), 564 "max_conn": int(b.MaxConn), 565 "port": int(b.Port), 566 "ssl_check_cert": b.SSLCheckCert, 567 "weight": int(b.Weight), 568 } 569 570 bl = append(bl, nb) 571 } 572 return bl 573 } 574 575 // findService finds a Fastly Service via the ListServices endpoint, returning 576 // the Service if found. 577 // 578 // Fastly API does not include any "deleted_at" type parameter to indicate 579 // that a Service has been deleted. GET requests to a deleted Service will 580 // return 200 OK and have the full output of the Service for an unknown time 581 // (days, in my testing). In order to determine if a Service is deleted, we 582 // need to hit /service and loop the returned Services, searching for the one 583 // in question. This endpoint only returns active or "alive" services. If the 584 // Service is not included, then it's "gone" 585 // 586 // Returns a fastlyNoServiceFoundErr error if the Service is not found in the 587 // ListServices response. 588 func findService(id string, meta interface{}) (*gofastly.Service, error) { 589 conn := meta.(*FastlyClient).conn 590 591 l, err := conn.ListServices(&gofastly.ListServicesInput{}) 592 if err != nil { 593 return nil, fmt.Errorf("[WARN] Error listing servcies when deleting Fastly Service (%s): %s", id, err) 594 } 595 596 for _, s := range l { 597 if s.ID == id { 598 log.Printf("[DEBUG] Found Service (%s)", id) 599 return s, nil 600 } 601 } 602 603 return nil, fastlyNoServiceFoundErr 604 }