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