From 441b1cca905d7c30e4592e29c1730d3c89d47904 Mon Sep 17 00:00:00 2001 From: Traver Tischio Date: Tue, 7 Feb 2017 05:34:58 -0500 Subject: [PATCH] provider/fastly Adds healthcheck service (#11709) * Adds schema for fastly healthcheck * Handles changes to the fastly healthcheck * Flattens and refreshed fastly healthchecks * Adds testing for fastly healthcheck * Adds website documentation for fastly healthcheck * Fixes terraform syntax in test examples --- .../fastly/resource_fastly_service_v1.go | 182 ++++++++++++++++ ...urce_fastly_service_v1_healthcheck_test.go | 199 ++++++++++++++++++ .../fastly/r/service_v1.html.markdown | 14 ++ 3 files changed, 395 insertions(+) create mode 100644 builtin/providers/fastly/resource_fastly_service_v1_healthcheck_test.go diff --git a/builtin/providers/fastly/resource_fastly_service_v1.go b/builtin/providers/fastly/resource_fastly_service_v1.go index 75527c71f2fb..b229eb01ac19 100644 --- a/builtin/providers/fastly/resource_fastly_service_v1.go +++ b/builtin/providers/fastly/resource_fastly_service_v1.go @@ -390,6 +390,80 @@ func resourceServiceV1() *schema.Resource { }, }, + "healthcheck": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + // required fields + "name": { + Type: schema.TypeString, + Required: true, + Description: "A name to refer to this healthcheck", + }, + "host": { + Type: schema.TypeString, + Required: true, + Description: "Which host to check", + }, + "path": { + Type: schema.TypeString, + Required: true, + Description: "The path to check", + }, + // optional fields + "check_interval": { + Type: schema.TypeInt, + Optional: true, + Default: 5000, + Description: "How often to run the healthcheck in milliseconds", + }, + "expected_response": { + Type: schema.TypeInt, + Optional: true, + Default: 200, + Description: "The status code expected from the host", + }, + "http_version": { + Type: schema.TypeString, + Optional: true, + Default: "1.1", + Description: "Whether to use version 1.0 or 1.1 HTTP", + }, + "initial": { + Type: schema.TypeInt, + Optional: true, + Default: 2, + Description: "When loading a config, the initial number of probes to be seen as OK", + }, + "method": { + Type: schema.TypeString, + Optional: true, + Default: "HEAD", + Description: "Which HTTP method to use", + }, + "threshold": { + Type: schema.TypeInt, + Optional: true, + Default: 3, + Description: "How many healthchecks must succeed to be considered healthy", + }, + "timeout": { + Type: schema.TypeInt, + Optional: true, + Default: 500, + Description: "Timeout in milliseconds", + }, + "window": { + Type: schema.TypeInt, + Optional: true, + Default: 5, + Description: "The number of most recent healthcheck queries to keep for this healthcheck", + }, + }, + }, + }, + "s3logging": { Type: schema.TypeSet, Optional: true, @@ -654,6 +728,7 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error { "default_ttl", "header", "gzip", + "healthcheck", "s3logging", "papertrail", "condition", @@ -1008,6 +1083,65 @@ func resourceServiceV1Update(d *schema.ResourceData, meta interface{}) error { } } + // find difference in Healthcheck + if d.HasChange("healthcheck") { + oh, nh := d.GetChange("healthcheck") + if oh == nil { + oh = new(schema.Set) + } + if nh == nil { + nh = new(schema.Set) + } + + ohs := oh.(*schema.Set) + nhs := nh.(*schema.Set) + removeHealthCheck := ohs.Difference(nhs).List() + addHealthCheck := nhs.Difference(ohs).List() + + // DELETE old healthcheck configurations + for _, hRaw := range removeHealthCheck { + hf := hRaw.(map[string]interface{}) + opts := gofastly.DeleteHealthCheckInput{ + Service: d.Id(), + Version: latestVersion, + Name: hf["name"].(string), + } + + log.Printf("[DEBUG] Fastly Healthcheck removal opts: %#v", opts) + err := conn.DeleteHealthCheck(&opts) + if err != nil { + return err + } + } + + // POST new/updated Healthcheck + for _, hRaw := range addHealthCheck { + hf := hRaw.(map[string]interface{}) + + opts := gofastly.CreateHealthCheckInput{ + Service: d.Id(), + Version: latestVersion, + Name: hf["name"].(string), + Host: hf["host"].(string), + Path: hf["path"].(string), + CheckInterval: uint(hf["check_interval"].(int)), + ExpectedResponse: uint(hf["expected_response"].(int)), + HTTPVersion: hf["http_version"].(string), + Initial: uint(hf["initial"].(int)), + Method: hf["method"].(string), + Threshold: uint(hf["threshold"].(int)), + Timeout: uint(hf["timeout"].(int)), + Window: uint(hf["window"].(int)), + } + + log.Printf("[DEBUG] Create Healthcheck Opts: %#v", opts) + _, err := conn.CreateHealthCheck(&opts) + if err != nil { + return err + } + } + } + // find difference in s3logging if d.HasChange("s3logging") { os, ns := d.GetChange("s3logging") @@ -1438,6 +1572,23 @@ func resourceServiceV1Read(d *schema.ResourceData, meta interface{}) error { log.Printf("[WARN] Error setting Gzips for (%s): %s", d.Id(), err) } + // refresh Healthcheck + log.Printf("[DEBUG] Refreshing Healthcheck for (%s)", d.Id()) + healthcheckList, err := conn.ListHealthChecks(&gofastly.ListHealthChecksInput{ + Service: d.Id(), + Version: s.ActiveVersion.Number, + }) + + if err != nil { + return fmt.Errorf("[ERR] Error looking up Healthcheck for (%s), version (%s): %s", d.Id(), s.ActiveVersion.Number, err) + } + + hcl := flattenHealthchecks(healthcheckList) + + if err := d.Set("healthcheck", hcl); err != nil { + log.Printf("[WARN] Error setting Healthcheck for (%s): %s", d.Id(), err) + } + // refresh S3 Logging log.Printf("[DEBUG] Refreshing S3 Logging for (%s)", d.Id()) s3List, err := conn.ListS3s(&gofastly.ListS3sInput{ @@ -1805,6 +1956,37 @@ func flattenGzips(gzipsList []*gofastly.Gzip) []map[string]interface{} { return gl } +func flattenHealthchecks(healthcheckList []*gofastly.HealthCheck) []map[string]interface{} { + var hl []map[string]interface{} + for _, h := range healthcheckList { + // Convert HealthChecks to a map for saving to state. + nh := map[string]interface{}{ + "name": h.Name, + "host": h.Host, + "path": h.Path, + "check_interval": h.CheckInterval, + "expected_response": h.ExpectedResponse, + "http_version": h.HTTPVersion, + "initial": h.Initial, + "method": h.Method, + "threshold": h.Threshold, + "timeout": h.Timeout, + "window": h.Window, + } + + // prune any empty values that come from the default string value in structs + for k, v := range nh { + if v == "" { + delete(nh, k) + } + } + + hl = append(hl, nh) + } + + return hl +} + func flattenS3s(s3List []*gofastly.S3) []map[string]interface{} { var sl []map[string]interface{} for _, s := range s3List { diff --git a/builtin/providers/fastly/resource_fastly_service_v1_healthcheck_test.go b/builtin/providers/fastly/resource_fastly_service_v1_healthcheck_test.go new file mode 100644 index 000000000000..04ee8dd256e7 --- /dev/null +++ b/builtin/providers/fastly/resource_fastly_service_v1_healthcheck_test.go @@ -0,0 +1,199 @@ +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_healthcheck_basic(t *testing.T) { + var service gofastly.ServiceDetail + name := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + domainName := fmt.Sprintf("%s.notadomain.com", acctest.RandString(10)) + + log1 := gofastly.HealthCheck{ + Version: "1", + Name: "example-healthcheck1", + Host: "example1.com", + Path: "/test1.txt", + CheckInterval: 4000, + ExpectedResponse: 200, + HTTPVersion: "1.1", + Initial: 2, + Method: "HEAD", + Threshold: 3, + Timeout: 5000, + Window: 5, + } + + log2 := gofastly.HealthCheck{ + Version: "1", + Name: "example-healthcheck2", + Host: "example2.com", + Path: "/test2.txt", + CheckInterval: 4500, + ExpectedResponse: 404, + HTTPVersion: "1.0", + Initial: 1, + Method: "POST", + Threshold: 4, + Timeout: 4000, + Window: 10, + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckServiceV1Destroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccServiceV1HealthCheckConfig(name, domainName), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceV1Exists("fastly_service_v1.foo", &service), + testAccCheckFastlyServiceV1HealthCheckAttributes(&service, []*gofastly.HealthCheck{&log1}), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "name", name), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "healthcheck.#", "1"), + ), + }, + + resource.TestStep{ + Config: testAccServiceV1HealthCheckConfig_update(name, domainName), + Check: resource.ComposeTestCheckFunc( + testAccCheckServiceV1Exists("fastly_service_v1.foo", &service), + testAccCheckFastlyServiceV1HealthCheckAttributes(&service, []*gofastly.HealthCheck{&log1, &log2}), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "name", name), + resource.TestCheckResourceAttr( + "fastly_service_v1.foo", "healthcheck.#", "2"), + ), + }, + }, + }) +} + +func testAccCheckFastlyServiceV1HealthCheckAttributes(service *gofastly.ServiceDetail, healthchecks []*gofastly.HealthCheck) resource.TestCheckFunc { + return func(s *terraform.State) error { + + conn := testAccProvider.Meta().(*FastlyClient).conn + healthcheckList, err := conn.ListHealthChecks(&gofastly.ListHealthChecksInput{ + Service: service.ID, + Version: service.ActiveVersion.Number, + }) + + if err != nil { + return fmt.Errorf("[ERR] Error looking up Healthcheck for (%s), version (%s): %s", service.Name, service.ActiveVersion.Number, err) + } + + if len(healthcheckList) != len(healthchecks) { + return fmt.Errorf("Healthcheck List count mismatch, expected (%d), got (%d)", len(healthchecks), len(healthcheckList)) + } + + var found int + for _, h := range healthchecks { + for _, lh := range healthcheckList { + if h.Name == lh.Name { + // we don't know these things ahead of time, so populate them now + h.ServiceID = service.ID + h.Version = service.ActiveVersion.Number + if !reflect.DeepEqual(h, lh) { + return fmt.Errorf("Bad match Healthcheck match, expected (%#v), got (%#v)", h, lh) + } + found++ + } + } + } + + if found != len(healthchecks) { + return fmt.Errorf("Error matching Healthcheck rules") + } + + return nil + } +} + +func testAccServiceV1HealthCheckConfig(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" + } + + healthcheck { + name = "example-healthcheck1" + host = "example1.com" + path = "/test1.txt" + check_interval = 4000 + expected_response = 200 + http_version = "1.1" + initial = 2 + method = "HEAD" + threshold = 3 + timeout = 5000 + window = 5 + } + + force_destroy = true +}`, name, domain) +} + +func testAccServiceV1HealthCheckConfig_update(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" + } + + healthcheck { + name = "example-healthcheck1" + host = "example1.com" + path = "/test1.txt" + check_interval = 4000 + expected_response = 200 + http_version = "1.1" + initial = 2 + method = "HEAD" + threshold = 3 + timeout = 5000 + window = 5 + } + + healthcheck { + name = "example-healthcheck2" + host = "example2.com" + path = "/test2.txt" + check_interval = 4500 + expected_response = 404 + http_version = "1.0" + initial = 1 + method = "POST" + threshold = 4 + timeout = 4000 + window = 10 + } + + 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 6ecb0c68dd47..d8990bd613ee 100644 --- a/website/source/docs/providers/fastly/r/service_v1.html.markdown +++ b/website/source/docs/providers/fastly/r/service_v1.html.markdown @@ -231,6 +231,20 @@ content. (Does not apply to the `delete` action.) * `substitution` - (Optional) Value to substitute in place of regular expression. (Only applies to the `regex` and `regex_repeat` actions.) * `priority` - (Optional) Lower priorities execute first. Default: `100`. +The `healthcheck` block supports: + +* `name` - (Required) A unique name to identify this Healthcheck. +* `host` - (Required) Address of the host to check. +* `path` - (Required) The path to check. +* `check_interval` - (Optional) How often to run the Healthcheck in milliseconds. Default `5000`. +* `expected_response` - (Optional) The status code expected from the host. Default `200`. +* `http_version` - (Optional) Whether to use version 1.0 or 1.1 HTTP. Default `1.1`. +* `initial` - (Optional) When loading a config, the initial number of probes to be seen as OK. Default `2`. +* `method` - (Optional) Which HTTP method to use. Default `HEAD`. +* `threshold` - (Optional) How many Healthchecks must succeed to be considered healthy. Default `3`. +* `timeout` - (Optional) Timeout in milliseconds. Default `500`. +* `window` - (Optional) The number of most recent Healthcheck queries to keep for this Healthcheck. Default `5`. + The `request_setting` block allow you to customize Fastly's request handling, by defining behavior that should change based on a predefined `condition`: