From 9437912d3f6668aff34271cd47e8beb263f980dd Mon Sep 17 00:00:00 2001 From: Clint Date: Mon, 23 May 2016 15:07:00 -0500 Subject: [PATCH] provider/fastly: Add support for Cache Settings (#6781) * provider/fastly: Add cache settings Docs, tests, and implementation for Cache Settings support --- .../fastly/resource_fastly_service_v1.go | 160 ++++++++++++++ ...ce_fastly_service_v1_cache_setting_test.go | 205 ++++++++++++++++++ .../fastly/r/service_v1.html.markdown | 13 ++ 3 files changed, 378 insertions(+) create mode 100644 builtin/providers/fastly/resource_fastly_service_v1_cache_setting_test.go diff --git a/builtin/providers/fastly/resource_fastly_service_v1.go b/builtin/providers/fastly/resource_fastly_service_v1.go index 7b289690d..e61802c55 100644 --- a/builtin/providers/fastly/resource_fastly_service_v1.go +++ b/builtin/providers/fastly/resource_fastly_service_v1.go @@ -21,6 +21,9 @@ func resourceServiceV1() *schema.Resource { Read: resourceServiceV1Read, Update: resourceServiceV1Update, Delete: resourceServiceV1Delete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, Schema: map[string]*schema.Schema{ "name": &schema.Schema{ @@ -193,6 +196,43 @@ func resourceServiceV1() *schema.Resource { Optional: true, }, + "cache_setting": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + // required fields + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "A name to refer to this Cache Setting", + }, + "cache_condition": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "Condition to check if this Cache Setting applies", + }, + "action": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "Action to take", + }, + // optional + "stale_ttl": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Description: "Max 'Time To Live' for stale (unreachable) objects.", + Default: 300, + }, + "ttl": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Description: "The 'Time To Live' for the object", + }, + }, + }, + }, + "gzip": &schema.Schema{ Type: schema.TypeSet, Optional: true, @@ -560,6 +600,7 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error { "s3logging", "condition", "request_setting", + "cache_setting", "vcl", } { if d.HasChange(v) { @@ -1020,6 +1061,7 @@ 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 @@ -1086,6 +1128,56 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error { } } + // Find differences in Cache Settings + if d.HasChange("cache_setting") { + oc, nc := d.GetChange("cache_setting") + if oc == nil { + oc = new(schema.Set) + } + if nc == nil { + nc = new(schema.Set) + } + + ocs := oc.(*schema.Set) + ncs := nc.(*schema.Set) + + remove := ocs.Difference(ncs).List() + add := ncs.Difference(ocs).List() + + // Delete removed Cache Settings + for _, dRaw := range remove { + df := dRaw.(map[string]interface{}) + opts := gofastly.DeleteCacheSettingInput{ + Service: d.Id(), + Version: latestVersion, + Name: df["name"].(string), + } + + log.Printf("[DEBUG] Fastly Cache Settings removal opts: %#v", opts) + err := conn.DeleteCacheSetting(&opts) + if err != nil { + return err + } + } + + // POST new Cache Settings + for _, dRaw := range add { + opts, err := buildCacheSetting(dRaw.(map[string]interface{})) + if err != nil { + log.Printf("[DEBUG] Error building Cache Setting: %s", err) + return err + } + opts.Service = d.Id() + opts.Version = latestVersion + + log.Printf("[DEBUG] Fastly Cache Settings Addition opts: %#v", opts) + _, err = conn.CreateCacheSetting(opts) + if err != nil { + return err + } + } + } + // validate version log.Printf("[DEBUG] Validating Fastly Service (%s), Version (%s)", d.Id(), latestVersion) valid, msg, err := conn.ValidateVersion(&gofastly.ValidateVersionInput{ @@ -1282,6 +1374,7 @@ 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{ @@ -1298,6 +1391,22 @@ func resourceServiceV1Read(d *schema.ResourceData, meta interface{}) error { log.Printf("[WARN] Error setting VCLs for (%s): %s", d.Id(), err) } + // refresh Cache Settings + log.Printf("[DEBUG] Refreshing Cache Settings for (%s)", d.Id()) + cslList, err := conn.ListCacheSettings(&gofastly.ListCacheSettingsInput{ + Service: d.Id(), + Version: s.ActiveVersion.Number, + }) + if err != nil { + return fmt.Errorf("[ERR] Error looking up Cache Settings for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) + } + + csl := flattenCacheSettings(cslList) + + if err := d.Set("cache_setting", csl); err != nil { + log.Printf("[WARN] Error setting Cache Settings for (%s): %s", d.Id(), err) + } + } else { log.Printf("[DEBUG] Active Version for Service (%s) is empty, no state to refresh", d.Id()) } @@ -1497,6 +1606,31 @@ func buildHeader(headerMap interface{}) (*gofastly.CreateHeaderInput, error) { return &opts, nil } +func buildCacheSetting(cacheMap interface{}) (*gofastly.CreateCacheSettingInput, error) { + df := cacheMap.(map[string]interface{}) + opts := gofastly.CreateCacheSettingInput{ + Name: df["name"].(string), + StaleTTL: uint(df["stale_ttl"].(int)), + CacheCondition: df["cache_condition"].(string), + } + + if v, ok := df["ttl"]; ok { + opts.TTL = uint(v.(int)) + } + + act := strings.ToLower(df["action"].(string)) + switch act { + case "cache": + opts.Action = gofastly.CacheSettingActionCache + case "pass": + opts.Action = gofastly.CacheSettingActionPass + case "restart": + opts.Action = gofastly.CacheSettingActionRestart + } + + return &opts, nil +} + func flattenGzips(gzipsList []*gofastly.Gzip) []map[string]interface{} { var gl []map[string]interface{} for _, g := range gzipsList { @@ -1662,6 +1796,32 @@ func buildRequestSetting(requestSettingMap interface{}) (*gofastly.CreateRequest return &opts, nil } + +func flattenCacheSettings(csList []*gofastly.CacheSetting) []map[string]interface{} { + var csl []map[string]interface{} + for _, cl := range csList { + // Convert Cache Settings to a map for saving to state. + clMap := map[string]interface{}{ + "name": cl.Name, + "action": cl.Action, + "cache_condition": cl.CacheCondition, + "stale_ttl": cl.StaleTTL, + "ttl": cl.TTL, + } + + // prune any empty values that come from the default string value in structs + for k, v := range clMap { + if v == "" { + delete(clMap, k) + } + } + + csl = append(csl, clMap) + } + + return csl +} + func flattenVCLs(vclList []*gofastly.VCL) []map[string]interface{} { var vl []map[string]interface{} for _, vcl := range vclList { diff --git a/builtin/providers/fastly/resource_fastly_service_v1_cache_setting_test.go b/builtin/providers/fastly/resource_fastly_service_v1_cache_setting_test.go new file mode 100644 index 000000000..194a55586 --- /dev/null +++ b/builtin/providers/fastly/resource_fastly_service_v1_cache_setting_test.go @@ -0,0 +1,205 @@ +package fastly + +import ( + "fmt" + "reflect" + "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 TestAccFastlyServiceV1CacheSetting_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)) + + cq1 := gofastly.CacheSetting{ + Name: "alt_backend", + Action: "pass", + StaleTTL: uint(3600), + CacheCondition: "serve_alt_backend", + } + + cq2 := gofastly.CacheSetting{ + Name: "cache_backend", + Action: "restart", + StaleTTL: uint(1600), + CacheCondition: "cache_alt_backend", + TTL: uint(300), + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckServiceV1Destroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccServiceV1CacheSetting(name, domainName1), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceV1Exists("fastly_service_v1.foo", &service), + testAccCheckFastlyServiceV1CacheSettingsAttributes(&service, []*gofastly.CacheSetting{&cq1}), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "name", name), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "cache_setting.#", "1"), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "condition.#", "1"), + ), + }, + + resource.TestStep{ + Config: testAccServiceV1CacheSetting_update(name, domainName1), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceV1Exists("fastly_service_v1.foo", &service), + testAccCheckFastlyServiceV1CacheSettingsAttributes(&service, []*gofastly.CacheSetting{&cq1, &cq2}), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "cache_setting.#", "2"), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "condition.#", "2"), + ), + }, + }, + }) +} + +func testAccCheckFastlyServiceV1CacheSettingsAttributes(service *gofastly.ServiceDetail, rqs []*gofastly.CacheSetting) resource.TestCheckFunc { + return func(s *terraform.State) error { + + conn := testAccProvider.Meta().(*FastlyClient).conn + rqList, err := conn.ListCacheSettings(&gofastly.ListCacheSettingsInput{ + Service: service.ID, + Version: service.ActiveVersion.Number, + }) + + if err != nil { + return fmt.Errorf("[ERR] Error looking up Request Setting for (%s), version (%s): %s", service.Name, service.ActiveVersion.Number, err) + } + + if len(rqList) != len(rqs) { + return fmt.Errorf("Request Setting List count mismatch, expected (%d), got (%d)", len(rqs), len(rqList)) + } + + var found int + for _, r := range rqs { + for _, lr := range rqList { + if r.Name == lr.Name { + // we don't know these things ahead of time, so populate them now + r.ServiceID = service.ID + r.Version = service.ActiveVersion.Number + if !reflect.DeepEqual(r, lr) { + return fmt.Errorf("Bad match Request Setting match, expected (%#v), got (%#v)", r, lr) + } + found++ + } + } + } + + if found != len(rqs) { + return fmt.Errorf("Error matching Request Setting rules (%d/%d)", found, len(rqs)) + } + + return nil + } +} + +func testAccServiceV1CacheSetting(name, domain string) string { + return fmt.Sprintf(` +resource "fastly_service_v1" "foo" { + name = "%s" + + domain { + name = "%s" + comment = "demo" + } + + backend { + address = "tftesting.tftesting.net.s3-website-us-west-2.amazonaws.com" + name = "AWS S3 hosting" + port = 80 + } + + backend { + address = "tftestingother.tftesting.net.s3-website-us-west-2.amazonaws.com" + name = "OtherAWSS3hosting" + port = 80 + } + + condition { + name = "serve_alt_backend" + type = "CACHE" + priority = 10 + statement = "req.url ~ \"^/alt/\"" + } + + cache_setting { + name = "alt_backend" + stale_ttl = 3600 + cache_condition = "serve_alt_backend" + action = "pass" + } + + default_host = "tftesting.tftesting.net.s3-website-us-west-2.amazonaws.com" + + force_destroy = true +}`, name, domain) +} + +func testAccServiceV1CacheSetting_update(name, domain string) string { + return fmt.Sprintf(` +resource "fastly_service_v1" "foo" { + name = "%s" + + domain { + name = "%s" + comment = "demo" + } + + backend { + address = "tftesting.tftesting.net.s3-website-us-west-2.amazonaws.com" + name = "AWS S3 hosting" + port = 80 + } + + backend { + address = "tftestingother.tftesting.net.s3-website-us-west-2.amazonaws.com" + name = "OtherAWSS3hosting" + port = 80 + } + + condition { + name = "serve_alt_backend" + type = "CACHE" + priority = 10 + statement = "req.url ~ \"^/alt/\"" + } + + condition { + name = "cache_alt_backend" + type = "CACHE" + priority = 20 + statement = "req.url ~ \"^/cache/\"" + } + + cache_setting { + name = "alt_backend" + stale_ttl = 3600 + cache_condition = "serve_alt_backend" + action = "pass" + } + + cache_setting { + name = "cache_backend" + stale_ttl = 1600 + cache_condition = "cache_alt_backend" + action = "restart" + ttl = 300 + } + + default_host = "tftesting.tftesting.net.s3-website-us-west-2.amazonaws.com" + + force_destroy = true +}`, name, domain) +} diff --git a/website/source/docs/providers/fastly/r/service_v1.html.markdown b/website/source/docs/providers/fastly/r/service_v1.html.markdown index 207f34d5c..2706e896b 100644 --- a/website/source/docs/providers/fastly/r/service_v1.html.markdown +++ b/website/source/docs/providers/fastly/r/service_v1.html.markdown @@ -134,6 +134,8 @@ Service. Defined below Defined below * `condition` - (Optional) A set of conditions to add logic to any basic configuration object in this service. Defined below +* `cache_setting` - (Optional) A set of Cache Settings, allowing you to override +when an item is not to be cached based on an above `condition`. Defined below * `gzip` - (Required) A set of gzip rules to control automatic gzipping of content. Defined below * `header` - (Optional) A set of Headers to manipulate for each request. Defined @@ -187,6 +189,17 @@ conditions execute. Lower numbers execute first * `type` - (Required) Type of the condition, either `REQUEST` (req), `RESPONSE` (req, resp), or `CACHE` (req, beresp) +The `cache_setting` block supports: + +* `name` - (Required) A unique name to label this Cache Setting +* `action` - (Required) One of `cache`, `pass`, or `restart`, as defined +on Fastly's documenation under ["Caching action descriptions"](https://docs.fastly.com/guides/performance-tuning/controlling-caching#caching-action-descriptions) +* `cache_condition` - (Required) Name of the condition used to test whether this settings object should be used. +This Condition must be of type `CACHE` +* `stale_ttl` - (Optional) Max "Time To Live" for stale (unreachable) objects. +Default `300` +* `ttl` - (Optional) The "Time To Live" for the object + The `gzip` block supports: * `name` - (Required) A unique name