diff --git a/builtin/providers/fastly/resource_fastly_service_v1.go b/builtin/providers/fastly/resource_fastly_service_v1.go index 4175715f2..7b289690d 100644 --- a/builtin/providers/fastly/resource_fastly_service_v1.go +++ b/builtin/providers/fastly/resource_fastly_service_v1.go @@ -1,6 +1,8 @@ package fastly import ( + "crypto/sha1" + "encoding/hex" "errors" "fmt" "log" @@ -469,11 +471,48 @@ func resourceServiceV1() *schema.Resource { }, }, }, + "vcl": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "A name to refer to this VCL configuration", + }, + "content": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "The contents of this VCL configuration", + StateFunc: func(v interface{}) string { + switch v.(type) { + case string: + hash := sha1.Sum([]byte(v.(string))) + return hex.EncodeToString(hash[:]) + default: + return "" + } + }, + }, + "main": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Should this VCL configuation be the main configuration", + }, + }, + }, + }, }, } } func resourceServiceV1Create(d *schema.ResourceData, meta interface{}) error { + if err := validateVCLs(d); err != nil { + return err + } + conn := meta.(*FastlyClient).conn service, err := conn.CreateService(&gofastly.CreateServiceInput{ Name: d.Get("name").(string), @@ -489,6 +528,10 @@ func resourceServiceV1Create(d *schema.ResourceData, meta interface{}) error { } func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error { + if err := validateVCLs(d); err != nil { + return err + } + conn := meta.(*FastlyClient).conn // Update Name. No new verions is required for this @@ -517,6 +560,7 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error { "s3logging", "condition", "request_setting", + "vcl", } { if d.HasChange(v) { needsChange = true @@ -976,6 +1020,71 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error { } } } + // Find differences in VCLs + if d.HasChange("vcl") { + // Note: as above with Gzip and S3 logging, we don't utilize the PUT + // endpoint to update a VCL, we simply destroy it and create a new one. + oldVCLVal, newVCLVal := d.GetChange("vcl") + if oldVCLVal == nil { + oldVCLVal = new(schema.Set) + } + if newVCLVal == nil { + newVCLVal = new(schema.Set) + } + + oldVCLSet := oldVCLVal.(*schema.Set) + newVCLSet := newVCLVal.(*schema.Set) + + remove := oldVCLSet.Difference(newVCLSet).List() + add := newVCLSet.Difference(oldVCLSet).List() + + // Delete removed VCL configurations + for _, dRaw := range remove { + df := dRaw.(map[string]interface{}) + opts := gofastly.DeleteVCLInput{ + Service: d.Id(), + Version: latestVersion, + Name: df["name"].(string), + } + + log.Printf("[DEBUG] Fastly VCL Removal opts: %#v", opts) + err := conn.DeleteVCL(&opts) + if err != nil { + return err + } + } + // POST new VCL configurations + for _, dRaw := range add { + df := dRaw.(map[string]interface{}) + opts := gofastly.CreateVCLInput{ + Service: d.Id(), + Version: latestVersion, + Name: df["name"].(string), + Content: df["content"].(string), + } + + log.Printf("[DEBUG] Fastly VCL Addition opts: %#v", opts) + _, err := conn.CreateVCL(&opts) + if err != nil { + return err + } + + // if this new VCL is the main + if df["main"].(bool) { + opts := gofastly.ActivateVCLInput{ + Service: d.Id(), + Version: latestVersion, + Name: df["name"].(string), + } + log.Printf("[DEBUG] Fastly VCL activation opts: %#v", opts) + _, err := conn.ActivateVCL(&opts) + if err != nil { + return err + } + + } + } + } // validate version log.Printf("[DEBUG] Validating Fastly Service (%s), Version (%s)", d.Id(), latestVersion) @@ -1173,6 +1282,21 @@ func resourceServiceV1Read(d *schema.ResourceData, meta interface{}) error { if err := d.Set("request_setting", rl); err != nil { log.Printf("[WARN] Error setting Request Settings for (%s): %s", d.Id(), err) } + // refresh VCLs + log.Printf("[DEBUG] Refreshing VCLs for (%s)", d.Id()) + vclList, err := conn.ListVCLs(&gofastly.ListVCLsInput{ + Service: d.Id(), + Version: s.ActiveVersion.Number, + }) + if err != nil { + return fmt.Errorf("[ERR] Error looking up VCLs for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) + } + + vl := flattenVCLs(vclList) + + if err := d.Set("vcl", vl); err != nil { + log.Printf("[WARN] Error setting VCLs for (%s): %s", d.Id(), err) + } } else { log.Printf("[DEBUG] Active Version for Service (%s) is empty, no state to refresh", d.Id()) @@ -1538,3 +1662,51 @@ func buildRequestSetting(requestSettingMap interface{}) (*gofastly.CreateRequest return &opts, nil } +func flattenVCLs(vclList []*gofastly.VCL) []map[string]interface{} { + var vl []map[string]interface{} + for _, vcl := range vclList { + // Convert VCLs to a map for saving to state. + vclMap := map[string]interface{}{ + "name": vcl.Name, + "content": vcl.Content, + "main": vcl.Main, + } + + // prune any empty values that come from the default string value in structs + for k, v := range vclMap { + if v == "" { + delete(vclMap, k) + } + } + + vl = append(vl, vclMap) + } + + return vl +} + +func validateVCLs(d *schema.ResourceData) error { + // TODO: this would be nice to move into a resource/collection validation function, once that is available + // (see https://github.com/hashicorp/terraform/pull/4348 and https://github.com/hashicorp/terraform/pull/6508) + vcls, exists := d.GetOk("vcl") + if !exists { + return nil + } + + numberOfMainVCLs, numberOfIncludeVCLs := 0, 0 + for _, vclElem := range vcls.(*schema.Set).List() { + vcl := vclElem.(map[string]interface{}) + if mainVal, hasMain := vcl["main"]; hasMain && mainVal.(bool) { + numberOfMainVCLs++ + } else { + numberOfIncludeVCLs++ + } + } + if numberOfMainVCLs == 0 && numberOfIncludeVCLs > 0 { + return fmt.Errorf("if you include VCL configurations, one of them should have main = true") + } + if numberOfMainVCLs > 1 { + return fmt.Errorf("you cannot have more than one VCL configuration with main = true") + } + return nil +} diff --git a/builtin/providers/fastly/resource_fastly_service_v1_vcl_test.go b/builtin/providers/fastly/resource_fastly_service_v1_vcl_test.go new file mode 100644 index 000000000..0d0c47543 --- /dev/null +++ b/builtin/providers/fastly/resource_fastly_service_v1_vcl_test.go @@ -0,0 +1,152 @@ +package fastly + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + gofastly "github.com/sethvargo/go-fastly" +) + +func TestAccFastlyServiceV1_VCL_basic(t *testing.T) { + var service gofastly.ServiceDetail + name := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + domainName1 := fmt.Sprintf("%s.notadomain.com", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckServiceV1Destroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccServiceV1VCLConfig(name, domainName1), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceV1Exists("fastly_service_v1.foo", &service), + testAccCheckFastlyServiceV1VCLAttributes(&service, name, 1), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "name", name), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "vcl.#", "1"), + ), + }, + + resource.TestStep{ + Config: testAccServiceV1VCLConfig_update(name, domainName1), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceV1Exists("fastly_service_v1.foo", &service), + testAccCheckFastlyServiceV1VCLAttributes(&service, name, 2), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "name", name), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "vcl.#", "2"), + ), + }, + }, + }) +} + +func testAccCheckFastlyServiceV1VCLAttributes(service *gofastly.ServiceDetail, name string, vclCount int) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if service.Name != name { + return fmt.Errorf("Bad name, expected (%s), got (%s)", name, service.Name) + } + + conn := testAccProvider.Meta().(*FastlyClient).conn + vclList, err := conn.ListVCLs(&gofastly.ListVCLsInput{ + Service: service.ID, + Version: service.ActiveVersion.Number, + }) + + if err != nil { + return fmt.Errorf("[ERR] Error looking up VCL for (%s), version (%s): %s", service.Name, service.ActiveVersion.Number, err) + } + + if len(vclList) != vclCount { + return fmt.Errorf("VCL count mismatch, expected (%d), got (%d)", vclCount, len(vclList)) + } + + return nil + } +} + +func testAccServiceV1VCLConfig(name, domain string) string { + return fmt.Sprintf(` +resource "fastly_service_v1" "foo" { + name = "%s" + + domain { + name = "%s" + comment = "tf-testing-domain" + } + + backend { + address = "aws.amazon.com" + name = "amazon docs" + } + + vcl { + name = "my_custom_main_vcl" + content = <.s3-website-.amazonaws.com`. The `default_host` attribute should be set to `.s3-website-.amazonaws.com`. See the @@ -113,6 +145,9 @@ order to destroy the Service, set `force_destroy` to `true`. Default `false`. * `request_setting` - (Optional) A set of Request modifiers. Defined below * `s3logging` - (Optional) A set of S3 Buckets to send streaming logs too. Defined below +* `vcl` - (Optional) A set of custom VCL configuration blocks. Note that the +ability to upload custom VCL code is not enabled by default for new Fastly +accounts (see the [Fastly documentation](https://docs.fastly.com/guides/vcl/uploading-custom-vcl) for details). The `domain` block supports: @@ -234,6 +269,13 @@ Apache Common Log format (`%h %l %u %t %r %>s`) Request Setting should be applied. For detailed information about Conditionals, see [Fastly's Documentation on Conditionals][fastly-conditionals] +The `vcl` block supports: + +* `name` - (Required) A unique name for this configuration block +* `content` - (Required) The custom VCL code to upload. +* `main` - (Optional) If `true`, use this block as the main configuration. If +`false`, use this block as an includable library. Only a single VCL block can be +marked as the main block. Default is `false`. ## Attributes Reference @@ -246,6 +288,7 @@ The following attributes are exported: * `backend` – Set of Backends. See above for details * `header` – Set of Headers. See above for details * `s3logging` – Set of S3 Logging configurations. See above for details +* `vcl` – Set of custom VCL configurations. See above for details * `default_host` – Default host specified * `default_ttl` - Default TTL * `force_destroy` - Force the destruction of the Service on delete