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