From 3eee40cd98767e0d6d952b5071ba5000ff40bf3a Mon Sep 17 00:00:00 2001 From: Clint Date: Mon, 9 May 2016 13:08:13 -0500 Subject: [PATCH] provider/fastly: Add support for Conditions for Fastly Services (#6481) * provider/fastly: Add support for Conditions for Fastly Services Docs here: - https://docs.fastly.com/guides/conditions/ Also Bump go-fastly version for domain support in S3 Logging --- .../fastly/resource_fastly_service_v1.go | 165 +++++++++++++++--- ...rce_fastly_service_v1_conditionals_test.go | 122 +++++++++++++ .../github.com/sethvargo/go-fastly/version.go | 16 +- .../fastly/r/service_v1.html.markdown | 16 ++ 4 files changed, 284 insertions(+), 35 deletions(-) create mode 100644 builtin/providers/fastly/resource_fastly_service_v1_conditionals_test.go diff --git a/builtin/providers/fastly/resource_fastly_service_v1.go b/builtin/providers/fastly/resource_fastly_service_v1.go index 578823180..a4bf4032c 100644 --- a/builtin/providers/fastly/resource_fastly_service_v1.go +++ b/builtin/providers/fastly/resource_fastly_service_v1.go @@ -55,6 +55,39 @@ func resourceServiceV1() *schema.Resource { }, }, + "condition": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "statement": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "The statement used to determine if the condition is met", + StateFunc: func(v interface{}) string { + value := v.(string) + // Trim newlines and spaces, to match Fastly API + return strings.TrimSpace(value) + }, + }, + "priority": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + Description: "A number used to determine the order in which multiple conditions execute. Lower numbers execute first", + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "Type of the condition, either `REQUEST`, `RESPONSE`, or `CACHE`", + }, + }, + }, + }, + "default_ttl": &schema.Schema{ Type: schema.TypeInt, Optional: true, @@ -409,6 +442,7 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error { "header", "gzip", "s3logging", + "condition", } { if d.HasChange(v) { needsChange = true @@ -463,13 +497,70 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error { } } + // Conditions need to be updated first, as they can be referenced by other + // configuraiton objects (Backends, Request Headers, etc) + + // Find difference in Conditions + if d.HasChange("condition") { + // Note: we don't utilize the PUT endpoint to update these objects, we simply + // destroy any that have changed, and create new ones with the updated + // values. This is how Terraform works with nested sub resources, we only + // get the full diff not a partial set item diff. Because this is done + // on a new version of the Fastly Service configuration, this is considered safe + + oc, nc := d.GetChange("condition") + if oc == nil { + oc = new(schema.Set) + } + if nc == nil { + nc = new(schema.Set) + } + + ocs := oc.(*schema.Set) + ncs := nc.(*schema.Set) + removeConditions := ocs.Difference(ncs).List() + addConditions := ncs.Difference(ocs).List() + + // DELETE old Conditions + for _, cRaw := range removeConditions { + cf := cRaw.(map[string]interface{}) + opts := gofastly.DeleteConditionInput{ + Service: d.Id(), + Version: latestVersion, + Name: cf["name"].(string), + } + + log.Printf("[DEBUG] Fastly Conditions Removal opts: %#v", opts) + err := conn.DeleteCondition(&opts) + if err != nil { + return err + } + } + + // POST new Conditions + for _, cRaw := range addConditions { + cf := cRaw.(map[string]interface{}) + opts := gofastly.CreateConditionInput{ + Service: d.Id(), + Version: latestVersion, + Name: cf["name"].(string), + Type: cf["type"].(string), + // need to trim leading/tailing spaces, incase the config has HEREDOC + // formatting and contains a trailing new line + Statement: strings.TrimSpace(cf["statement"].(string)), + Priority: cf["priority"].(int), + } + + log.Printf("[DEBUG] Create Conditions Opts: %#v", opts) + _, err := conn.CreateCondition(&opts) + if err != nil { + return err + } + } + } + // Find differences in domains if d.HasChange("domain") { - // Note: we don't utilize the PUT endpoint to update a Domain, we simply - // destroy it and create a new one. This is how Terraform works with nested - // sub resources, we only get the full diff not a partial set item diff. - // Because this is done on a new version of the configuration, this is - // considered safe od, nd := d.GetChange("domain") if od == nil { od = new(schema.Set) @@ -523,12 +614,6 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error { // find difference in backends if d.HasChange("backend") { - // POST new Backends - // Note: we don't utilize the PUT endpoint to update a Backend, we simply - // destroy it and create a new one. This is how Terraform works with nested - // sub resources, we only get the full diff not a partial set item diff. - // Because this is done on a new version of the configuration, this is - // considered safe ob, nb := d.GetChange("backend") if ob == nil { ob = new(schema.Set) @@ -558,6 +643,7 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error { } } + // Find and post new Backends for _, dRaw := range addBackends { df := dRaw.(map[string]interface{}) opts := gofastly.CreateBackendInput{ @@ -585,11 +671,6 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error { } if d.HasChange("header") { - // Note: we don't utilize the PUT endpoint to update a Header, we simply - // destroy it and create a new one. This is how Terraform works with nested - // sub resources, we only get the full diff not a partial set item diff. - // Because this is done on a new version of the configuration, this is - // considered safe oh, nh := d.GetChange("header") if oh == nil { oh = new(schema.Set) @@ -640,11 +721,6 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error { // Find differences in Gzips if d.HasChange("gzip") { - // Note: we don't utilize the PUT endpoint to update a Gzip rule, we simply - // destroy it and create a new one. This is how Terraform works with nested - // sub resources, we only get the full diff not a partial set item diff. - // Because this is done on a new version of the configuration, this is - // considered safe og, ng := d.GetChange("gzip") if og == nil { og = new(schema.Set) @@ -714,12 +790,6 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error { // find difference in s3logging if d.HasChange("s3logging") { - // POST new Logging - // Note: we don't utilize the PUT endpoint to update a S3 Logs, we simply - // destroy it and create a new one. This is how Terraform works with nested - // sub resources, we only get the full diff not a partial set item diff. - // Because this is done on a new version of the configuration, this is - // considered safe os, ns := d.GetChange("s3logging") if os == nil { os = new(schema.Set) @@ -947,6 +1017,23 @@ func resourceServiceV1Read(d *schema.ResourceData, meta interface{}) error { log.Printf("[WARN] Error setting S3 Logging for (%s): %s", d.Id(), err) } + // refresh Conditions + log.Printf("[DEBUG] Refreshing Conditions for (%s)", d.Id()) + conditionList, err := conn.ListConditions(&gofastly.ListConditionsInput{ + Service: d.Id(), + Version: s.ActiveVersion.Number, + }) + + if err != nil { + return fmt.Errorf("[ERR] Error looking up Conditions for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) + } + + cl := flattenConditions(conditionList) + + if err := d.Set("condition", cl); err != nil { + log.Printf("[WARN] Error setting Conditions for (%s): %s", d.Id(), err) + } + } else { log.Printf("[DEBUG] Active Version for Service (%s) is empty, no state to refresh", d.Id()) } @@ -1215,3 +1302,27 @@ func flattenS3s(s3List []*gofastly.S3) []map[string]interface{} { return sl } + +func flattenConditions(conditionList []*gofastly.Condition) []map[string]interface{} { + var cl []map[string]interface{} + for _, c := range conditionList { + // Convert Conditions to a map for saving to state. + nc := map[string]interface{}{ + "name": c.Name, + "statement": c.Statement, + "type": c.Type, + "priority": c.Priority, + } + + // prune any empty values that come from the default string value in structs + for k, v := range nc { + if v == "" { + delete(nc, k) + } + } + + cl = append(cl, nc) + } + + return cl +} diff --git a/builtin/providers/fastly/resource_fastly_service_v1_conditionals_test.go b/builtin/providers/fastly/resource_fastly_service_v1_conditionals_test.go new file mode 100644 index 000000000..ab64a20d9 --- /dev/null +++ b/builtin/providers/fastly/resource_fastly_service_v1_conditionals_test.go @@ -0,0 +1,122 @@ +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 TestAccFastlyServiceV1_conditional_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)) + + con1 := gofastly.Condition{ + Name: "some amz condition", + Priority: 10, + Type: "REQUEST", + Statement: `req.url ~ "^/yolo/"`, + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckServiceV1Destroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccServiceV1ConditionConfig(name, domainName1), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceV1Exists("fastly_service_v1.foo", &service), + testAccCheckFastlyServiceV1ConditionalAttributes(&service, name, []*gofastly.Condition{&con1}), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "name", name), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "condition.#", "1"), + ), + }, + }, + }) +} + +func testAccCheckFastlyServiceV1ConditionalAttributes(service *gofastly.ServiceDetail, name string, conditions []*gofastly.Condition) 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 + conditionList, err := conn.ListConditions(&gofastly.ListConditionsInput{ + Service: service.ID, + Version: service.ActiveVersion.Number, + }) + + if err != nil { + return fmt.Errorf("[ERR] Error looking up Conditions for (%s), version (%s): %s", service.Name, service.ActiveVersion.Number, err) + } + + if len(conditionList) != len(conditions) { + return fmt.Errorf("Error: mis match count of conditions, expected (%d), got (%d)", len(conditions), len(conditionList)) + } + + var found int + for _, c := range conditions { + for _, lc := range conditionList { + if c.Name == lc.Name { + // we don't know these things ahead of time, so populate them now + c.ServiceID = service.ID + c.Version = service.ActiveVersion.Number + if !reflect.DeepEqual(c, lc) { + return fmt.Errorf("Bad match Conditions match, expected (%#v), got (%#v)", c, lc) + } + found++ + } + } + } + + if found != len(conditions) { + return fmt.Errorf("Error matching Conditions rules") + } + return nil + } +} + +func testAccServiceV1ConditionConfig(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" + } + + header { + destination = "http.x-amz-request-id" + type = "cache" + action = "delete" + name = "remove x-amz-request-id" + } + + condition { + name = "some amz condition" + type = "REQUEST" + + statement = "req.url ~ \"^/yolo/\"" + + priority = 10 + } + + force_destroy = true +}`, name, domain) +} diff --git a/vendor/github.com/sethvargo/go-fastly/version.go b/vendor/github.com/sethvargo/go-fastly/version.go index 8b54c9cee..fd22f9067 100644 --- a/vendor/github.com/sethvargo/go-fastly/version.go +++ b/vendor/github.com/sethvargo/go-fastly/version.go @@ -7,14 +7,14 @@ import ( // Version represents a distinct configuration version. type Version struct { - Number string `mapstructure:"number"` - Comment string `mapstructure:"comment"` - ServiceID string `mapstructure:"service_id"` - Active bool `mapstructure:"active"` - Locked bool `mapstructure:"locked"` - Deployed bool `mapstructure:"deployed"` - Staging bool `mapstructure:"staging"` - Testing bool `mapstructure:"testing"` + Number string `mapstructure:"number"` + Comment string `mapstructure:"comment"` + ServiceID string `mapstructure:"service_id"` + Active bool `mapstructure:"active"` + Locked bool `mapstructure:"locked"` + Deployed bool `mapstructure:"deployed"` + Staging bool `mapstructure:"staging"` + Testing bool `mapstructure:"testing"` } // versionsByNumber is a sortable list of versions. This is used by the version 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 38e047f16..d3f6cd580 100644 --- a/website/source/docs/providers/fastly/r/service_v1.html.markdown +++ b/website/source/docs/providers/fastly/r/service_v1.html.markdown @@ -100,6 +100,8 @@ The following arguments are supported: Service. Defined below * `backend` - (Required) A set of Backends to service requests from your Domains. Defined below +* `condition` - (Optional) A set of conditions to add logic to any basic +configuration object in this service. 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 @@ -135,6 +137,20 @@ Default `200` * `ssl_check_cert` - (Optional) Be strict on checking SSL certs. Default `true` * `weight` - (Optional) The [portion of traffic](https://docs.fastly.com/guides/performance-tuning/load-balancing-configuration.html#how-weight-affects-load-balancing) to send to this Backend. Each Backend receives `weight / total` of the traffic. Default `100` +The `condition` block supports allows you to add logic to any basic configuration +object in a service. See Fastly's documentation +["About Conditions"](https://docs.fastly.com/guides/conditions/about-conditions) +for more detailed information on using Conditions. The Condition `name` can be +used in the `request_condition`, `response_condition`, or +`cache_condition` attributes of other block settings + +* `name` - (Required) A unique name of the condition +* `statement` - (Required) The statement used to determine if the condition is met +* `priority` - (Required) A number used to determine the order in which multiple +conditions execute. Lower numbers execute first +* `type` - (Required) Type of the condition, either `REQUEST` (req), `RESPONSE` +(req, resp), or `CACHE` (req, beresp) + The `gzip` block supports: * `name` - (Required) A unique name