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