github.com/jdextraze/terraform@v0.6.17-0.20160511153921-e33847c8a8af/builtin/providers/fastly/resource_fastly_service_v1.go (about) 1 package fastly 2 3 import ( 4 "errors" 5 "fmt" 6 "log" 7 "strings" 8 "time" 9 10 "github.com/hashicorp/terraform/helper/schema" 11 gofastly "github.com/sethvargo/go-fastly" 12 ) 13 14 var fastlyNoServiceFoundErr = errors.New("No matching Fastly Service found") 15 16 func resourceServiceV1() *schema.Resource { 17 return &schema.Resource{ 18 Create: resourceServiceV1Create, 19 Read: resourceServiceV1Read, 20 Update: resourceServiceV1Update, 21 Delete: resourceServiceV1Delete, 22 23 Schema: map[string]*schema.Schema{ 24 "name": &schema.Schema{ 25 Type: schema.TypeString, 26 Required: true, 27 Description: "Unique name for this Service", 28 }, 29 30 // Active Version represents the currently activated version in Fastly. In 31 // Terraform, we abstract this number away from the users and manage 32 // creating and activating. It's used internally, but also exported for 33 // users to see. 34 "active_version": &schema.Schema{ 35 Type: schema.TypeString, 36 Computed: true, 37 }, 38 39 "domain": &schema.Schema{ 40 Type: schema.TypeSet, 41 Required: true, 42 Elem: &schema.Resource{ 43 Schema: map[string]*schema.Schema{ 44 "name": &schema.Schema{ 45 Type: schema.TypeString, 46 Required: true, 47 Description: "The domain that this Service will respond to", 48 }, 49 50 "comment": &schema.Schema{ 51 Type: schema.TypeString, 52 Optional: true, 53 }, 54 }, 55 }, 56 }, 57 58 "condition": &schema.Schema{ 59 Type: schema.TypeSet, 60 Optional: true, 61 Elem: &schema.Resource{ 62 Schema: map[string]*schema.Schema{ 63 "name": &schema.Schema{ 64 Type: schema.TypeString, 65 Required: true, 66 }, 67 "statement": &schema.Schema{ 68 Type: schema.TypeString, 69 Required: true, 70 Description: "The statement used to determine if the condition is met", 71 StateFunc: func(v interface{}) string { 72 value := v.(string) 73 // Trim newlines and spaces, to match Fastly API 74 return strings.TrimSpace(value) 75 }, 76 }, 77 "priority": &schema.Schema{ 78 Type: schema.TypeInt, 79 Required: true, 80 Description: "A number used to determine the order in which multiple conditions execute. Lower numbers execute first", 81 }, 82 "type": &schema.Schema{ 83 Type: schema.TypeString, 84 Required: true, 85 Description: "Type of the condition, either `REQUEST`, `RESPONSE`, or `CACHE`", 86 }, 87 }, 88 }, 89 }, 90 91 "default_ttl": &schema.Schema{ 92 Type: schema.TypeInt, 93 Optional: true, 94 Default: 3600, 95 Description: "The default Time-to-live (TTL) for the version", 96 }, 97 98 "default_host": &schema.Schema{ 99 Type: schema.TypeString, 100 Optional: true, 101 Computed: true, 102 Description: "The default hostname for the version", 103 }, 104 105 "backend": &schema.Schema{ 106 Type: schema.TypeSet, 107 Required: true, 108 Elem: &schema.Resource{ 109 Schema: map[string]*schema.Schema{ 110 // required fields 111 "name": &schema.Schema{ 112 Type: schema.TypeString, 113 Required: true, 114 Description: "A name for this Backend", 115 }, 116 "address": &schema.Schema{ 117 Type: schema.TypeString, 118 Required: true, 119 Description: "An IPv4, hostname, or IPv6 address for the Backend", 120 }, 121 // Optional fields, defaults where they exist 122 "auto_loadbalance": &schema.Schema{ 123 Type: schema.TypeBool, 124 Optional: true, 125 Default: true, 126 Description: "Should this Backend be load balanced", 127 }, 128 "between_bytes_timeout": &schema.Schema{ 129 Type: schema.TypeInt, 130 Optional: true, 131 Default: 10000, 132 Description: "How long to wait between bytes in milliseconds", 133 }, 134 "connect_timeout": &schema.Schema{ 135 Type: schema.TypeInt, 136 Optional: true, 137 Default: 1000, 138 Description: "How long to wait for a timeout in milliseconds", 139 }, 140 "error_threshold": &schema.Schema{ 141 Type: schema.TypeInt, 142 Optional: true, 143 Default: 0, 144 Description: "Number of errors to allow before the Backend is marked as down", 145 }, 146 "first_byte_timeout": &schema.Schema{ 147 Type: schema.TypeInt, 148 Optional: true, 149 Default: 15000, 150 Description: "How long to wait for the first bytes in milliseconds", 151 }, 152 "max_conn": &schema.Schema{ 153 Type: schema.TypeInt, 154 Optional: true, 155 Default: 200, 156 Description: "Maximum number of connections for this Backend", 157 }, 158 "port": &schema.Schema{ 159 Type: schema.TypeInt, 160 Optional: true, 161 Default: 80, 162 Description: "The port number Backend responds on. Default 80", 163 }, 164 "ssl_check_cert": &schema.Schema{ 165 Type: schema.TypeBool, 166 Optional: true, 167 Default: true, 168 Description: "Be strict on checking SSL certs", 169 }, 170 // UseSSL is something we want to support in the future, but 171 // requires SSL setup we don't yet have 172 // TODO: Provide all SSL fields from https://docs.fastly.com/api/config#backend 173 // "use_ssl": &schema.Schema{ 174 // Type: schema.TypeBool, 175 // Optional: true, 176 // Default: false, 177 // Description: "Whether or not to use SSL to reach the Backend", 178 // }, 179 "weight": &schema.Schema{ 180 Type: schema.TypeInt, 181 Optional: true, 182 Default: 100, 183 Description: "The portion of traffic to send to a specific origins. Each origin receives weight/total of the traffic.", 184 }, 185 }, 186 }, 187 }, 188 189 "force_destroy": &schema.Schema{ 190 Type: schema.TypeBool, 191 Optional: true, 192 }, 193 194 "gzip": &schema.Schema{ 195 Type: schema.TypeSet, 196 Optional: true, 197 Elem: &schema.Resource{ 198 Schema: map[string]*schema.Schema{ 199 // required fields 200 "name": &schema.Schema{ 201 Type: schema.TypeString, 202 Required: true, 203 Description: "A name to refer to this gzip condition", 204 }, 205 // optional fields 206 "content_types": &schema.Schema{ 207 Type: schema.TypeSet, 208 Optional: true, 209 Description: "Content types to apply automatic gzip to", 210 Elem: &schema.Schema{Type: schema.TypeString}, 211 }, 212 "extensions": &schema.Schema{ 213 Type: schema.TypeSet, 214 Optional: true, 215 Description: "File extensions to apply automatic gzip to. Do not include '.'", 216 Elem: &schema.Schema{Type: schema.TypeString}, 217 }, 218 // These fields represent Fastly options that Terraform does not 219 // currently support 220 "cache_condition": &schema.Schema{ 221 Type: schema.TypeString, 222 Computed: true, 223 Description: "Optional name of a CacheCondition to apply.", 224 }, 225 }, 226 }, 227 }, 228 229 "header": &schema.Schema{ 230 Type: schema.TypeSet, 231 Optional: true, 232 Elem: &schema.Resource{ 233 Schema: map[string]*schema.Schema{ 234 // required fields 235 "name": &schema.Schema{ 236 Type: schema.TypeString, 237 Required: true, 238 Description: "A name to refer to this Header object", 239 }, 240 "action": &schema.Schema{ 241 Type: schema.TypeString, 242 Required: true, 243 Description: "One of set, append, delete, regex, or regex_repeat", 244 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 245 var found bool 246 for _, t := range []string{"set", "append", "delete", "regex", "regex_repeat"} { 247 if v.(string) == t { 248 found = true 249 } 250 } 251 if !found { 252 es = append(es, fmt.Errorf( 253 "Fastly Header action is case sensitive and must be one of 'set', 'append', 'delete', 'regex', or 'regex_repeat'; found: %s", v.(string))) 254 } 255 return 256 }, 257 }, 258 "type": &schema.Schema{ 259 Type: schema.TypeString, 260 Required: true, 261 Description: "Type to manipulate: request, fetch, cache, response", 262 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 263 var found bool 264 for _, t := range []string{"request", "fetch", "cache", "response"} { 265 if v.(string) == t { 266 found = true 267 } 268 } 269 if !found { 270 es = append(es, fmt.Errorf( 271 "Fastly Header type is case sensitive and must be one of 'request', 'fetch', 'cache', or 'response'; found: %s", v.(string))) 272 } 273 return 274 }, 275 }, 276 "destination": &schema.Schema{ 277 Type: schema.TypeString, 278 Required: true, 279 Description: "Header this affects", 280 }, 281 // Optional fields, defaults where they exist 282 "ignore_if_set": &schema.Schema{ 283 Type: schema.TypeBool, 284 Optional: true, 285 Default: false, 286 Description: "Don't add the header if it is already. (Only applies to 'set' action.). Default `false`", 287 }, 288 "source": &schema.Schema{ 289 Type: schema.TypeString, 290 Optional: true, 291 Computed: true, 292 Description: "Variable to be used as a source for the header content (Does not apply to 'delete' action.)", 293 }, 294 "regex": &schema.Schema{ 295 Type: schema.TypeString, 296 Optional: true, 297 Computed: true, 298 Description: "Regular expression to use (Only applies to 'regex' and 'regex_repeat' actions.)", 299 }, 300 "substitution": &schema.Schema{ 301 Type: schema.TypeString, 302 Optional: true, 303 Computed: true, 304 Description: "Value to substitute in place of regular expression. (Only applies to 'regex' and 'regex_repeat'.)", 305 }, 306 "priority": &schema.Schema{ 307 Type: schema.TypeInt, 308 Optional: true, 309 Default: 100, 310 Description: "Lower priorities execute first. (Default: 100.)", 311 }, 312 // These fields represent Fastly options that Terraform does not 313 // currently support 314 "request_condition": &schema.Schema{ 315 Type: schema.TypeString, 316 Computed: true, 317 Description: "Optional name of a RequestCondition to apply.", 318 }, 319 "cache_condition": &schema.Schema{ 320 Type: schema.TypeString, 321 Computed: true, 322 Description: "Optional name of a CacheCondition to apply.", 323 }, 324 "response_condition": &schema.Schema{ 325 Type: schema.TypeString, 326 Computed: true, 327 Description: "Optional name of a ResponseCondition to apply.", 328 }, 329 }, 330 }, 331 }, 332 333 "s3logging": &schema.Schema{ 334 Type: schema.TypeSet, 335 Optional: true, 336 Elem: &schema.Resource{ 337 Schema: map[string]*schema.Schema{ 338 // Required fields 339 "name": &schema.Schema{ 340 Type: schema.TypeString, 341 Required: true, 342 Description: "Unique name to refer to this logging setup", 343 }, 344 "bucket_name": &schema.Schema{ 345 Type: schema.TypeString, 346 Required: true, 347 Description: "S3 Bucket name to store logs in", 348 }, 349 "s3_access_key": &schema.Schema{ 350 Type: schema.TypeString, 351 Optional: true, 352 DefaultFunc: schema.EnvDefaultFunc("FASTLY_S3_ACCESS_KEY", ""), 353 Description: "AWS Access Key", 354 }, 355 "s3_secret_key": &schema.Schema{ 356 Type: schema.TypeString, 357 Optional: true, 358 DefaultFunc: schema.EnvDefaultFunc("FASTLY_S3_SECRET_KEY", ""), 359 Description: "AWS Secret Key", 360 }, 361 // Optional fields 362 "path": &schema.Schema{ 363 Type: schema.TypeString, 364 Optional: true, 365 Description: "Path to store the files. Must end with a trailing slash", 366 }, 367 "domain": &schema.Schema{ 368 Type: schema.TypeString, 369 Optional: true, 370 Description: "Bucket endpoint", 371 }, 372 "gzip_level": &schema.Schema{ 373 Type: schema.TypeInt, 374 Optional: true, 375 Default: 0, 376 Description: "Gzip Compression level", 377 }, 378 "period": &schema.Schema{ 379 Type: schema.TypeInt, 380 Optional: true, 381 Default: 3600, 382 Description: "How frequently the logs should be transferred, in seconds (Default 3600)", 383 }, 384 "format": &schema.Schema{ 385 Type: schema.TypeString, 386 Optional: true, 387 Default: "%h %l %u %t %r %>s", 388 Description: "Apache-style string or VCL variables to use for log formatting", 389 }, 390 "timestamp_format": &schema.Schema{ 391 Type: schema.TypeString, 392 Optional: true, 393 Default: "%Y-%m-%dT%H:%M:%S.000", 394 Description: "specified timestamp formatting (default `%Y-%m-%dT%H:%M:%S.000`)", 395 }, 396 }, 397 }, 398 }, 399 }, 400 } 401 } 402 403 func resourceServiceV1Create(d *schema.ResourceData, meta interface{}) error { 404 conn := meta.(*FastlyClient).conn 405 service, err := conn.CreateService(&gofastly.CreateServiceInput{ 406 Name: d.Get("name").(string), 407 Comment: "Managed by Terraform", 408 }) 409 410 if err != nil { 411 return err 412 } 413 414 d.SetId(service.ID) 415 return resourceServiceV1Update(d, meta) 416 } 417 418 func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error { 419 conn := meta.(*FastlyClient).conn 420 421 // Update Name. No new verions is required for this 422 if d.HasChange("name") { 423 _, err := conn.UpdateService(&gofastly.UpdateServiceInput{ 424 ID: d.Id(), 425 Name: d.Get("name").(string), 426 }) 427 if err != nil { 428 return err 429 } 430 } 431 432 // Once activated, Versions are locked and become immutable. This is true for 433 // versions that are no longer active. For Domains, Backends, DefaultHost and 434 // DefaultTTL, a new Version must be created first, and updates posted to that 435 // Version. Loop these attributes and determine if we need to create a new version first 436 var needsChange bool 437 for _, v := range []string{ 438 "domain", 439 "backend", 440 "default_host", 441 "default_ttl", 442 "header", 443 "gzip", 444 "s3logging", 445 "condition", 446 } { 447 if d.HasChange(v) { 448 needsChange = true 449 } 450 } 451 452 if needsChange { 453 latestVersion := d.Get("active_version").(string) 454 if latestVersion == "" { 455 // If the service was just created, there is an empty Version 1 available 456 // that is unlocked and can be updated 457 latestVersion = "1" 458 } else { 459 // Clone the latest version, giving us an unlocked version we can modify 460 log.Printf("[DEBUG] Creating clone of version (%s) for updates", latestVersion) 461 newVersion, err := conn.CloneVersion(&gofastly.CloneVersionInput{ 462 Service: d.Id(), 463 Version: latestVersion, 464 }) 465 if err != nil { 466 return err 467 } 468 469 // The new version number is named "Number", but it's actually a string 470 latestVersion = newVersion.Number 471 472 // New versions are not immediately found in the API, or are not 473 // immediately mutable, so we need to sleep a few and let Fastly ready 474 // itself. Typically, 7 seconds is enough 475 log.Printf("[DEBUG] Sleeping 7 seconds to allow Fastly Version to be available") 476 time.Sleep(7 * time.Second) 477 } 478 479 // update general settings 480 if d.HasChange("default_host") || d.HasChange("default_ttl") { 481 opts := gofastly.UpdateSettingsInput{ 482 Service: d.Id(), 483 Version: latestVersion, 484 // default_ttl has the same default value of 3600 that is provided by 485 // the Fastly API, so it's safe to include here 486 DefaultTTL: uint(d.Get("default_ttl").(int)), 487 } 488 489 if attr, ok := d.GetOk("default_host"); ok { 490 opts.DefaultHost = attr.(string) 491 } 492 493 log.Printf("[DEBUG] Update Settings opts: %#v", opts) 494 _, err := conn.UpdateSettings(&opts) 495 if err != nil { 496 return err 497 } 498 } 499 500 // Conditions need to be updated first, as they can be referenced by other 501 // configuraiton objects (Backends, Request Headers, etc) 502 503 // Find difference in Conditions 504 if d.HasChange("condition") { 505 // Note: we don't utilize the PUT endpoint to update these objects, we simply 506 // destroy any that have changed, and create new ones with the updated 507 // values. This is how Terraform works with nested sub resources, we only 508 // get the full diff not a partial set item diff. Because this is done 509 // on a new version of the Fastly Service configuration, this is considered safe 510 511 oc, nc := d.GetChange("condition") 512 if oc == nil { 513 oc = new(schema.Set) 514 } 515 if nc == nil { 516 nc = new(schema.Set) 517 } 518 519 ocs := oc.(*schema.Set) 520 ncs := nc.(*schema.Set) 521 removeConditions := ocs.Difference(ncs).List() 522 addConditions := ncs.Difference(ocs).List() 523 524 // DELETE old Conditions 525 for _, cRaw := range removeConditions { 526 cf := cRaw.(map[string]interface{}) 527 opts := gofastly.DeleteConditionInput{ 528 Service: d.Id(), 529 Version: latestVersion, 530 Name: cf["name"].(string), 531 } 532 533 log.Printf("[DEBUG] Fastly Conditions Removal opts: %#v", opts) 534 err := conn.DeleteCondition(&opts) 535 if err != nil { 536 return err 537 } 538 } 539 540 // POST new Conditions 541 for _, cRaw := range addConditions { 542 cf := cRaw.(map[string]interface{}) 543 opts := gofastly.CreateConditionInput{ 544 Service: d.Id(), 545 Version: latestVersion, 546 Name: cf["name"].(string), 547 Type: cf["type"].(string), 548 // need to trim leading/tailing spaces, incase the config has HEREDOC 549 // formatting and contains a trailing new line 550 Statement: strings.TrimSpace(cf["statement"].(string)), 551 Priority: cf["priority"].(int), 552 } 553 554 log.Printf("[DEBUG] Create Conditions Opts: %#v", opts) 555 _, err := conn.CreateCondition(&opts) 556 if err != nil { 557 return err 558 } 559 } 560 } 561 562 // Find differences in domains 563 if d.HasChange("domain") { 564 od, nd := d.GetChange("domain") 565 if od == nil { 566 od = new(schema.Set) 567 } 568 if nd == nil { 569 nd = new(schema.Set) 570 } 571 572 ods := od.(*schema.Set) 573 nds := nd.(*schema.Set) 574 575 remove := ods.Difference(nds).List() 576 add := nds.Difference(ods).List() 577 578 // Delete removed domains 579 for _, dRaw := range remove { 580 df := dRaw.(map[string]interface{}) 581 opts := gofastly.DeleteDomainInput{ 582 Service: d.Id(), 583 Version: latestVersion, 584 Name: df["name"].(string), 585 } 586 587 log.Printf("[DEBUG] Fastly Domain Removal opts: %#v", opts) 588 err := conn.DeleteDomain(&opts) 589 if err != nil { 590 return err 591 } 592 } 593 594 // POST new Domains 595 for _, dRaw := range add { 596 df := dRaw.(map[string]interface{}) 597 opts := gofastly.CreateDomainInput{ 598 Service: d.Id(), 599 Version: latestVersion, 600 Name: df["name"].(string), 601 } 602 603 if v, ok := df["comment"]; ok { 604 opts.Comment = v.(string) 605 } 606 607 log.Printf("[DEBUG] Fastly Domain Addition opts: %#v", opts) 608 _, err := conn.CreateDomain(&opts) 609 if err != nil { 610 return err 611 } 612 } 613 } 614 615 // find difference in backends 616 if d.HasChange("backend") { 617 ob, nb := d.GetChange("backend") 618 if ob == nil { 619 ob = new(schema.Set) 620 } 621 if nb == nil { 622 nb = new(schema.Set) 623 } 624 625 obs := ob.(*schema.Set) 626 nbs := nb.(*schema.Set) 627 removeBackends := obs.Difference(nbs).List() 628 addBackends := nbs.Difference(obs).List() 629 630 // DELETE old Backends 631 for _, bRaw := range removeBackends { 632 bf := bRaw.(map[string]interface{}) 633 opts := gofastly.DeleteBackendInput{ 634 Service: d.Id(), 635 Version: latestVersion, 636 Name: bf["name"].(string), 637 } 638 639 log.Printf("[DEBUG] Fastly Backend Removal opts: %#v", opts) 640 err := conn.DeleteBackend(&opts) 641 if err != nil { 642 return err 643 } 644 } 645 646 // Find and post new Backends 647 for _, dRaw := range addBackends { 648 df := dRaw.(map[string]interface{}) 649 opts := gofastly.CreateBackendInput{ 650 Service: d.Id(), 651 Version: latestVersion, 652 Name: df["name"].(string), 653 Address: df["address"].(string), 654 AutoLoadbalance: df["auto_loadbalance"].(bool), 655 SSLCheckCert: df["ssl_check_cert"].(bool), 656 Port: uint(df["port"].(int)), 657 BetweenBytesTimeout: uint(df["between_bytes_timeout"].(int)), 658 ConnectTimeout: uint(df["connect_timeout"].(int)), 659 ErrorThreshold: uint(df["error_threshold"].(int)), 660 FirstByteTimeout: uint(df["first_byte_timeout"].(int)), 661 MaxConn: uint(df["max_conn"].(int)), 662 Weight: uint(df["weight"].(int)), 663 } 664 665 log.Printf("[DEBUG] Create Backend Opts: %#v", opts) 666 _, err := conn.CreateBackend(&opts) 667 if err != nil { 668 return err 669 } 670 } 671 } 672 673 if d.HasChange("header") { 674 oh, nh := d.GetChange("header") 675 if oh == nil { 676 oh = new(schema.Set) 677 } 678 if nh == nil { 679 nh = new(schema.Set) 680 } 681 682 ohs := oh.(*schema.Set) 683 nhs := nh.(*schema.Set) 684 685 remove := ohs.Difference(nhs).List() 686 add := nhs.Difference(ohs).List() 687 688 // Delete removed headers 689 for _, dRaw := range remove { 690 df := dRaw.(map[string]interface{}) 691 opts := gofastly.DeleteHeaderInput{ 692 Service: d.Id(), 693 Version: latestVersion, 694 Name: df["name"].(string), 695 } 696 697 log.Printf("[DEBUG] Fastly Header Removal opts: %#v", opts) 698 err := conn.DeleteHeader(&opts) 699 if err != nil { 700 return err 701 } 702 } 703 704 // POST new Headers 705 for _, dRaw := range add { 706 opts, err := buildHeader(dRaw.(map[string]interface{})) 707 if err != nil { 708 log.Printf("[DEBUG] Error building Header: %s", err) 709 return err 710 } 711 opts.Service = d.Id() 712 opts.Version = latestVersion 713 714 log.Printf("[DEBUG] Fastly Header Addition opts: %#v", opts) 715 _, err = conn.CreateHeader(opts) 716 if err != nil { 717 return err 718 } 719 } 720 } 721 722 // Find differences in Gzips 723 if d.HasChange("gzip") { 724 og, ng := d.GetChange("gzip") 725 if og == nil { 726 og = new(schema.Set) 727 } 728 if ng == nil { 729 ng = new(schema.Set) 730 } 731 732 ogs := og.(*schema.Set) 733 ngs := ng.(*schema.Set) 734 735 remove := ogs.Difference(ngs).List() 736 add := ngs.Difference(ogs).List() 737 738 // Delete removed gzip rules 739 for _, dRaw := range remove { 740 df := dRaw.(map[string]interface{}) 741 opts := gofastly.DeleteGzipInput{ 742 Service: d.Id(), 743 Version: latestVersion, 744 Name: df["name"].(string), 745 } 746 747 log.Printf("[DEBUG] Fastly Gzip Removal opts: %#v", opts) 748 err := conn.DeleteGzip(&opts) 749 if err != nil { 750 return err 751 } 752 } 753 754 // POST new Gzips 755 for _, dRaw := range add { 756 df := dRaw.(map[string]interface{}) 757 opts := gofastly.CreateGzipInput{ 758 Service: d.Id(), 759 Version: latestVersion, 760 Name: df["name"].(string), 761 } 762 763 if v, ok := df["content_types"]; ok { 764 if len(v.(*schema.Set).List()) > 0 { 765 var cl []string 766 for _, c := range v.(*schema.Set).List() { 767 cl = append(cl, c.(string)) 768 } 769 opts.ContentTypes = strings.Join(cl, " ") 770 } 771 } 772 773 if v, ok := df["extensions"]; ok { 774 if len(v.(*schema.Set).List()) > 0 { 775 var el []string 776 for _, e := range v.(*schema.Set).List() { 777 el = append(el, e.(string)) 778 } 779 opts.Extensions = strings.Join(el, " ") 780 } 781 } 782 783 log.Printf("[DEBUG] Fastly Gzip Addition opts: %#v", opts) 784 _, err := conn.CreateGzip(&opts) 785 if err != nil { 786 return err 787 } 788 } 789 } 790 791 // find difference in s3logging 792 if d.HasChange("s3logging") { 793 os, ns := d.GetChange("s3logging") 794 if os == nil { 795 os = new(schema.Set) 796 } 797 if ns == nil { 798 ns = new(schema.Set) 799 } 800 801 oss := os.(*schema.Set) 802 nss := ns.(*schema.Set) 803 removeS3Logging := oss.Difference(nss).List() 804 addS3Logging := nss.Difference(oss).List() 805 806 // DELETE old S3 Log configurations 807 for _, sRaw := range removeS3Logging { 808 sf := sRaw.(map[string]interface{}) 809 opts := gofastly.DeleteS3Input{ 810 Service: d.Id(), 811 Version: latestVersion, 812 Name: sf["name"].(string), 813 } 814 815 log.Printf("[DEBUG] Fastly S3 Logging Removal opts: %#v", opts) 816 err := conn.DeleteS3(&opts) 817 if err != nil { 818 return err 819 } 820 } 821 822 // POST new/updated S3 Logging 823 for _, sRaw := range addS3Logging { 824 sf := sRaw.(map[string]interface{}) 825 826 // Fastly API will not error if these are omitted, so we throw an error 827 // if any of these are empty 828 for _, sk := range []string{"s3_access_key", "s3_secret_key"} { 829 if sf[sk].(string) == "" { 830 return fmt.Errorf("[ERR] No %s found for S3 Log stream setup for Service (%s)", sk, d.Id()) 831 } 832 } 833 834 opts := gofastly.CreateS3Input{ 835 Service: d.Id(), 836 Version: latestVersion, 837 Name: sf["name"].(string), 838 BucketName: sf["bucket_name"].(string), 839 AccessKey: sf["s3_access_key"].(string), 840 SecretKey: sf["s3_secret_key"].(string), 841 Period: uint(sf["period"].(int)), 842 GzipLevel: uint(sf["gzip_level"].(int)), 843 Domain: sf["domain"].(string), 844 Path: sf["path"].(string), 845 Format: sf["format"].(string), 846 TimestampFormat: sf["timestamp_format"].(string), 847 } 848 849 log.Printf("[DEBUG] Create S3 Logging Opts: %#v", opts) 850 _, err := conn.CreateS3(&opts) 851 if err != nil { 852 return err 853 } 854 } 855 } 856 857 // validate version 858 log.Printf("[DEBUG] Validating Fastly Service (%s), Version (%s)", d.Id(), latestVersion) 859 valid, msg, err := conn.ValidateVersion(&gofastly.ValidateVersionInput{ 860 Service: d.Id(), 861 Version: latestVersion, 862 }) 863 864 if err != nil { 865 return fmt.Errorf("[ERR] Error checking validation: %s", err) 866 } 867 868 if !valid { 869 return fmt.Errorf("[ERR] Invalid configuration for Fastly Service (%s): %s", d.Id(), msg) 870 } 871 872 log.Printf("[DEBUG] Activating Fastly Service (%s), Version (%s)", d.Id(), latestVersion) 873 _, err = conn.ActivateVersion(&gofastly.ActivateVersionInput{ 874 Service: d.Id(), 875 Version: latestVersion, 876 }) 877 if err != nil { 878 return fmt.Errorf("[ERR] Error activating version (%s): %s", latestVersion, err) 879 } 880 881 // Only if the version is valid and activated do we set the active_version. 882 // This prevents us from getting stuck in cloning an invalid version 883 d.Set("active_version", latestVersion) 884 } 885 886 return resourceServiceV1Read(d, meta) 887 } 888 889 func resourceServiceV1Read(d *schema.ResourceData, meta interface{}) error { 890 conn := meta.(*FastlyClient).conn 891 892 // Find the Service. Discard the service because we need the ServiceDetails, 893 // not just a Service record 894 _, err := findService(d.Id(), meta) 895 if err != nil { 896 switch err { 897 case fastlyNoServiceFoundErr: 898 log.Printf("[WARN] %s for ID (%s)", err, d.Id()) 899 d.SetId("") 900 return nil 901 default: 902 return err 903 } 904 } 905 906 s, err := conn.GetServiceDetails(&gofastly.GetServiceInput{ 907 ID: d.Id(), 908 }) 909 910 if err != nil { 911 return err 912 } 913 914 d.Set("name", s.Name) 915 d.Set("active_version", s.ActiveVersion.Number) 916 917 // If CreateService succeeds, but initial updates to the Service fail, we'll 918 // have an empty ActiveService version (no version is active, so we can't 919 // query for information on it) 920 if s.ActiveVersion.Number != "" { 921 settingsOpts := gofastly.GetSettingsInput{ 922 Service: d.Id(), 923 Version: s.ActiveVersion.Number, 924 } 925 if settings, err := conn.GetSettings(&settingsOpts); err == nil { 926 d.Set("default_host", settings.DefaultHost) 927 d.Set("default_ttl", settings.DefaultTTL) 928 } else { 929 return fmt.Errorf("[ERR] Error looking up Version settings for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 930 } 931 932 // TODO: update go-fastly to support an ActiveVersion struct, which contains 933 // domain and backend info in the response. Here we do 2 additional queries 934 // to find out that info 935 log.Printf("[DEBUG] Refreshing Domains for (%s)", d.Id()) 936 domainList, err := conn.ListDomains(&gofastly.ListDomainsInput{ 937 Service: d.Id(), 938 Version: s.ActiveVersion.Number, 939 }) 940 941 if err != nil { 942 return fmt.Errorf("[ERR] Error looking up Domains for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 943 } 944 945 // Refresh Domains 946 dl := flattenDomains(domainList) 947 948 if err := d.Set("domain", dl); err != nil { 949 log.Printf("[WARN] Error setting Domains for (%s): %s", d.Id(), err) 950 } 951 952 // Refresh Backends 953 log.Printf("[DEBUG] Refreshing Backends for (%s)", d.Id()) 954 backendList, err := conn.ListBackends(&gofastly.ListBackendsInput{ 955 Service: d.Id(), 956 Version: s.ActiveVersion.Number, 957 }) 958 959 if err != nil { 960 return fmt.Errorf("[ERR] Error looking up Backends for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 961 } 962 963 bl := flattenBackends(backendList) 964 965 if err := d.Set("backend", bl); err != nil { 966 log.Printf("[WARN] Error setting Backends for (%s): %s", d.Id(), err) 967 } 968 969 // refresh headers 970 log.Printf("[DEBUG] Refreshing Headers for (%s)", d.Id()) 971 headerList, err := conn.ListHeaders(&gofastly.ListHeadersInput{ 972 Service: d.Id(), 973 Version: s.ActiveVersion.Number, 974 }) 975 976 if err != nil { 977 return fmt.Errorf("[ERR] Error looking up Headers for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 978 } 979 980 hl := flattenHeaders(headerList) 981 982 if err := d.Set("header", hl); err != nil { 983 log.Printf("[WARN] Error setting Headers for (%s): %s", d.Id(), err) 984 } 985 986 // refresh gzips 987 log.Printf("[DEBUG] Refreshing Gzips for (%s)", d.Id()) 988 gzipsList, err := conn.ListGzips(&gofastly.ListGzipsInput{ 989 Service: d.Id(), 990 Version: s.ActiveVersion.Number, 991 }) 992 993 if err != nil { 994 return fmt.Errorf("[ERR] Error looking up Gzips for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 995 } 996 997 gl := flattenGzips(gzipsList) 998 999 if err := d.Set("gzip", gl); err != nil { 1000 log.Printf("[WARN] Error setting Gzips for (%s): %s", d.Id(), err) 1001 } 1002 1003 // refresh S3 Logging 1004 log.Printf("[DEBUG] Refreshing S3 Logging for (%s)", d.Id()) 1005 s3List, err := conn.ListS3s(&gofastly.ListS3sInput{ 1006 Service: d.Id(), 1007 Version: s.ActiveVersion.Number, 1008 }) 1009 1010 if err != nil { 1011 return fmt.Errorf("[ERR] Error looking up S3 Logging for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 1012 } 1013 1014 sl := flattenS3s(s3List) 1015 1016 if err := d.Set("s3logging", sl); err != nil { 1017 log.Printf("[WARN] Error setting S3 Logging for (%s): %s", d.Id(), err) 1018 } 1019 1020 // refresh Conditions 1021 log.Printf("[DEBUG] Refreshing Conditions for (%s)", d.Id()) 1022 conditionList, err := conn.ListConditions(&gofastly.ListConditionsInput{ 1023 Service: d.Id(), 1024 Version: s.ActiveVersion.Number, 1025 }) 1026 1027 if err != nil { 1028 return fmt.Errorf("[ERR] Error looking up Conditions for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 1029 } 1030 1031 cl := flattenConditions(conditionList) 1032 1033 if err := d.Set("condition", cl); err != nil { 1034 log.Printf("[WARN] Error setting Conditions for (%s): %s", d.Id(), err) 1035 } 1036 1037 } else { 1038 log.Printf("[DEBUG] Active Version for Service (%s) is empty, no state to refresh", d.Id()) 1039 } 1040 1041 return nil 1042 } 1043 1044 func resourceServiceV1Delete(d *schema.ResourceData, meta interface{}) error { 1045 conn := meta.(*FastlyClient).conn 1046 1047 // Fastly will fail to delete any service with an Active Version. 1048 // If `force_destroy` is given, we deactivate the active version and then send 1049 // the DELETE call 1050 if d.Get("force_destroy").(bool) { 1051 s, err := conn.GetServiceDetails(&gofastly.GetServiceInput{ 1052 ID: d.Id(), 1053 }) 1054 1055 if err != nil { 1056 return err 1057 } 1058 1059 if s.ActiveVersion.Number != "" { 1060 _, err := conn.DeactivateVersion(&gofastly.DeactivateVersionInput{ 1061 Service: d.Id(), 1062 Version: s.ActiveVersion.Number, 1063 }) 1064 if err != nil { 1065 return err 1066 } 1067 } 1068 } 1069 1070 err := conn.DeleteService(&gofastly.DeleteServiceInput{ 1071 ID: d.Id(), 1072 }) 1073 1074 if err != nil { 1075 return err 1076 } 1077 1078 _, err = findService(d.Id(), meta) 1079 if err != nil { 1080 switch err { 1081 // we expect no records to be found here 1082 case fastlyNoServiceFoundErr: 1083 d.SetId("") 1084 return nil 1085 default: 1086 return err 1087 } 1088 } 1089 1090 // findService above returned something and nil error, but shouldn't have 1091 return fmt.Errorf("[WARN] Tried deleting Service (%s), but was still found", d.Id()) 1092 1093 } 1094 1095 func flattenDomains(list []*gofastly.Domain) []map[string]interface{} { 1096 dl := make([]map[string]interface{}, 0, len(list)) 1097 1098 for _, d := range list { 1099 dl = append(dl, map[string]interface{}{ 1100 "name": d.Name, 1101 "comment": d.Comment, 1102 }) 1103 } 1104 1105 return dl 1106 } 1107 1108 func flattenBackends(backendList []*gofastly.Backend) []map[string]interface{} { 1109 var bl []map[string]interface{} 1110 for _, b := range backendList { 1111 // Convert Backend to a map for saving to state. 1112 nb := map[string]interface{}{ 1113 "name": b.Name, 1114 "address": b.Address, 1115 "auto_loadbalance": b.AutoLoadbalance, 1116 "between_bytes_timeout": int(b.BetweenBytesTimeout), 1117 "connect_timeout": int(b.ConnectTimeout), 1118 "error_threshold": int(b.ErrorThreshold), 1119 "first_byte_timeout": int(b.FirstByteTimeout), 1120 "max_conn": int(b.MaxConn), 1121 "port": int(b.Port), 1122 "ssl_check_cert": b.SSLCheckCert, 1123 "weight": int(b.Weight), 1124 } 1125 1126 bl = append(bl, nb) 1127 } 1128 return bl 1129 } 1130 1131 // findService finds a Fastly Service via the ListServices endpoint, returning 1132 // the Service if found. 1133 // 1134 // Fastly API does not include any "deleted_at" type parameter to indicate 1135 // that a Service has been deleted. GET requests to a deleted Service will 1136 // return 200 OK and have the full output of the Service for an unknown time 1137 // (days, in my testing). In order to determine if a Service is deleted, we 1138 // need to hit /service and loop the returned Services, searching for the one 1139 // in question. This endpoint only returns active or "alive" services. If the 1140 // Service is not included, then it's "gone" 1141 // 1142 // Returns a fastlyNoServiceFoundErr error if the Service is not found in the 1143 // ListServices response. 1144 func findService(id string, meta interface{}) (*gofastly.Service, error) { 1145 conn := meta.(*FastlyClient).conn 1146 1147 l, err := conn.ListServices(&gofastly.ListServicesInput{}) 1148 if err != nil { 1149 return nil, fmt.Errorf("[WARN] Error listing services when deleting Fastly Service (%s): %s", id, err) 1150 } 1151 1152 for _, s := range l { 1153 if s.ID == id { 1154 log.Printf("[DEBUG] Found Service (%s)", id) 1155 return s, nil 1156 } 1157 } 1158 1159 return nil, fastlyNoServiceFoundErr 1160 } 1161 1162 func flattenHeaders(headerList []*gofastly.Header) []map[string]interface{} { 1163 var hl []map[string]interface{} 1164 for _, h := range headerList { 1165 // Convert Header to a map for saving to state. 1166 nh := map[string]interface{}{ 1167 "name": h.Name, 1168 "action": h.Action, 1169 "ignore_if_set": h.IgnoreIfSet, 1170 "type": h.Type, 1171 "destination": h.Destination, 1172 "source": h.Source, 1173 "regex": h.Regex, 1174 "substitution": h.Substitution, 1175 "priority": int(h.Priority), 1176 "request_condition": h.RequestCondition, 1177 "cache_condition": h.CacheCondition, 1178 "response_condition": h.ResponseCondition, 1179 } 1180 1181 for k, v := range nh { 1182 if v == "" { 1183 delete(nh, k) 1184 } 1185 } 1186 1187 hl = append(hl, nh) 1188 } 1189 return hl 1190 } 1191 1192 func buildHeader(headerMap interface{}) (*gofastly.CreateHeaderInput, error) { 1193 df := headerMap.(map[string]interface{}) 1194 opts := gofastly.CreateHeaderInput{ 1195 Name: df["name"].(string), 1196 IgnoreIfSet: df["ignore_if_set"].(bool), 1197 Destination: df["destination"].(string), 1198 Priority: uint(df["priority"].(int)), 1199 Source: df["source"].(string), 1200 Regex: df["regex"].(string), 1201 Substitution: df["substitution"].(string), 1202 RequestCondition: df["request_condition"].(string), 1203 CacheCondition: df["cache_condition"].(string), 1204 ResponseCondition: df["response_condition"].(string), 1205 } 1206 1207 act := strings.ToLower(df["action"].(string)) 1208 switch act { 1209 case "set": 1210 opts.Action = gofastly.HeaderActionSet 1211 case "append": 1212 opts.Action = gofastly.HeaderActionAppend 1213 case "delete": 1214 opts.Action = gofastly.HeaderActionDelete 1215 case "regex": 1216 opts.Action = gofastly.HeaderActionRegex 1217 case "regex_repeat": 1218 opts.Action = gofastly.HeaderActionRegexRepeat 1219 } 1220 1221 ty := strings.ToLower(df["type"].(string)) 1222 switch ty { 1223 case "request": 1224 opts.Type = gofastly.HeaderTypeRequest 1225 case "fetch": 1226 opts.Type = gofastly.HeaderTypeFetch 1227 case "cache": 1228 opts.Type = gofastly.HeaderTypeCache 1229 case "response": 1230 opts.Type = gofastly.HeaderTypeResponse 1231 } 1232 1233 return &opts, nil 1234 } 1235 1236 func flattenGzips(gzipsList []*gofastly.Gzip) []map[string]interface{} { 1237 var gl []map[string]interface{} 1238 for _, g := range gzipsList { 1239 // Convert Gzip to a map for saving to state. 1240 ng := map[string]interface{}{ 1241 "name": g.Name, 1242 "cache_condition": g.CacheCondition, 1243 } 1244 1245 if g.Extensions != "" { 1246 e := strings.Split(g.Extensions, " ") 1247 var et []interface{} 1248 for _, ev := range e { 1249 et = append(et, ev) 1250 } 1251 ng["extensions"] = schema.NewSet(schema.HashString, et) 1252 } 1253 1254 if g.ContentTypes != "" { 1255 c := strings.Split(g.ContentTypes, " ") 1256 var ct []interface{} 1257 for _, cv := range c { 1258 ct = append(ct, cv) 1259 } 1260 ng["content_types"] = schema.NewSet(schema.HashString, ct) 1261 } 1262 1263 // prune any empty values that come from the default string value in structs 1264 for k, v := range ng { 1265 if v == "" { 1266 delete(ng, k) 1267 } 1268 } 1269 1270 gl = append(gl, ng) 1271 } 1272 1273 return gl 1274 } 1275 1276 func flattenS3s(s3List []*gofastly.S3) []map[string]interface{} { 1277 var sl []map[string]interface{} 1278 for _, s := range s3List { 1279 // Convert S3s to a map for saving to state. 1280 ns := map[string]interface{}{ 1281 "name": s.Name, 1282 "bucket_name": s.BucketName, 1283 "s3_access_key": s.AccessKey, 1284 "s3_secret_key": s.SecretKey, 1285 "path": s.Path, 1286 "period": s.Period, 1287 "domain": s.Domain, 1288 "gzip_level": s.GzipLevel, 1289 "format": s.Format, 1290 "timestamp_format": s.TimestampFormat, 1291 } 1292 1293 // prune any empty values that come from the default string value in structs 1294 for k, v := range ns { 1295 if v == "" { 1296 delete(ns, k) 1297 } 1298 } 1299 1300 sl = append(sl, ns) 1301 } 1302 1303 return sl 1304 } 1305 1306 func flattenConditions(conditionList []*gofastly.Condition) []map[string]interface{} { 1307 var cl []map[string]interface{} 1308 for _, c := range conditionList { 1309 // Convert Conditions to a map for saving to state. 1310 nc := map[string]interface{}{ 1311 "name": c.Name, 1312 "statement": c.Statement, 1313 "type": c.Type, 1314 "priority": c.Priority, 1315 } 1316 1317 // prune any empty values that come from the default string value in structs 1318 for k, v := range nc { 1319 if v == "" { 1320 delete(nc, k) 1321 } 1322 } 1323 1324 cl = append(cl, nc) 1325 } 1326 1327 return cl 1328 }