github.com/pbthorste/terraform@v0.8.6-0.20170127005045-deb56bd93da2/builtin/providers/fastly/resource_fastly_service_v1.go (about) 1 package fastly 2 3 import ( 4 "crypto/sha1" 5 "encoding/hex" 6 "errors" 7 "fmt" 8 "log" 9 "strings" 10 "time" 11 12 "github.com/hashicorp/terraform/helper/schema" 13 gofastly "github.com/sethvargo/go-fastly" 14 ) 15 16 var fastlyNoServiceFoundErr = errors.New("No matching Fastly Service found") 17 18 func resourceServiceV1() *schema.Resource { 19 return &schema.Resource{ 20 Create: resourceServiceV1Create, 21 Read: resourceServiceV1Read, 22 Update: resourceServiceV1Update, 23 Delete: resourceServiceV1Delete, 24 Importer: &schema.ResourceImporter{ 25 State: schema.ImportStatePassthrough, 26 }, 27 28 Schema: map[string]*schema.Schema{ 29 "name": &schema.Schema{ 30 Type: schema.TypeString, 31 Required: true, 32 Description: "Unique name for this Service", 33 }, 34 35 // Active Version represents the currently activated version in Fastly. In 36 // Terraform, we abstract this number away from the users and manage 37 // creating and activating. It's used internally, but also exported for 38 // users to see. 39 "active_version": &schema.Schema{ 40 Type: schema.TypeString, 41 Computed: true, 42 }, 43 44 "domain": &schema.Schema{ 45 Type: schema.TypeSet, 46 Required: true, 47 Elem: &schema.Resource{ 48 Schema: map[string]*schema.Schema{ 49 "name": &schema.Schema{ 50 Type: schema.TypeString, 51 Required: true, 52 Description: "The domain that this Service will respond to", 53 }, 54 55 "comment": &schema.Schema{ 56 Type: schema.TypeString, 57 Optional: true, 58 }, 59 }, 60 }, 61 }, 62 63 "condition": &schema.Schema{ 64 Type: schema.TypeSet, 65 Optional: true, 66 Elem: &schema.Resource{ 67 Schema: map[string]*schema.Schema{ 68 "name": &schema.Schema{ 69 Type: schema.TypeString, 70 Required: true, 71 }, 72 "statement": &schema.Schema{ 73 Type: schema.TypeString, 74 Required: true, 75 Description: "The statement used to determine if the condition is met", 76 StateFunc: func(v interface{}) string { 77 value := v.(string) 78 // Trim newlines and spaces, to match Fastly API 79 return strings.TrimSpace(value) 80 }, 81 }, 82 "priority": &schema.Schema{ 83 Type: schema.TypeInt, 84 Required: true, 85 Description: "A number used to determine the order in which multiple conditions execute. Lower numbers execute first", 86 }, 87 "type": &schema.Schema{ 88 Type: schema.TypeString, 89 Required: true, 90 Description: "Type of the condition, either `REQUEST`, `RESPONSE`, or `CACHE`", 91 }, 92 }, 93 }, 94 }, 95 96 "default_ttl": &schema.Schema{ 97 Type: schema.TypeInt, 98 Optional: true, 99 Default: 3600, 100 Description: "The default Time-to-live (TTL) for the version", 101 }, 102 103 "default_host": &schema.Schema{ 104 Type: schema.TypeString, 105 Optional: true, 106 Computed: true, 107 Description: "The default hostname for the version", 108 }, 109 110 "backend": &schema.Schema{ 111 Type: schema.TypeSet, 112 Required: true, 113 Elem: &schema.Resource{ 114 Schema: map[string]*schema.Schema{ 115 // required fields 116 "name": &schema.Schema{ 117 Type: schema.TypeString, 118 Required: true, 119 Description: "A name for this Backend", 120 }, 121 "address": &schema.Schema{ 122 Type: schema.TypeString, 123 Required: true, 124 Description: "An IPv4, hostname, or IPv6 address for the Backend", 125 }, 126 // Optional fields, defaults where they exist 127 "auto_loadbalance": &schema.Schema{ 128 Type: schema.TypeBool, 129 Optional: true, 130 Default: true, 131 Description: "Should this Backend be load balanced", 132 }, 133 "between_bytes_timeout": &schema.Schema{ 134 Type: schema.TypeInt, 135 Optional: true, 136 Default: 10000, 137 Description: "How long to wait between bytes in milliseconds", 138 }, 139 "connect_timeout": &schema.Schema{ 140 Type: schema.TypeInt, 141 Optional: true, 142 Default: 1000, 143 Description: "How long to wait for a timeout in milliseconds", 144 }, 145 "error_threshold": &schema.Schema{ 146 Type: schema.TypeInt, 147 Optional: true, 148 Default: 0, 149 Description: "Number of errors to allow before the Backend is marked as down", 150 }, 151 "first_byte_timeout": &schema.Schema{ 152 Type: schema.TypeInt, 153 Optional: true, 154 Default: 15000, 155 Description: "How long to wait for the first bytes in milliseconds", 156 }, 157 "max_conn": &schema.Schema{ 158 Type: schema.TypeInt, 159 Optional: true, 160 Default: 200, 161 Description: "Maximum number of connections for this Backend", 162 }, 163 "port": &schema.Schema{ 164 Type: schema.TypeInt, 165 Optional: true, 166 Default: 80, 167 Description: "The port number Backend responds on. Default 80", 168 }, 169 "request_condition": &schema.Schema{ 170 Type: schema.TypeString, 171 Optional: true, 172 Default: "", 173 Description: "Condition, which if met, will select this backend during a request.", 174 }, 175 "shield": &schema.Schema{ 176 Type: schema.TypeString, 177 Optional: true, 178 Default: "", 179 Description: "The POP of the shield designated to reduce inbound load.", 180 }, 181 "ssl_check_cert": &schema.Schema{ 182 Type: schema.TypeBool, 183 Optional: true, 184 Default: true, 185 Description: "Be strict on checking SSL certs", 186 }, 187 "ssl_hostname": &schema.Schema{ 188 Type: schema.TypeString, 189 Optional: true, 190 Default: "", 191 Description: "SSL certificate hostname", 192 }, 193 // UseSSL is something we want to support in the future, but 194 // requires SSL setup we don't yet have 195 // TODO: Provide all SSL fields from https://docs.fastly.com/api/config#backend 196 // "use_ssl": &schema.Schema{ 197 // Type: schema.TypeBool, 198 // Optional: true, 199 // Default: false, 200 // Description: "Whether or not to use SSL to reach the Backend", 201 // }, 202 "weight": &schema.Schema{ 203 Type: schema.TypeInt, 204 Optional: true, 205 Default: 100, 206 Description: "The portion of traffic to send to a specific origins. Each origin receives weight/total of the traffic.", 207 }, 208 }, 209 }, 210 }, 211 212 "force_destroy": &schema.Schema{ 213 Type: schema.TypeBool, 214 Optional: true, 215 }, 216 217 "cache_setting": &schema.Schema{ 218 Type: schema.TypeSet, 219 Optional: true, 220 Elem: &schema.Resource{ 221 Schema: map[string]*schema.Schema{ 222 // required fields 223 "name": &schema.Schema{ 224 Type: schema.TypeString, 225 Required: true, 226 Description: "A name to refer to this Cache Setting", 227 }, 228 "cache_condition": &schema.Schema{ 229 Type: schema.TypeString, 230 Required: true, 231 Description: "Condition to check if this Cache Setting applies", 232 }, 233 "action": &schema.Schema{ 234 Type: schema.TypeString, 235 Optional: true, 236 Description: "Action to take", 237 }, 238 // optional 239 "stale_ttl": &schema.Schema{ 240 Type: schema.TypeInt, 241 Optional: true, 242 Description: "Max 'Time To Live' for stale (unreachable) objects.", 243 Default: 300, 244 }, 245 "ttl": &schema.Schema{ 246 Type: schema.TypeInt, 247 Optional: true, 248 Description: "The 'Time To Live' for the object", 249 }, 250 }, 251 }, 252 }, 253 254 "gzip": &schema.Schema{ 255 Type: schema.TypeSet, 256 Optional: true, 257 Elem: &schema.Resource{ 258 Schema: map[string]*schema.Schema{ 259 // required fields 260 "name": &schema.Schema{ 261 Type: schema.TypeString, 262 Required: true, 263 Description: "A name to refer to this gzip condition", 264 }, 265 // optional fields 266 "content_types": &schema.Schema{ 267 Type: schema.TypeSet, 268 Optional: true, 269 Description: "Content types to apply automatic gzip to", 270 Elem: &schema.Schema{Type: schema.TypeString}, 271 }, 272 "extensions": &schema.Schema{ 273 Type: schema.TypeSet, 274 Optional: true, 275 Description: "File extensions to apply automatic gzip to. Do not include '.'", 276 Elem: &schema.Schema{Type: schema.TypeString}, 277 }, 278 // These fields represent Fastly options that Terraform does not 279 // currently support 280 "cache_condition": &schema.Schema{ 281 Type: schema.TypeString, 282 Computed: true, 283 Description: "Optional name of a CacheCondition to apply.", 284 }, 285 }, 286 }, 287 }, 288 289 "header": &schema.Schema{ 290 Type: schema.TypeSet, 291 Optional: true, 292 Elem: &schema.Resource{ 293 Schema: map[string]*schema.Schema{ 294 // required fields 295 "name": &schema.Schema{ 296 Type: schema.TypeString, 297 Required: true, 298 Description: "A name to refer to this Header object", 299 }, 300 "action": &schema.Schema{ 301 Type: schema.TypeString, 302 Required: true, 303 Description: "One of set, append, delete, regex, or regex_repeat", 304 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 305 var found bool 306 for _, t := range []string{"set", "append", "delete", "regex", "regex_repeat"} { 307 if v.(string) == t { 308 found = true 309 } 310 } 311 if !found { 312 es = append(es, fmt.Errorf( 313 "Fastly Header action is case sensitive and must be one of 'set', 'append', 'delete', 'regex', or 'regex_repeat'; found: %s", v.(string))) 314 } 315 return 316 }, 317 }, 318 "type": &schema.Schema{ 319 Type: schema.TypeString, 320 Required: true, 321 Description: "Type to manipulate: request, fetch, cache, response", 322 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 323 var found bool 324 for _, t := range []string{"request", "fetch", "cache", "response"} { 325 if v.(string) == t { 326 found = true 327 } 328 } 329 if !found { 330 es = append(es, fmt.Errorf( 331 "Fastly Header type is case sensitive and must be one of 'request', 'fetch', 'cache', or 'response'; found: %s", v.(string))) 332 } 333 return 334 }, 335 }, 336 "destination": &schema.Schema{ 337 Type: schema.TypeString, 338 Required: true, 339 Description: "Header this affects", 340 }, 341 // Optional fields, defaults where they exist 342 "ignore_if_set": &schema.Schema{ 343 Type: schema.TypeBool, 344 Optional: true, 345 Default: false, 346 Description: "Don't add the header if it is already. (Only applies to 'set' action.). Default `false`", 347 }, 348 "source": &schema.Schema{ 349 Type: schema.TypeString, 350 Optional: true, 351 Computed: true, 352 Description: "Variable to be used as a source for the header content (Does not apply to 'delete' action.)", 353 }, 354 "regex": &schema.Schema{ 355 Type: schema.TypeString, 356 Optional: true, 357 Computed: true, 358 Description: "Regular expression to use (Only applies to 'regex' and 'regex_repeat' actions.)", 359 }, 360 "substitution": &schema.Schema{ 361 Type: schema.TypeString, 362 Optional: true, 363 Computed: true, 364 Description: "Value to substitute in place of regular expression. (Only applies to 'regex' and 'regex_repeat'.)", 365 }, 366 "priority": &schema.Schema{ 367 Type: schema.TypeInt, 368 Optional: true, 369 Default: 100, 370 Description: "Lower priorities execute first. (Default: 100.)", 371 }, 372 // These fields represent Fastly options that Terraform does not 373 // currently support 374 "request_condition": &schema.Schema{ 375 Type: schema.TypeString, 376 Computed: true, 377 Description: "Optional name of a RequestCondition to apply.", 378 }, 379 "cache_condition": &schema.Schema{ 380 Type: schema.TypeString, 381 Computed: true, 382 Description: "Optional name of a CacheCondition to apply.", 383 }, 384 "response_condition": &schema.Schema{ 385 Type: schema.TypeString, 386 Computed: true, 387 Description: "Optional name of a ResponseCondition to apply.", 388 }, 389 }, 390 }, 391 }, 392 393 "s3logging": &schema.Schema{ 394 Type: schema.TypeSet, 395 Optional: true, 396 Elem: &schema.Resource{ 397 Schema: map[string]*schema.Schema{ 398 // Required fields 399 "name": &schema.Schema{ 400 Type: schema.TypeString, 401 Required: true, 402 Description: "Unique name to refer to this logging setup", 403 }, 404 "bucket_name": &schema.Schema{ 405 Type: schema.TypeString, 406 Required: true, 407 Description: "S3 Bucket name to store logs in", 408 }, 409 "s3_access_key": &schema.Schema{ 410 Type: schema.TypeString, 411 Optional: true, 412 DefaultFunc: schema.EnvDefaultFunc("FASTLY_S3_ACCESS_KEY", ""), 413 Description: "AWS Access Key", 414 }, 415 "s3_secret_key": &schema.Schema{ 416 Type: schema.TypeString, 417 Optional: true, 418 DefaultFunc: schema.EnvDefaultFunc("FASTLY_S3_SECRET_KEY", ""), 419 Description: "AWS Secret Key", 420 }, 421 // Optional fields 422 "path": &schema.Schema{ 423 Type: schema.TypeString, 424 Optional: true, 425 Description: "Path to store the files. Must end with a trailing slash", 426 }, 427 "domain": &schema.Schema{ 428 Type: schema.TypeString, 429 Optional: true, 430 Description: "Bucket endpoint", 431 }, 432 "gzip_level": &schema.Schema{ 433 Type: schema.TypeInt, 434 Optional: true, 435 Default: 0, 436 Description: "Gzip Compression level", 437 }, 438 "period": &schema.Schema{ 439 Type: schema.TypeInt, 440 Optional: true, 441 Default: 3600, 442 Description: "How frequently the logs should be transferred, in seconds (Default 3600)", 443 }, 444 "format": &schema.Schema{ 445 Type: schema.TypeString, 446 Optional: true, 447 Default: "%h %l %u %t %r %>s", 448 Description: "Apache-style string or VCL variables to use for log formatting", 449 }, 450 "timestamp_format": &schema.Schema{ 451 Type: schema.TypeString, 452 Optional: true, 453 Default: "%Y-%m-%dT%H:%M:%S.000", 454 Description: "specified timestamp formatting (default `%Y-%m-%dT%H:%M:%S.000`)", 455 }, 456 }, 457 }, 458 }, 459 460 "request_setting": &schema.Schema{ 461 Type: schema.TypeSet, 462 Optional: true, 463 Elem: &schema.Resource{ 464 Schema: map[string]*schema.Schema{ 465 // Required fields 466 "name": &schema.Schema{ 467 Type: schema.TypeString, 468 Required: true, 469 Description: "Unique name to refer to this Request Setting", 470 }, 471 "request_condition": &schema.Schema{ 472 Type: schema.TypeString, 473 Required: true, 474 Description: "Name of a RequestCondition to apply.", 475 }, 476 // Optional fields 477 "max_stale_age": &schema.Schema{ 478 Type: schema.TypeInt, 479 Optional: true, 480 Default: 60, 481 Description: "How old an object is allowed to be, in seconds. Default `60`", 482 }, 483 "force_miss": &schema.Schema{ 484 Type: schema.TypeBool, 485 Optional: true, 486 Description: "Force a cache miss for the request", 487 }, 488 "force_ssl": &schema.Schema{ 489 Type: schema.TypeBool, 490 Optional: true, 491 Description: "Forces the request use SSL", 492 }, 493 "action": &schema.Schema{ 494 Type: schema.TypeString, 495 Optional: true, 496 Description: "Allows you to terminate request handling and immediately perform an action", 497 }, 498 "bypass_busy_wait": &schema.Schema{ 499 Type: schema.TypeBool, 500 Optional: true, 501 Description: "Disable collapsed forwarding", 502 }, 503 "hash_keys": &schema.Schema{ 504 Type: schema.TypeString, 505 Optional: true, 506 Description: "Comma separated list of varnish request object fields that should be in the hash key", 507 }, 508 "xff": &schema.Schema{ 509 Type: schema.TypeString, 510 Optional: true, 511 Default: "append", 512 Description: "X-Forwarded-For options", 513 }, 514 "timer_support": &schema.Schema{ 515 Type: schema.TypeBool, 516 Optional: true, 517 Description: "Injects the X-Timer info into the request", 518 }, 519 "geo_headers": &schema.Schema{ 520 Type: schema.TypeBool, 521 Optional: true, 522 Description: "Inject Fastly-Geo-Country, Fastly-Geo-City, and Fastly-Geo-Region", 523 }, 524 "default_host": &schema.Schema{ 525 Type: schema.TypeString, 526 Optional: true, 527 Description: "the host header", 528 }, 529 }, 530 }, 531 }, 532 "vcl": &schema.Schema{ 533 Type: schema.TypeSet, 534 Optional: true, 535 Elem: &schema.Resource{ 536 Schema: map[string]*schema.Schema{ 537 "name": &schema.Schema{ 538 Type: schema.TypeString, 539 Required: true, 540 Description: "A name to refer to this VCL configuration", 541 }, 542 "content": &schema.Schema{ 543 Type: schema.TypeString, 544 Required: true, 545 Description: "The contents of this VCL configuration", 546 StateFunc: func(v interface{}) string { 547 switch v.(type) { 548 case string: 549 hash := sha1.Sum([]byte(v.(string))) 550 return hex.EncodeToString(hash[:]) 551 default: 552 return "" 553 } 554 }, 555 }, 556 "main": &schema.Schema{ 557 Type: schema.TypeBool, 558 Optional: true, 559 Default: false, 560 Description: "Should this VCL configuration be the main configuration", 561 }, 562 }, 563 }, 564 }, 565 }, 566 } 567 } 568 569 func resourceServiceV1Create(d *schema.ResourceData, meta interface{}) error { 570 if err := validateVCLs(d); err != nil { 571 return err 572 } 573 574 conn := meta.(*FastlyClient).conn 575 service, err := conn.CreateService(&gofastly.CreateServiceInput{ 576 Name: d.Get("name").(string), 577 Comment: "Managed by Terraform", 578 }) 579 580 if err != nil { 581 return err 582 } 583 584 d.SetId(service.ID) 585 return resourceServiceV1Update(d, meta) 586 } 587 588 func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error { 589 if err := validateVCLs(d); err != nil { 590 return err 591 } 592 593 conn := meta.(*FastlyClient).conn 594 595 // Update Name. No new verions is required for this 596 if d.HasChange("name") { 597 _, err := conn.UpdateService(&gofastly.UpdateServiceInput{ 598 ID: d.Id(), 599 Name: d.Get("name").(string), 600 }) 601 if err != nil { 602 return err 603 } 604 } 605 606 // Once activated, Versions are locked and become immutable. This is true for 607 // versions that are no longer active. For Domains, Backends, DefaultHost and 608 // DefaultTTL, a new Version must be created first, and updates posted to that 609 // Version. Loop these attributes and determine if we need to create a new version first 610 var needsChange bool 611 for _, v := range []string{ 612 "domain", 613 "backend", 614 "default_host", 615 "default_ttl", 616 "header", 617 "gzip", 618 "s3logging", 619 "condition", 620 "request_setting", 621 "cache_setting", 622 "vcl", 623 } { 624 if d.HasChange(v) { 625 needsChange = true 626 } 627 } 628 629 if needsChange { 630 latestVersion := d.Get("active_version").(string) 631 if latestVersion == "" { 632 // If the service was just created, there is an empty Version 1 available 633 // that is unlocked and can be updated 634 latestVersion = "1" 635 } else { 636 // Clone the latest version, giving us an unlocked version we can modify 637 log.Printf("[DEBUG] Creating clone of version (%s) for updates", latestVersion) 638 newVersion, err := conn.CloneVersion(&gofastly.CloneVersionInput{ 639 Service: d.Id(), 640 Version: latestVersion, 641 }) 642 if err != nil { 643 return err 644 } 645 646 // The new version number is named "Number", but it's actually a string 647 latestVersion = newVersion.Number 648 649 // New versions are not immediately found in the API, or are not 650 // immediately mutable, so we need to sleep a few and let Fastly ready 651 // itself. Typically, 7 seconds is enough 652 log.Printf("[DEBUG] Sleeping 7 seconds to allow Fastly Version to be available") 653 time.Sleep(7 * time.Second) 654 } 655 656 // update general settings 657 if d.HasChange("default_host") || d.HasChange("default_ttl") { 658 opts := gofastly.UpdateSettingsInput{ 659 Service: d.Id(), 660 Version: latestVersion, 661 // default_ttl has the same default value of 3600 that is provided by 662 // the Fastly API, so it's safe to include here 663 DefaultTTL: uint(d.Get("default_ttl").(int)), 664 } 665 666 if attr, ok := d.GetOk("default_host"); ok { 667 opts.DefaultHost = attr.(string) 668 } 669 670 log.Printf("[DEBUG] Update Settings opts: %#v", opts) 671 _, err := conn.UpdateSettings(&opts) 672 if err != nil { 673 return err 674 } 675 } 676 677 // Conditions need to be updated first, as they can be referenced by other 678 // configuraiton objects (Backends, Request Headers, etc) 679 680 // Find difference in Conditions 681 if d.HasChange("condition") { 682 // Note: we don't utilize the PUT endpoint to update these objects, we simply 683 // destroy any that have changed, and create new ones with the updated 684 // values. This is how Terraform works with nested sub resources, we only 685 // get the full diff not a partial set item diff. Because this is done 686 // on a new version of the Fastly Service configuration, this is considered safe 687 688 oc, nc := d.GetChange("condition") 689 if oc == nil { 690 oc = new(schema.Set) 691 } 692 if nc == nil { 693 nc = new(schema.Set) 694 } 695 696 ocs := oc.(*schema.Set) 697 ncs := nc.(*schema.Set) 698 removeConditions := ocs.Difference(ncs).List() 699 addConditions := ncs.Difference(ocs).List() 700 701 // DELETE old Conditions 702 for _, cRaw := range removeConditions { 703 cf := cRaw.(map[string]interface{}) 704 opts := gofastly.DeleteConditionInput{ 705 Service: d.Id(), 706 Version: latestVersion, 707 Name: cf["name"].(string), 708 } 709 710 log.Printf("[DEBUG] Fastly Conditions Removal opts: %#v", opts) 711 err := conn.DeleteCondition(&opts) 712 if err != nil { 713 return err 714 } 715 } 716 717 // POST new Conditions 718 for _, cRaw := range addConditions { 719 cf := cRaw.(map[string]interface{}) 720 opts := gofastly.CreateConditionInput{ 721 Service: d.Id(), 722 Version: latestVersion, 723 Name: cf["name"].(string), 724 Type: cf["type"].(string), 725 // need to trim leading/tailing spaces, incase the config has HEREDOC 726 // formatting and contains a trailing new line 727 Statement: strings.TrimSpace(cf["statement"].(string)), 728 Priority: cf["priority"].(int), 729 } 730 731 log.Printf("[DEBUG] Create Conditions Opts: %#v", opts) 732 _, err := conn.CreateCondition(&opts) 733 if err != nil { 734 return err 735 } 736 } 737 } 738 739 // Find differences in domains 740 if d.HasChange("domain") { 741 od, nd := d.GetChange("domain") 742 if od == nil { 743 od = new(schema.Set) 744 } 745 if nd == nil { 746 nd = new(schema.Set) 747 } 748 749 ods := od.(*schema.Set) 750 nds := nd.(*schema.Set) 751 752 remove := ods.Difference(nds).List() 753 add := nds.Difference(ods).List() 754 755 // Delete removed domains 756 for _, dRaw := range remove { 757 df := dRaw.(map[string]interface{}) 758 opts := gofastly.DeleteDomainInput{ 759 Service: d.Id(), 760 Version: latestVersion, 761 Name: df["name"].(string), 762 } 763 764 log.Printf("[DEBUG] Fastly Domain removal opts: %#v", opts) 765 err := conn.DeleteDomain(&opts) 766 if err != nil { 767 return err 768 } 769 } 770 771 // POST new Domains 772 for _, dRaw := range add { 773 df := dRaw.(map[string]interface{}) 774 opts := gofastly.CreateDomainInput{ 775 Service: d.Id(), 776 Version: latestVersion, 777 Name: df["name"].(string), 778 } 779 780 if v, ok := df["comment"]; ok { 781 opts.Comment = v.(string) 782 } 783 784 log.Printf("[DEBUG] Fastly Domain Addition opts: %#v", opts) 785 _, err := conn.CreateDomain(&opts) 786 if err != nil { 787 return err 788 } 789 } 790 } 791 792 // find difference in backends 793 if d.HasChange("backend") { 794 ob, nb := d.GetChange("backend") 795 if ob == nil { 796 ob = new(schema.Set) 797 } 798 if nb == nil { 799 nb = new(schema.Set) 800 } 801 802 obs := ob.(*schema.Set) 803 nbs := nb.(*schema.Set) 804 removeBackends := obs.Difference(nbs).List() 805 addBackends := nbs.Difference(obs).List() 806 807 // DELETE old Backends 808 for _, bRaw := range removeBackends { 809 bf := bRaw.(map[string]interface{}) 810 opts := gofastly.DeleteBackendInput{ 811 Service: d.Id(), 812 Version: latestVersion, 813 Name: bf["name"].(string), 814 } 815 816 log.Printf("[DEBUG] Fastly Backend removal opts: %#v", opts) 817 err := conn.DeleteBackend(&opts) 818 if err != nil { 819 return err 820 } 821 } 822 823 // Find and post new Backends 824 for _, dRaw := range addBackends { 825 df := dRaw.(map[string]interface{}) 826 opts := gofastly.CreateBackendInput{ 827 Service: d.Id(), 828 Version: latestVersion, 829 Name: df["name"].(string), 830 Address: df["address"].(string), 831 AutoLoadbalance: gofastly.CBool(df["auto_loadbalance"].(bool)), 832 SSLCheckCert: gofastly.CBool(df["ssl_check_cert"].(bool)), 833 SSLHostname: df["ssl_hostname"].(string), 834 Shield: df["shield"].(string), 835 Port: uint(df["port"].(int)), 836 BetweenBytesTimeout: uint(df["between_bytes_timeout"].(int)), 837 ConnectTimeout: uint(df["connect_timeout"].(int)), 838 ErrorThreshold: uint(df["error_threshold"].(int)), 839 FirstByteTimeout: uint(df["first_byte_timeout"].(int)), 840 MaxConn: uint(df["max_conn"].(int)), 841 Weight: uint(df["weight"].(int)), 842 RequestCondition: df["request_condition"].(string), 843 } 844 845 log.Printf("[DEBUG] Create Backend Opts: %#v", opts) 846 _, err := conn.CreateBackend(&opts) 847 if err != nil { 848 return err 849 } 850 } 851 } 852 853 if d.HasChange("header") { 854 oh, nh := d.GetChange("header") 855 if oh == nil { 856 oh = new(schema.Set) 857 } 858 if nh == nil { 859 nh = new(schema.Set) 860 } 861 862 ohs := oh.(*schema.Set) 863 nhs := nh.(*schema.Set) 864 865 remove := ohs.Difference(nhs).List() 866 add := nhs.Difference(ohs).List() 867 868 // Delete removed headers 869 for _, dRaw := range remove { 870 df := dRaw.(map[string]interface{}) 871 opts := gofastly.DeleteHeaderInput{ 872 Service: d.Id(), 873 Version: latestVersion, 874 Name: df["name"].(string), 875 } 876 877 log.Printf("[DEBUG] Fastly Header removal opts: %#v", opts) 878 err := conn.DeleteHeader(&opts) 879 if err != nil { 880 return err 881 } 882 } 883 884 // POST new Headers 885 for _, dRaw := range add { 886 opts, err := buildHeader(dRaw.(map[string]interface{})) 887 if err != nil { 888 log.Printf("[DEBUG] Error building Header: %s", err) 889 return err 890 } 891 opts.Service = d.Id() 892 opts.Version = latestVersion 893 894 log.Printf("[DEBUG] Fastly Header Addition opts: %#v", opts) 895 _, err = conn.CreateHeader(opts) 896 if err != nil { 897 return err 898 } 899 } 900 } 901 902 // Find differences in Gzips 903 if d.HasChange("gzip") { 904 og, ng := d.GetChange("gzip") 905 if og == nil { 906 og = new(schema.Set) 907 } 908 if ng == nil { 909 ng = new(schema.Set) 910 } 911 912 ogs := og.(*schema.Set) 913 ngs := ng.(*schema.Set) 914 915 remove := ogs.Difference(ngs).List() 916 add := ngs.Difference(ogs).List() 917 918 // Delete removed gzip rules 919 for _, dRaw := range remove { 920 df := dRaw.(map[string]interface{}) 921 opts := gofastly.DeleteGzipInput{ 922 Service: d.Id(), 923 Version: latestVersion, 924 Name: df["name"].(string), 925 } 926 927 log.Printf("[DEBUG] Fastly Gzip removal opts: %#v", opts) 928 err := conn.DeleteGzip(&opts) 929 if err != nil { 930 return err 931 } 932 } 933 934 // POST new Gzips 935 for _, dRaw := range add { 936 df := dRaw.(map[string]interface{}) 937 opts := gofastly.CreateGzipInput{ 938 Service: d.Id(), 939 Version: latestVersion, 940 Name: df["name"].(string), 941 } 942 943 if v, ok := df["content_types"]; ok { 944 if len(v.(*schema.Set).List()) > 0 { 945 var cl []string 946 for _, c := range v.(*schema.Set).List() { 947 cl = append(cl, c.(string)) 948 } 949 opts.ContentTypes = strings.Join(cl, " ") 950 } 951 } 952 953 if v, ok := df["extensions"]; ok { 954 if len(v.(*schema.Set).List()) > 0 { 955 var el []string 956 for _, e := range v.(*schema.Set).List() { 957 el = append(el, e.(string)) 958 } 959 opts.Extensions = strings.Join(el, " ") 960 } 961 } 962 963 log.Printf("[DEBUG] Fastly Gzip Addition opts: %#v", opts) 964 _, err := conn.CreateGzip(&opts) 965 if err != nil { 966 return err 967 } 968 } 969 } 970 971 // find difference in s3logging 972 if d.HasChange("s3logging") { 973 os, ns := d.GetChange("s3logging") 974 if os == nil { 975 os = new(schema.Set) 976 } 977 if ns == nil { 978 ns = new(schema.Set) 979 } 980 981 oss := os.(*schema.Set) 982 nss := ns.(*schema.Set) 983 removeS3Logging := oss.Difference(nss).List() 984 addS3Logging := nss.Difference(oss).List() 985 986 // DELETE old S3 Log configurations 987 for _, sRaw := range removeS3Logging { 988 sf := sRaw.(map[string]interface{}) 989 opts := gofastly.DeleteS3Input{ 990 Service: d.Id(), 991 Version: latestVersion, 992 Name: sf["name"].(string), 993 } 994 995 log.Printf("[DEBUG] Fastly S3 Logging removal opts: %#v", opts) 996 err := conn.DeleteS3(&opts) 997 if err != nil { 998 return err 999 } 1000 } 1001 1002 // POST new/updated S3 Logging 1003 for _, sRaw := range addS3Logging { 1004 sf := sRaw.(map[string]interface{}) 1005 1006 // Fastly API will not error if these are omitted, so we throw an error 1007 // if any of these are empty 1008 for _, sk := range []string{"s3_access_key", "s3_secret_key"} { 1009 if sf[sk].(string) == "" { 1010 return fmt.Errorf("[ERR] No %s found for S3 Log stream setup for Service (%s)", sk, d.Id()) 1011 } 1012 } 1013 1014 opts := gofastly.CreateS3Input{ 1015 Service: d.Id(), 1016 Version: latestVersion, 1017 Name: sf["name"].(string), 1018 BucketName: sf["bucket_name"].(string), 1019 AccessKey: sf["s3_access_key"].(string), 1020 SecretKey: sf["s3_secret_key"].(string), 1021 Period: uint(sf["period"].(int)), 1022 GzipLevel: uint(sf["gzip_level"].(int)), 1023 Domain: sf["domain"].(string), 1024 Path: sf["path"].(string), 1025 Format: sf["format"].(string), 1026 TimestampFormat: sf["timestamp_format"].(string), 1027 } 1028 1029 log.Printf("[DEBUG] Create S3 Logging Opts: %#v", opts) 1030 _, err := conn.CreateS3(&opts) 1031 if err != nil { 1032 return err 1033 } 1034 } 1035 } 1036 1037 // find difference in request settings 1038 if d.HasChange("request_setting") { 1039 os, ns := d.GetChange("request_setting") 1040 if os == nil { 1041 os = new(schema.Set) 1042 } 1043 if ns == nil { 1044 ns = new(schema.Set) 1045 } 1046 1047 ors := os.(*schema.Set) 1048 nrs := ns.(*schema.Set) 1049 removeRequestSettings := ors.Difference(nrs).List() 1050 addRequestSettings := nrs.Difference(ors).List() 1051 1052 // DELETE old Request Settings configurations 1053 for _, sRaw := range removeRequestSettings { 1054 sf := sRaw.(map[string]interface{}) 1055 opts := gofastly.DeleteRequestSettingInput{ 1056 Service: d.Id(), 1057 Version: latestVersion, 1058 Name: sf["name"].(string), 1059 } 1060 1061 log.Printf("[DEBUG] Fastly Request Setting removal opts: %#v", opts) 1062 err := conn.DeleteRequestSetting(&opts) 1063 if err != nil { 1064 return err 1065 } 1066 } 1067 1068 // POST new/updated Request Setting 1069 for _, sRaw := range addRequestSettings { 1070 opts, err := buildRequestSetting(sRaw.(map[string]interface{})) 1071 if err != nil { 1072 log.Printf("[DEBUG] Error building Requset Setting: %s", err) 1073 return err 1074 } 1075 opts.Service = d.Id() 1076 opts.Version = latestVersion 1077 1078 log.Printf("[DEBUG] Create Request Setting Opts: %#v", opts) 1079 _, err = conn.CreateRequestSetting(opts) 1080 if err != nil { 1081 return err 1082 } 1083 } 1084 } 1085 1086 // Find differences in VCLs 1087 if d.HasChange("vcl") { 1088 // Note: as above with Gzip and S3 logging, we don't utilize the PUT 1089 // endpoint to update a VCL, we simply destroy it and create a new one. 1090 oldVCLVal, newVCLVal := d.GetChange("vcl") 1091 if oldVCLVal == nil { 1092 oldVCLVal = new(schema.Set) 1093 } 1094 if newVCLVal == nil { 1095 newVCLVal = new(schema.Set) 1096 } 1097 1098 oldVCLSet := oldVCLVal.(*schema.Set) 1099 newVCLSet := newVCLVal.(*schema.Set) 1100 1101 remove := oldVCLSet.Difference(newVCLSet).List() 1102 add := newVCLSet.Difference(oldVCLSet).List() 1103 1104 // Delete removed VCL configurations 1105 for _, dRaw := range remove { 1106 df := dRaw.(map[string]interface{}) 1107 opts := gofastly.DeleteVCLInput{ 1108 Service: d.Id(), 1109 Version: latestVersion, 1110 Name: df["name"].(string), 1111 } 1112 1113 log.Printf("[DEBUG] Fastly VCL Removal opts: %#v", opts) 1114 err := conn.DeleteVCL(&opts) 1115 if err != nil { 1116 return err 1117 } 1118 } 1119 // POST new VCL configurations 1120 for _, dRaw := range add { 1121 df := dRaw.(map[string]interface{}) 1122 opts := gofastly.CreateVCLInput{ 1123 Service: d.Id(), 1124 Version: latestVersion, 1125 Name: df["name"].(string), 1126 Content: df["content"].(string), 1127 } 1128 1129 log.Printf("[DEBUG] Fastly VCL Addition opts: %#v", opts) 1130 _, err := conn.CreateVCL(&opts) 1131 if err != nil { 1132 return err 1133 } 1134 1135 // if this new VCL is the main 1136 if df["main"].(bool) { 1137 opts := gofastly.ActivateVCLInput{ 1138 Service: d.Id(), 1139 Version: latestVersion, 1140 Name: df["name"].(string), 1141 } 1142 log.Printf("[DEBUG] Fastly VCL activation opts: %#v", opts) 1143 _, err := conn.ActivateVCL(&opts) 1144 if err != nil { 1145 return err 1146 } 1147 1148 } 1149 } 1150 } 1151 1152 // Find differences in Cache Settings 1153 if d.HasChange("cache_setting") { 1154 oc, nc := d.GetChange("cache_setting") 1155 if oc == nil { 1156 oc = new(schema.Set) 1157 } 1158 if nc == nil { 1159 nc = new(schema.Set) 1160 } 1161 1162 ocs := oc.(*schema.Set) 1163 ncs := nc.(*schema.Set) 1164 1165 remove := ocs.Difference(ncs).List() 1166 add := ncs.Difference(ocs).List() 1167 1168 // Delete removed Cache Settings 1169 for _, dRaw := range remove { 1170 df := dRaw.(map[string]interface{}) 1171 opts := gofastly.DeleteCacheSettingInput{ 1172 Service: d.Id(), 1173 Version: latestVersion, 1174 Name: df["name"].(string), 1175 } 1176 1177 log.Printf("[DEBUG] Fastly Cache Settings removal opts: %#v", opts) 1178 err := conn.DeleteCacheSetting(&opts) 1179 if err != nil { 1180 return err 1181 } 1182 } 1183 1184 // POST new Cache Settings 1185 for _, dRaw := range add { 1186 opts, err := buildCacheSetting(dRaw.(map[string]interface{})) 1187 if err != nil { 1188 log.Printf("[DEBUG] Error building Cache Setting: %s", err) 1189 return err 1190 } 1191 opts.Service = d.Id() 1192 opts.Version = latestVersion 1193 1194 log.Printf("[DEBUG] Fastly Cache Settings Addition opts: %#v", opts) 1195 _, err = conn.CreateCacheSetting(opts) 1196 if err != nil { 1197 return err 1198 } 1199 } 1200 } 1201 1202 // validate version 1203 log.Printf("[DEBUG] Validating Fastly Service (%s), Version (%s)", d.Id(), latestVersion) 1204 valid, msg, err := conn.ValidateVersion(&gofastly.ValidateVersionInput{ 1205 Service: d.Id(), 1206 Version: latestVersion, 1207 }) 1208 1209 if err != nil { 1210 return fmt.Errorf("[ERR] Error checking validation: %s", err) 1211 } 1212 1213 if !valid { 1214 return fmt.Errorf("[ERR] Invalid configuration for Fastly Service (%s): %s", d.Id(), msg) 1215 } 1216 1217 log.Printf("[DEBUG] Activating Fastly Service (%s), Version (%s)", d.Id(), latestVersion) 1218 _, err = conn.ActivateVersion(&gofastly.ActivateVersionInput{ 1219 Service: d.Id(), 1220 Version: latestVersion, 1221 }) 1222 if err != nil { 1223 return fmt.Errorf("[ERR] Error activating version (%s): %s", latestVersion, err) 1224 } 1225 1226 // Only if the version is valid and activated do we set the active_version. 1227 // This prevents us from getting stuck in cloning an invalid version 1228 d.Set("active_version", latestVersion) 1229 } 1230 1231 return resourceServiceV1Read(d, meta) 1232 } 1233 1234 func resourceServiceV1Read(d *schema.ResourceData, meta interface{}) error { 1235 conn := meta.(*FastlyClient).conn 1236 1237 // Find the Service. Discard the service because we need the ServiceDetails, 1238 // not just a Service record 1239 _, err := findService(d.Id(), meta) 1240 if err != nil { 1241 switch err { 1242 case fastlyNoServiceFoundErr: 1243 log.Printf("[WARN] %s for ID (%s)", err, d.Id()) 1244 d.SetId("") 1245 return nil 1246 default: 1247 return err 1248 } 1249 } 1250 1251 s, err := conn.GetServiceDetails(&gofastly.GetServiceInput{ 1252 ID: d.Id(), 1253 }) 1254 1255 if err != nil { 1256 return err 1257 } 1258 1259 d.Set("name", s.Name) 1260 d.Set("active_version", s.ActiveVersion.Number) 1261 1262 // If CreateService succeeds, but initial updates to the Service fail, we'll 1263 // have an empty ActiveService version (no version is active, so we can't 1264 // query for information on it) 1265 if s.ActiveVersion.Number != "" { 1266 settingsOpts := gofastly.GetSettingsInput{ 1267 Service: d.Id(), 1268 Version: s.ActiveVersion.Number, 1269 } 1270 if settings, err := conn.GetSettings(&settingsOpts); err == nil { 1271 d.Set("default_host", settings.DefaultHost) 1272 d.Set("default_ttl", settings.DefaultTTL) 1273 } else { 1274 return fmt.Errorf("[ERR] Error looking up Version settings for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 1275 } 1276 1277 // TODO: update go-fastly to support an ActiveVersion struct, which contains 1278 // domain and backend info in the response. Here we do 2 additional queries 1279 // to find out that info 1280 log.Printf("[DEBUG] Refreshing Domains for (%s)", d.Id()) 1281 domainList, err := conn.ListDomains(&gofastly.ListDomainsInput{ 1282 Service: d.Id(), 1283 Version: s.ActiveVersion.Number, 1284 }) 1285 1286 if err != nil { 1287 return fmt.Errorf("[ERR] Error looking up Domains for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 1288 } 1289 1290 // Refresh Domains 1291 dl := flattenDomains(domainList) 1292 1293 if err := d.Set("domain", dl); err != nil { 1294 log.Printf("[WARN] Error setting Domains for (%s): %s", d.Id(), err) 1295 } 1296 1297 // Refresh Backends 1298 log.Printf("[DEBUG] Refreshing Backends for (%s)", d.Id()) 1299 backendList, err := conn.ListBackends(&gofastly.ListBackendsInput{ 1300 Service: d.Id(), 1301 Version: s.ActiveVersion.Number, 1302 }) 1303 1304 if err != nil { 1305 return fmt.Errorf("[ERR] Error looking up Backends for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 1306 } 1307 1308 bl := flattenBackends(backendList) 1309 1310 if err := d.Set("backend", bl); err != nil { 1311 log.Printf("[WARN] Error setting Backends for (%s): %s", d.Id(), err) 1312 } 1313 1314 // refresh headers 1315 log.Printf("[DEBUG] Refreshing Headers for (%s)", d.Id()) 1316 headerList, err := conn.ListHeaders(&gofastly.ListHeadersInput{ 1317 Service: d.Id(), 1318 Version: s.ActiveVersion.Number, 1319 }) 1320 1321 if err != nil { 1322 return fmt.Errorf("[ERR] Error looking up Headers for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 1323 } 1324 1325 hl := flattenHeaders(headerList) 1326 1327 if err := d.Set("header", hl); err != nil { 1328 log.Printf("[WARN] Error setting Headers for (%s): %s", d.Id(), err) 1329 } 1330 1331 // refresh gzips 1332 log.Printf("[DEBUG] Refreshing Gzips for (%s)", d.Id()) 1333 gzipsList, err := conn.ListGzips(&gofastly.ListGzipsInput{ 1334 Service: d.Id(), 1335 Version: s.ActiveVersion.Number, 1336 }) 1337 1338 if err != nil { 1339 return fmt.Errorf("[ERR] Error looking up Gzips for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 1340 } 1341 1342 gl := flattenGzips(gzipsList) 1343 1344 if err := d.Set("gzip", gl); err != nil { 1345 log.Printf("[WARN] Error setting Gzips for (%s): %s", d.Id(), err) 1346 } 1347 1348 // refresh S3 Logging 1349 log.Printf("[DEBUG] Refreshing S3 Logging for (%s)", d.Id()) 1350 s3List, err := conn.ListS3s(&gofastly.ListS3sInput{ 1351 Service: d.Id(), 1352 Version: s.ActiveVersion.Number, 1353 }) 1354 1355 if err != nil { 1356 return fmt.Errorf("[ERR] Error looking up S3 Logging for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 1357 } 1358 1359 sl := flattenS3s(s3List) 1360 1361 if err := d.Set("s3logging", sl); err != nil { 1362 log.Printf("[WARN] Error setting S3 Logging for (%s): %s", d.Id(), err) 1363 } 1364 1365 // refresh Conditions 1366 log.Printf("[DEBUG] Refreshing Conditions for (%s)", d.Id()) 1367 conditionList, err := conn.ListConditions(&gofastly.ListConditionsInput{ 1368 Service: d.Id(), 1369 Version: s.ActiveVersion.Number, 1370 }) 1371 1372 if err != nil { 1373 return fmt.Errorf("[ERR] Error looking up Conditions for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 1374 } 1375 1376 cl := flattenConditions(conditionList) 1377 1378 if err := d.Set("condition", cl); err != nil { 1379 log.Printf("[WARN] Error setting Conditions for (%s): %s", d.Id(), err) 1380 } 1381 1382 // refresh Request Settings 1383 log.Printf("[DEBUG] Refreshing Request Settings for (%s)", d.Id()) 1384 rsList, err := conn.ListRequestSettings(&gofastly.ListRequestSettingsInput{ 1385 Service: d.Id(), 1386 Version: s.ActiveVersion.Number, 1387 }) 1388 1389 if err != nil { 1390 return fmt.Errorf("[ERR] Error looking up Request Settings for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 1391 } 1392 1393 rl := flattenRequestSettings(rsList) 1394 1395 if err := d.Set("request_setting", rl); err != nil { 1396 log.Printf("[WARN] Error setting Request Settings for (%s): %s", d.Id(), err) 1397 } 1398 1399 // refresh VCLs 1400 log.Printf("[DEBUG] Refreshing VCLs for (%s)", d.Id()) 1401 vclList, err := conn.ListVCLs(&gofastly.ListVCLsInput{ 1402 Service: d.Id(), 1403 Version: s.ActiveVersion.Number, 1404 }) 1405 if err != nil { 1406 return fmt.Errorf("[ERR] Error looking up VCLs for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 1407 } 1408 1409 vl := flattenVCLs(vclList) 1410 1411 if err := d.Set("vcl", vl); err != nil { 1412 log.Printf("[WARN] Error setting VCLs for (%s): %s", d.Id(), err) 1413 } 1414 1415 // refresh Cache Settings 1416 log.Printf("[DEBUG] Refreshing Cache Settings for (%s)", d.Id()) 1417 cslList, err := conn.ListCacheSettings(&gofastly.ListCacheSettingsInput{ 1418 Service: d.Id(), 1419 Version: s.ActiveVersion.Number, 1420 }) 1421 if err != nil { 1422 return fmt.Errorf("[ERR] Error looking up Cache Settings for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) 1423 } 1424 1425 csl := flattenCacheSettings(cslList) 1426 1427 if err := d.Set("cache_setting", csl); err != nil { 1428 log.Printf("[WARN] Error setting Cache Settings for (%s): %s", d.Id(), err) 1429 } 1430 1431 } else { 1432 log.Printf("[DEBUG] Active Version for Service (%s) is empty, no state to refresh", d.Id()) 1433 } 1434 1435 return nil 1436 } 1437 1438 func resourceServiceV1Delete(d *schema.ResourceData, meta interface{}) error { 1439 conn := meta.(*FastlyClient).conn 1440 1441 // Fastly will fail to delete any service with an Active Version. 1442 // If `force_destroy` is given, we deactivate the active version and then send 1443 // the DELETE call 1444 if d.Get("force_destroy").(bool) { 1445 s, err := conn.GetServiceDetails(&gofastly.GetServiceInput{ 1446 ID: d.Id(), 1447 }) 1448 1449 if err != nil { 1450 return err 1451 } 1452 1453 if s.ActiveVersion.Number != "" { 1454 _, err := conn.DeactivateVersion(&gofastly.DeactivateVersionInput{ 1455 Service: d.Id(), 1456 Version: s.ActiveVersion.Number, 1457 }) 1458 if err != nil { 1459 return err 1460 } 1461 } 1462 } 1463 1464 err := conn.DeleteService(&gofastly.DeleteServiceInput{ 1465 ID: d.Id(), 1466 }) 1467 1468 if err != nil { 1469 return err 1470 } 1471 1472 _, err = findService(d.Id(), meta) 1473 if err != nil { 1474 switch err { 1475 // we expect no records to be found here 1476 case fastlyNoServiceFoundErr: 1477 d.SetId("") 1478 return nil 1479 default: 1480 return err 1481 } 1482 } 1483 1484 // findService above returned something and nil error, but shouldn't have 1485 return fmt.Errorf("[WARN] Tried deleting Service (%s), but was still found", d.Id()) 1486 1487 } 1488 1489 func flattenDomains(list []*gofastly.Domain) []map[string]interface{} { 1490 dl := make([]map[string]interface{}, 0, len(list)) 1491 1492 for _, d := range list { 1493 dl = append(dl, map[string]interface{}{ 1494 "name": d.Name, 1495 "comment": d.Comment, 1496 }) 1497 } 1498 1499 return dl 1500 } 1501 1502 func flattenBackends(backendList []*gofastly.Backend) []map[string]interface{} { 1503 var bl []map[string]interface{} 1504 for _, b := range backendList { 1505 // Convert Backend to a map for saving to state. 1506 nb := map[string]interface{}{ 1507 "name": b.Name, 1508 "address": b.Address, 1509 "auto_loadbalance": gofastly.CBool(b.AutoLoadbalance), 1510 "between_bytes_timeout": int(b.BetweenBytesTimeout), 1511 "connect_timeout": int(b.ConnectTimeout), 1512 "error_threshold": int(b.ErrorThreshold), 1513 "first_byte_timeout": int(b.FirstByteTimeout), 1514 "max_conn": int(b.MaxConn), 1515 "port": int(b.Port), 1516 "shield": b.Shield, 1517 "ssl_check_cert": gofastly.CBool(b.SSLCheckCert), 1518 "ssl_hostname": b.SSLHostname, 1519 "weight": int(b.Weight), 1520 "request_condition": b.RequestCondition, 1521 } 1522 1523 bl = append(bl, nb) 1524 } 1525 return bl 1526 } 1527 1528 // findService finds a Fastly Service via the ListServices endpoint, returning 1529 // the Service if found. 1530 // 1531 // Fastly API does not include any "deleted_at" type parameter to indicate 1532 // that a Service has been deleted. GET requests to a deleted Service will 1533 // return 200 OK and have the full output of the Service for an unknown time 1534 // (days, in my testing). In order to determine if a Service is deleted, we 1535 // need to hit /service and loop the returned Services, searching for the one 1536 // in question. This endpoint only returns active or "alive" services. If the 1537 // Service is not included, then it's "gone" 1538 // 1539 // Returns a fastlyNoServiceFoundErr error if the Service is not found in the 1540 // ListServices response. 1541 func findService(id string, meta interface{}) (*gofastly.Service, error) { 1542 conn := meta.(*FastlyClient).conn 1543 1544 l, err := conn.ListServices(&gofastly.ListServicesInput{}) 1545 if err != nil { 1546 return nil, fmt.Errorf("[WARN] Error listing services (%s): %s", id, err) 1547 } 1548 1549 for _, s := range l { 1550 if s.ID == id { 1551 log.Printf("[DEBUG] Found Service (%s)", id) 1552 return s, nil 1553 } 1554 } 1555 1556 return nil, fastlyNoServiceFoundErr 1557 } 1558 1559 func flattenHeaders(headerList []*gofastly.Header) []map[string]interface{} { 1560 var hl []map[string]interface{} 1561 for _, h := range headerList { 1562 // Convert Header to a map for saving to state. 1563 nh := map[string]interface{}{ 1564 "name": h.Name, 1565 "action": h.Action, 1566 "ignore_if_set": h.IgnoreIfSet, 1567 "type": h.Type, 1568 "destination": h.Destination, 1569 "source": h.Source, 1570 "regex": h.Regex, 1571 "substitution": h.Substitution, 1572 "priority": int(h.Priority), 1573 "request_condition": h.RequestCondition, 1574 "cache_condition": h.CacheCondition, 1575 "response_condition": h.ResponseCondition, 1576 } 1577 1578 for k, v := range nh { 1579 if v == "" { 1580 delete(nh, k) 1581 } 1582 } 1583 1584 hl = append(hl, nh) 1585 } 1586 return hl 1587 } 1588 1589 func buildHeader(headerMap interface{}) (*gofastly.CreateHeaderInput, error) { 1590 df := headerMap.(map[string]interface{}) 1591 opts := gofastly.CreateHeaderInput{ 1592 Name: df["name"].(string), 1593 IgnoreIfSet: gofastly.CBool(df["ignore_if_set"].(bool)), 1594 Destination: df["destination"].(string), 1595 Priority: uint(df["priority"].(int)), 1596 Source: df["source"].(string), 1597 Regex: df["regex"].(string), 1598 Substitution: df["substitution"].(string), 1599 RequestCondition: df["request_condition"].(string), 1600 CacheCondition: df["cache_condition"].(string), 1601 ResponseCondition: df["response_condition"].(string), 1602 } 1603 1604 act := strings.ToLower(df["action"].(string)) 1605 switch act { 1606 case "set": 1607 opts.Action = gofastly.HeaderActionSet 1608 case "append": 1609 opts.Action = gofastly.HeaderActionAppend 1610 case "delete": 1611 opts.Action = gofastly.HeaderActionDelete 1612 case "regex": 1613 opts.Action = gofastly.HeaderActionRegex 1614 case "regex_repeat": 1615 opts.Action = gofastly.HeaderActionRegexRepeat 1616 } 1617 1618 ty := strings.ToLower(df["type"].(string)) 1619 switch ty { 1620 case "request": 1621 opts.Type = gofastly.HeaderTypeRequest 1622 case "fetch": 1623 opts.Type = gofastly.HeaderTypeFetch 1624 case "cache": 1625 opts.Type = gofastly.HeaderTypeCache 1626 case "response": 1627 opts.Type = gofastly.HeaderTypeResponse 1628 } 1629 1630 return &opts, nil 1631 } 1632 1633 func buildCacheSetting(cacheMap interface{}) (*gofastly.CreateCacheSettingInput, error) { 1634 df := cacheMap.(map[string]interface{}) 1635 opts := gofastly.CreateCacheSettingInput{ 1636 Name: df["name"].(string), 1637 StaleTTL: uint(df["stale_ttl"].(int)), 1638 CacheCondition: df["cache_condition"].(string), 1639 } 1640 1641 if v, ok := df["ttl"]; ok { 1642 opts.TTL = uint(v.(int)) 1643 } 1644 1645 act := strings.ToLower(df["action"].(string)) 1646 switch act { 1647 case "cache": 1648 opts.Action = gofastly.CacheSettingActionCache 1649 case "pass": 1650 opts.Action = gofastly.CacheSettingActionPass 1651 case "restart": 1652 opts.Action = gofastly.CacheSettingActionRestart 1653 } 1654 1655 return &opts, nil 1656 } 1657 1658 func flattenGzips(gzipsList []*gofastly.Gzip) []map[string]interface{} { 1659 var gl []map[string]interface{} 1660 for _, g := range gzipsList { 1661 // Convert Gzip to a map for saving to state. 1662 ng := map[string]interface{}{ 1663 "name": g.Name, 1664 "cache_condition": g.CacheCondition, 1665 } 1666 1667 if g.Extensions != "" { 1668 e := strings.Split(g.Extensions, " ") 1669 var et []interface{} 1670 for _, ev := range e { 1671 et = append(et, ev) 1672 } 1673 ng["extensions"] = schema.NewSet(schema.HashString, et) 1674 } 1675 1676 if g.ContentTypes != "" { 1677 c := strings.Split(g.ContentTypes, " ") 1678 var ct []interface{} 1679 for _, cv := range c { 1680 ct = append(ct, cv) 1681 } 1682 ng["content_types"] = schema.NewSet(schema.HashString, ct) 1683 } 1684 1685 // prune any empty values that come from the default string value in structs 1686 for k, v := range ng { 1687 if v == "" { 1688 delete(ng, k) 1689 } 1690 } 1691 1692 gl = append(gl, ng) 1693 } 1694 1695 return gl 1696 } 1697 1698 func flattenS3s(s3List []*gofastly.S3) []map[string]interface{} { 1699 var sl []map[string]interface{} 1700 for _, s := range s3List { 1701 // Convert S3s to a map for saving to state. 1702 ns := map[string]interface{}{ 1703 "name": s.Name, 1704 "bucket_name": s.BucketName, 1705 "s3_access_key": s.AccessKey, 1706 "s3_secret_key": s.SecretKey, 1707 "path": s.Path, 1708 "period": s.Period, 1709 "domain": s.Domain, 1710 "gzip_level": s.GzipLevel, 1711 "format": s.Format, 1712 "timestamp_format": s.TimestampFormat, 1713 } 1714 1715 // prune any empty values that come from the default string value in structs 1716 for k, v := range ns { 1717 if v == "" { 1718 delete(ns, k) 1719 } 1720 } 1721 1722 sl = append(sl, ns) 1723 } 1724 1725 return sl 1726 } 1727 1728 func flattenConditions(conditionList []*gofastly.Condition) []map[string]interface{} { 1729 var cl []map[string]interface{} 1730 for _, c := range conditionList { 1731 // Convert Conditions to a map for saving to state. 1732 nc := map[string]interface{}{ 1733 "name": c.Name, 1734 "statement": c.Statement, 1735 "type": c.Type, 1736 "priority": c.Priority, 1737 } 1738 1739 // prune any empty values that come from the default string value in structs 1740 for k, v := range nc { 1741 if v == "" { 1742 delete(nc, k) 1743 } 1744 } 1745 1746 cl = append(cl, nc) 1747 } 1748 1749 return cl 1750 } 1751 1752 func flattenRequestSettings(rsList []*gofastly.RequestSetting) []map[string]interface{} { 1753 var rl []map[string]interface{} 1754 for _, r := range rsList { 1755 // Convert Request Settings to a map for saving to state. 1756 nrs := map[string]interface{}{ 1757 "name": r.Name, 1758 "max_stale_age": r.MaxStaleAge, 1759 "force_miss": r.ForceMiss, 1760 "force_ssl": r.ForceSSL, 1761 "action": r.Action, 1762 "bypass_busy_wait": r.BypassBusyWait, 1763 "hash_keys": r.HashKeys, 1764 "xff": r.XForwardedFor, 1765 "timer_support": r.TimerSupport, 1766 "geo_headers": r.GeoHeaders, 1767 "default_host": r.DefaultHost, 1768 "request_condition": r.RequestCondition, 1769 } 1770 1771 // prune any empty values that come from the default string value in structs 1772 for k, v := range nrs { 1773 if v == "" { 1774 delete(nrs, k) 1775 } 1776 } 1777 1778 rl = append(rl, nrs) 1779 } 1780 1781 return rl 1782 } 1783 1784 func buildRequestSetting(requestSettingMap interface{}) (*gofastly.CreateRequestSettingInput, error) { 1785 df := requestSettingMap.(map[string]interface{}) 1786 opts := gofastly.CreateRequestSettingInput{ 1787 Name: df["name"].(string), 1788 MaxStaleAge: uint(df["max_stale_age"].(int)), 1789 ForceMiss: gofastly.CBool(df["force_miss"].(bool)), 1790 ForceSSL: gofastly.CBool(df["force_ssl"].(bool)), 1791 BypassBusyWait: gofastly.CBool(df["bypass_busy_wait"].(bool)), 1792 HashKeys: df["hash_keys"].(string), 1793 TimerSupport: gofastly.CBool(df["timer_support"].(bool)), 1794 GeoHeaders: gofastly.CBool(df["geo_headers"].(bool)), 1795 DefaultHost: df["default_host"].(string), 1796 RequestCondition: df["request_condition"].(string), 1797 } 1798 1799 act := strings.ToLower(df["action"].(string)) 1800 switch act { 1801 case "lookup": 1802 opts.Action = gofastly.RequestSettingActionLookup 1803 case "pass": 1804 opts.Action = gofastly.RequestSettingActionPass 1805 } 1806 1807 xff := strings.ToLower(df["xff"].(string)) 1808 switch xff { 1809 case "clear": 1810 opts.XForwardedFor = gofastly.RequestSettingXFFClear 1811 case "leave": 1812 opts.XForwardedFor = gofastly.RequestSettingXFFLeave 1813 case "append": 1814 opts.XForwardedFor = gofastly.RequestSettingXFFAppend 1815 case "append_all": 1816 opts.XForwardedFor = gofastly.RequestSettingXFFAppendAll 1817 case "overwrite": 1818 opts.XForwardedFor = gofastly.RequestSettingXFFOverwrite 1819 } 1820 1821 return &opts, nil 1822 } 1823 1824 func flattenCacheSettings(csList []*gofastly.CacheSetting) []map[string]interface{} { 1825 var csl []map[string]interface{} 1826 for _, cl := range csList { 1827 // Convert Cache Settings to a map for saving to state. 1828 clMap := map[string]interface{}{ 1829 "name": cl.Name, 1830 "action": cl.Action, 1831 "cache_condition": cl.CacheCondition, 1832 "stale_ttl": cl.StaleTTL, 1833 "ttl": cl.TTL, 1834 } 1835 1836 // prune any empty values that come from the default string value in structs 1837 for k, v := range clMap { 1838 if v == "" { 1839 delete(clMap, k) 1840 } 1841 } 1842 1843 csl = append(csl, clMap) 1844 } 1845 1846 return csl 1847 } 1848 1849 func flattenVCLs(vclList []*gofastly.VCL) []map[string]interface{} { 1850 var vl []map[string]interface{} 1851 for _, vcl := range vclList { 1852 // Convert VCLs to a map for saving to state. 1853 vclMap := map[string]interface{}{ 1854 "name": vcl.Name, 1855 "content": vcl.Content, 1856 "main": vcl.Main, 1857 } 1858 1859 // prune any empty values that come from the default string value in structs 1860 for k, v := range vclMap { 1861 if v == "" { 1862 delete(vclMap, k) 1863 } 1864 } 1865 1866 vl = append(vl, vclMap) 1867 } 1868 1869 return vl 1870 } 1871 1872 func validateVCLs(d *schema.ResourceData) error { 1873 // TODO: this would be nice to move into a resource/collection validation function, once that is available 1874 // (see https://github.com/hashicorp/terraform/pull/4348 and https://github.com/hashicorp/terraform/pull/6508) 1875 vcls, exists := d.GetOk("vcl") 1876 if !exists { 1877 return nil 1878 } 1879 1880 numberOfMainVCLs, numberOfIncludeVCLs := 0, 0 1881 for _, vclElem := range vcls.(*schema.Set).List() { 1882 vcl := vclElem.(map[string]interface{}) 1883 if mainVal, hasMain := vcl["main"]; hasMain && mainVal.(bool) { 1884 numberOfMainVCLs++ 1885 } else { 1886 numberOfIncludeVCLs++ 1887 } 1888 } 1889 if numberOfMainVCLs == 0 && numberOfIncludeVCLs > 0 { 1890 return fmt.Errorf("if you include VCL configurations, one of them should have main = true") 1891 } 1892 if numberOfMainVCLs > 1 { 1893 return fmt.Errorf("you cannot have more than one VCL configuration with main = true") 1894 } 1895 return nil 1896 }