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