From c3e40205b9ba6767beed77a11572fda8b92d2a34 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Wed, 17 Jul 2019 15:23:42 -0500 Subject: [PATCH 01/16] add SLO support --- ...esource_datadog_service_level_objective.go | 433 ++++++++++++++++++ ...ce_datadog_service_level_objective_test.go | 199 ++++++++ .../service_level_objectives.html.markdown | 137 ++++++ 3 files changed, 769 insertions(+) create mode 100644 datadog/resource_datadog_service_level_objective.go create mode 100644 datadog/resource_datadog_service_level_objective_test.go create mode 100644 website/docs/r/examples/service_level_objectives.html.markdown diff --git a/datadog/resource_datadog_service_level_objective.go b/datadog/resource_datadog_service_level_objective.go new file mode 100644 index 000000000..4c9aa5f9f --- /dev/null +++ b/datadog/resource_datadog_service_level_objective.go @@ -0,0 +1,433 @@ +package datadog + +import ( + "fmt" + "log" + "sort" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/zorkian/go-datadog-api" +) + +func resourceDatadogServiceLevelObjective() *schema.Resource { + return &schema.Resource{ + Create: resourceDatadogServiceLevelObjectiveCreate, + Read: resourceDatadogServiceLevelObjectiveRead, + Update: resourceDatadogServiceLevelObjectiveUpdate, + Delete: resourceDatadogServiceLevelObjectiveDelete, + Exists: resourceDatadogServiceLevelObjectiveExists, + Importer: &schema.ResourceImporter{ + State: resourceDatadogServiceLevelObjectiveImport, + }, + + Schema: map[string]*schema.Schema{ + // Common + "name": { + Type: schema.TypeString, + Required: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + StateFunc: func(val interface{}) string { + return strings.TrimSpace(val.(string)) + }, + }, + "tags": { + // we use TypeSet to represent tags, paradoxically to be able to maintain them ordered; + // we order them explicitly in the read/create/update methods of this resource and using + // TypeSet makes Terraform ignore differences in order when creating a plan + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "thresholds": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "timeframe": { + Type: schema.TypeString, + Required: true, + }, + "target": { + Type: schema.TypeFloat, + Required: true, + DiffSuppressFunc: suppressDataDogFloatIntDiff, + }, + "target_display": { + Type: schema.TypeString, + Required: false, + DiffSuppressFunc: suppressDataDogFloatIntDiff, + }, + "warning": { + Type: schema.TypeFloat, + Optional: true, + DiffSuppressFunc: suppressDataDogFloatIntDiff, + }, + "warning_display": { + Type: schema.TypeString, + Required: false, + DiffSuppressFunc: suppressDataDogFloatIntDiff, + }, + }, + }, + }, + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: ValidateServiceLevelObjectiveTypeString, + }, + + // Metric-Based SLO + "query": { + Type: schema.TypeMap, + Optional: true, + ConflictsWith: []string{"monitor_ids", "monitor_search"}, + Description: "The metric query of good / total events", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "numerator": { + Type: schema.TypeString, + Required: true, + StateFunc: func(val interface{}) string { + return strings.TrimSpace(val.(string)) + }, + }, + "denominator": { + Type: schema.TypeString, + Required: true, + StateFunc: func(val interface{}) string { + return strings.TrimSpace(val.(string)) + }, + }, + }, + }, + }, + + // Monitor-Based SLO + "monitor_ids": { + Type: schema.TypeSet, + Optional: true, + ConflictsWith: []string{"query", "monitor_search"}, + Description: "A static set of monitor IDs to use as part of the SLO", + Elem: &schema.Schema{Type: schema.TypeInt, MinItems: 1}, + }, + "monitor_search": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"query", "monitor_ids"}, + Description: "A dynamic search on creation for the SLO", + }, + "groups": { + Type: schema.TypeSet, + Optional: true, + Description: "A static set of groups to filter monitor-based SLOs", + ConflictsWith: []string{"query"}, + Elem: &schema.Schema{Type: schema.TypeString, MinItems: 1}, + }, + }, + } +} + +// ValidateServiceLevelObjectiveTypeString is a ValidateFunc that ensures the SLO is of one of the supported types +func ValidateServiceLevelObjectiveTypeString(v interface{}, k string) (ws []string, errors []error) { + switch v.(string) { + case datadog.ServiceLevelObjectiveTypeMonitor: + break + case datadog.ServiceLevelObjectiveTypeMetric: + break + default: + errors = append(errors, fmt.Errorf("invalid type %s specified for SLO", v.(string))) + } + return +} + +func buildServiceLevelObjectiveStruct(d *schema.ResourceData) *datadog.ServiceLevelObjective { + + slo := datadog.ServiceLevelObjective{ + Type: datadog.String(d.Get("type").(string)), + Name: datadog.String(d.Get("name").(string)), + Description: datadog.String(d.Get("description").(string)), + } + + if attr, ok := d.GetOk("tags"); ok { + tags := make([]string, 0) + for _, s := range attr.(*schema.Set).List() { + tags = append(tags, s.(string)) + } + // sort to make them determinate + if len(tags) > 0 { + sort.Strings(tags) + slo.Tags = tags + } + } + + if attr, ok := d.GetOk("thresholds"); ok { + sloThresholds := make(datadog.ServiceLevelObjectiveThresholds, 0) + for _, rawThreshold := range attr.([]interface{}) { + threshold := rawThreshold.(map[string]interface{}) + t := datadog.ServiceLevelObjectiveThreshold{} + if tf, ok := threshold["timeframe"]; ok { + t.TimeFrame = datadog.String(tf.(string)) + } + + if targetValue, ok := threshold["target"]; ok { + t.Target = datadog.Float64(targetValue.(float64)) + } + + if warningValue, ok := threshold["warning"]; ok { + t.Warning = datadog.Float64(warningValue.(float64)) + } + + if targetDisplayValue, ok := threshold["target_display"]; ok { + t.TargetDisplay = datadog.String(targetDisplayValue.(string)) + } + + if warningDisplayValue, ok := threshold["warning_display"]; ok { + t.WarningDisplay = datadog.String(warningDisplayValue.(string)) + } + + sloThresholds = append(sloThresholds, &t) + } + sort.Sort(sloThresholds) + slo.Thresholds = sloThresholds + } + + switch d.Get("type").(string) { + case datadog.ServiceLevelObjectiveTypeMonitor: + // add monitor components + if attr, ok := d.GetOk("monitor_ids"); ok { + monitorIDs := make([]int, 0) + for _, s := range attr.(*schema.Set).List() { + monitorIDs = append(monitorIDs, s.(int)) + } + if len(monitorIDs) > 0 { + sort.Ints(monitorIDs) + slo.MonitorIDs = monitorIDs + } + } + if attr, ok := d.GetOk("monitor_search"); ok { + if len(attr.(string)) > 0 { + slo.MonitorSearch = datadog.String(attr.(string)) + } + } + if attr, ok := d.GetOk("groups"); ok { + groups := make([]string, 0) + for _, s := range attr.(*schema.Set).List() { + groups = append(groups, s.(string)) + } + if len(groups) > 0 { + sort.Strings(groups) + slo.Groups = groups + } + } + default: + // query type + slo.Query = &datadog.ServiceLevelObjectiveMetricQuery{ + Numerator: datadog.String(d.Get("query.numerator").(string)), + Denominator: datadog.String(d.Get("query.denominator").(string)), + } + } + + return &slo +} + +func resourceDatadogServiceLevelObjectiveCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*datadog.Client) + + slo := buildServiceLevelObjectiveStruct(d) + slo, err := client.CreateServiceLevelObjective(slo) + if err != nil { + return fmt.Errorf("error creating service level objective: %s", err.Error()) + } + + d.SetId(slo.GetID()) + + return resourceDatadogServiceLevelObjectiveRead(d, meta) +} + +func resourceDatadogServiceLevelObjectiveExists(d *schema.ResourceData, meta interface{}) (b bool, e error) { + // Exists - This is called to verify a resource still exists. It is called prior to Read, + // and lowers the burden of Read to be able to assume the resource exists. + client := meta.(*datadog.Client) + + if _, err := client.GetServiceLevelObjective(d.Id()); err != nil { + errStr := strings.ToLower(err.Error()) + if strings.Contains(errStr, "not found") || strings.Contains(errStr, "no slo specified") { + return false, nil + } + return false, err + } + + return true, nil +} + +func resourceDatadogServiceLevelObjectiveRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*datadog.Client) + + slo, err := client.GetServiceLevelObjective(d.Id()) + if err != nil { + return err + } + + thresholds := make([]map[string]interface{}, 0) + sort.Sort(slo.Thresholds) + for _, threshold := range slo.Thresholds { + t := map[string]interface{}{ + "timeframe": threshold.GetTimeFrame(), + "target": threshold.GetTarget(), + "warning": threshold.GetWarning(), + } + if targetDisplay, ok := threshold.GetTargetDisplayOk(); ok { + t["target_display"] = targetDisplay + } + if warningDisplay, ok := threshold.GetWarningDisplayOk(); ok { + t["warning_display"] = warningDisplay + } + thresholds = append(thresholds, t) + } + + tags := make([]string, 0) + for _, s := range slo.Tags { + tags = append(tags, s) + } + sort.Strings(tags) + + log.Printf("[DEBUG] service level objective: %+v", slo) + d.Set("name", slo.GetName()) + d.Set("description", slo.GetDescription()) + d.Set("type", slo.GetType()) + d.Set("tags", tags) + d.Set("thresholds", thresholds) + switch slo.GetType() { + case datadog.ServiceLevelObjectiveTypeMonitor: + // monitor type + if len(slo.MonitorIDs) > 0 { + sort.Ints(slo.MonitorIDs) + d.Set("monitor_ids", slo.MonitorIDs) + } + if ms, ok := slo.GetMonitorSearchOk(); ok { + d.Set("monitor_search", ms) + } + sort.Strings(slo.Groups) + d.Set("groups", slo.Groups) + default: + // metric type + query := make(map[string]interface{}) + q := slo.GetQuery() + query["numerator"] = q.GetNumerator() + query["denominator"] = q.GetDenominator() + d.Set("query", query) + } + + return nil +} + +func resourceDatadogServiceLevelObjectiveUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*datadog.Client) + slo := &datadog.ServiceLevelObjective{ + ID: datadog.String(d.Id()), + } + + if attr, ok := d.GetOk("name"); ok { + slo.SetName(attr.(string)) + } + + if attr, ok := d.GetOk("description"); ok { + slo.SetDescription(attr.(string)) + } + + if attr, ok := d.GetOk("type"); ok { + slo.SetType(attr.(string)) + } + + switch slo.GetType() { + case datadog.ServiceLevelObjectiveTypeMonitor: + // monitor type + if attr, ok := d.GetOk("monitor_ids"); ok { + s := make([]int, 0) + for _, v := range attr.(*schema.Set).List() { + s = append(s, v.(int)) + } + sort.Ints(s) + slo.MonitorIDs = s + } + if attr, ok := d.GetOk("monitor_search"); ok { + slo.SetMonitorSearch(attr.(string)) + } + if attr, ok := d.GetOk("groups"); ok { + s := make([]string, 0) + for _, v := range attr.(*schema.Set).List() { + s = append(s, v.(string)) + } + sort.Strings(s) + slo.Groups = s + } + default: + // metric type + if attr, ok := d.GetOk("query"); ok { + query := attr.(map[string]interface{}) + slo.SetQuery(datadog.ServiceLevelObjectiveMetricQuery{ + Numerator: datadog.String(query["numerator"].(string)), + Denominator: datadog.String(query["denominator"].(string)), + }) + } + } + + if attr, ok := d.GetOk("tags"); ok { + s := make([]string, 0) + for _, v := range attr.(*schema.Set).List() { + s = append(s, v.(string)) + } + sort.Strings(s) + slo.Tags = s + } + + if attr, ok := d.GetOk("thresholds"); ok { + sloThresholds := make(datadog.ServiceLevelObjectiveThresholds, 0) + thresholds := attr.([]map[string]interface{}) + for _, threshold := range thresholds { + t := datadog.ServiceLevelObjectiveThreshold{ + TimeFrame: datadog.String(threshold["timeframe"].(string)), + Target: datadog.Float64(threshold["target"].(float64)), + } + if warningValueRaw, ok := threshold["warning"]; ok { + t.Warning = datadog.Float64(warningValueRaw.(float64)) + } + // display settings + if targetDisplay, ok := threshold["target_display"]; ok { + t.TargetDisplay = datadog.String(targetDisplay.(string)) + } + if warningDisplay, ok := threshold["warning_display"]; ok { + t.WarningDisplay = datadog.String(warningDisplay.(string)) + } + sloThresholds = append(sloThresholds, &t) + } + if len(sloThresholds) > 0 { + sort.Sort(sloThresholds) + slo.Thresholds = sloThresholds + } + } + + if _, err := client.UpdateServiceLevelObjective(slo); err != nil { + return fmt.Errorf("error updating service level objective: %s", err.Error()) + } + + return resourceDatadogServiceLevelObjectiveRead(d, meta) +} + +func resourceDatadogServiceLevelObjectiveDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*datadog.Client) + + return client.DeleteServiceLevelObjective(d.Id()) +} + +func resourceDatadogServiceLevelObjectiveImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + if err := resourceDatadogServiceLevelObjectiveRead(d, meta); err != nil { + return nil, err + } + return []*schema.ResourceData{d}, nil +} diff --git a/datadog/resource_datadog_service_level_objective_test.go b/datadog/resource_datadog_service_level_objective_test.go new file mode 100644 index 000000000..92ff9dda9 --- /dev/null +++ b/datadog/resource_datadog_service_level_objective_test.go @@ -0,0 +1,199 @@ +package datadog + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/zorkian/go-datadog-api" +) + +// config +const testAccCheckDatadogServiceLevelObjectiveConfig = ` +resource "datadog_service_level_objective" "foo" { + name = "name for metric SLO foo" + type = "metric" + description = "some description about foo SLO" + query = { + numerator = "sum:my.metric{type:good}.as_count()" + denominator = "sum:my.metric{*}.as_count()" + } + + thresholds = [ + { + timeframe = "7d" + slo = 99.5 + warning = 99.8 + }, + { + timeframe = "30d" + slo = 99 + } + ] + + tags = ["foo:bar", "baz"] +} +` + +const testAccCheckDatadogServiceLevelObjectiveConfigUpdated = ` +resource "datadog_service_level_objective" "foo" { + name = "updated name for metric SLO foo" + type = "metric" + description = "some updated description about foo SLO" + query = { + numerator = "sum:my.metric{type:good}.as_count()" + denominator = "sum:my.metric{type:good}.as_count() + sum:my.metric{type:bad}.as_count()" + } + + thresholds = [ + { + timeframe = "7d" + slo = 99.5 + warning = 99.8 + }, + { + timeframe = "30d" + slo = 98 + } + ] + + tags = ["foo:bar", "baz"] +} +` + +// tests + +func TestAccDatadogServiceLevelObjective_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDatadogServiceLevelObjectiveDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckDatadogServiceLevelObjectiveConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckDatadogServiceLevelObjectiveExists("datadog_service_level_objective.foo"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "name", "name for metric SLO foo"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "description", "some description about foo SLO"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "type", "metric"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "query.numerator", "sum:my.metric{type:good}.as_count()"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "query.denominator", "sum:my.metric{*}.as_count()"), + // Thresholds are a TypeList, that are sorted by timeframe alphabetically. + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.#", "2"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.0.timeframe", "7d"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.0.slo", "99.5"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.0.warning", "99.8"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.1.timeframe", "30d"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.1.slo", "99"), + // Tags are a TypeSet => use a weird way to access members by their hash + // TF TypeSet is internally represented as a map that maps computed hashes + // to actual values. Since the hashes are always the same for one value, + // this is the way to get them. + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "tags.#", "2"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "tags.2644851163", "baz"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "tags.1750285118", "foo:bar"), + ), + }, + { + Config: testAccCheckDatadogServiceLevelObjectiveConfigUpdated, + Check: resource.ComposeTestCheckFunc( + testAccCheckDatadogServiceLevelObjectiveExists("datadog_service_level_objective.foo"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "name", "updated name for metric SLO foo"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "description", "some updated description about foo SLO"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "type", "metric"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "query.numerator", "sum:my.metric{type:good}.as_count()"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "query.denominator", "sum:my.metric{type:good}.as_count() + sum:my.metric{type:bad}.as_count()"), + // Thresholds are a TypeList, that are sorted by timeframe alphabetically. + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.#", "2"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.0.timeframe", "7d"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.0.slo", "99.5"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.0.warning", "99.8"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.1.timeframe", "30d"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "thresholds.1.slo", "98"), + // Tags are a TypeSet => use a weird way to access members by their hash + // TF TypeSet is internally represented as a map that maps computed hashes + // to actual values. Since the hashes are always the same for one value, + // this is the way to get them. + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "tags.#", "2"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "tags.2644851163", "baz"), + resource.TestCheckResourceAttr( + "datadog_service_level_objective.foo", "tags.1750285118", "foo:bar"), + ), + }, + }, + }) +} + +// helpers + +func destroyServiceLevelObjectiveHelper(s *terraform.State, client *datadog.Client) error { + for _, r := range s.RootModule().Resources { + if r.Primary.ID != "" { + if _, err := client.GetServiceLevelObjective(r.Primary.ID); err != nil { + if strings.Contains(strings.ToLower(err.Error()), "not found") { + continue + } + return fmt.Errorf("Received an error retrieving service level objective %s", err) + } + return fmt.Errorf("Service Level Objective still exists") + } + } + return nil +} + +func existsServiceLevelObjectiveHelper(s *terraform.State, client *datadog.Client) error { + for _, r := range s.RootModule().Resources { + if _, err := client.GetServiceLevelObjective(r.Primary.ID); err != nil { + return fmt.Errorf("Received an error retrieving service level objective %s", err) + } + } + return nil +} + +func testAccCheckDatadogServiceLevelObjectiveDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*datadog.Client) + + if err := destroyServiceLevelObjectiveHelper(s, client); err != nil { + return err + } + return nil +} + +func testAccCheckDatadogServiceLevelObjectiveExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*datadog.Client) + if err := existsHelper(s, client); err != nil { + return err + } + return nil + } +} diff --git a/website/docs/r/examples/service_level_objectives.html.markdown b/website/docs/r/examples/service_level_objectives.html.markdown new file mode 100644 index 000000000..1471c6c60 --- /dev/null +++ b/website/docs/r/examples/service_level_objectives.html.markdown @@ -0,0 +1,137 @@ +--- +layout: "datadog" +page_title: "Datadog: datadog_service_level_objective" +sidebar_current: "docs-datadog-resource-service-level-objective" +description: |- + Provides a Datadog service level objective resource. This can be used to create and manage service level objectives. +--- + +# datadog_service_level_objective + +Provides a Datadog service level objective resource. This can be used to create and manage Datadog service level objectives. + +## Example Usage + +### Metric-Based SLO +```hcl +# Create a new Datadog service level objective +resource "datadog_service_level_objective" "foo" { + name = "Name for SLO foo" + type = "metric" + description = "My custom metric SLO" + query = { + numerator = "sum:my.custom.count.metric{type:good_events}.as_count()" + denominator = "sum:my.custom.count.metric{*}.as_count()" + } + + thresholds = [ + { + timeframe = "7d" + target = 99.9 + warning = 99.99 + target_display = "99.900" + warning_display = "99.990" + }, + { + timeframe = "30d" + target = 99.9 + warning = 99.99 + target_display = "99.900" + warning_display = "99.990" + } + ] + + tags = ["foo:bar", "baz"] +} +``` + +### Monitor-Based SLO +```hcl +# Create a new Datadog service level objective +resource "datadog_service_level_objective" "foo" { + name = "Name for SLO foo" + type = "monitor" + description = "My custom monitor SLO" + monitor_ids = [1, 2, 3] + + thresholds = [ + { + timeframe = "7d" + target = 99.9 + warning = 99.99 + }, + { + timeframe = "30d" + target = 99.9 + warning = 99.99 + } + ] + + tags = ["foo:bar", "baz"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `type` - (Required) The type of the service level objective. The mapping from these types to the types found in the Datadog Web UI can be found in the Datadog API [documentation](https://docs.datadoghq.com/api/?lang=python#create-a-service-level-objective) page. Available options to choose from are: + * `metric` + * `monitor` +* `name` - (Required) Name of Datadog service level objective +* `description` - (Optional) A description of this service level objective. +* `tags` (Optional) A list of tags to associate with your service level objective. This can help you categorize and filter service level objectives in the service level objectives page of the UI. Note: it's not currently possible to filter by these tags when querying via the API +* `thresholds` - (Required) - A list of thresholds and targets that define the service level objectives from the provided SLIs. + * `timeframe` (Required) - the time frame for the objective. The mapping from these types to the types found in the Datadog Web UI can be found in the Datadog API [documentation](https://docs.datadoghq.com/api/?lang=python#create-a-service-level-objective) page. Available options to choose from are: + * `7d` + * `30d` + * `90d` + * `target` - (Required) the objective's target `[0,100]` + * `target_display` - (Optional) the string version to specify additional digits in the case of `99` but want 3 digits like `99.000` to display. + * `warning` - (Optional) the objective's warning value `[0,100]`. This must be `> target` value. + * `warning_display` - (Optional) the string version to specify additional digits in the case of `99` but want 3 digits like `99.000` to display. + * Example Usage: + ```hcl + thresholds = [ + { + timeframe = "7d" + target = 99.9 + warning = 99.95 + }, + { + timeframe = "30d" + target = 99.9 + warning = 99.95 + } + ] + ``` +* `metric` type SLOs: + * `query` - (Required) The metric query configuration to use for the SLI. This is a dictionary and requires both the `numerator` and `denominator` fields which should be `count` metrics using the `sum` aggregator. + * `numerator` - (Required) the sum of all the `good` events + * `denominator` - (Required) the sum of the `total` events + * Example Usage: + ```hcl + query = { + numerator = "sum:my.custom.count.metric{type:good}.as_count()" + denominator = "sum:my.custom.count.metric{*}.as_count()" + } + ``` +* `monitor` type SLOs: + * `monitor_ids` - (Optional) A list of numeric monitor IDs for which to use as SLIs. Their tags will be auto-imported into `monitor_tags` field in the API resource. At least 1 of `monitor_ids` or `monitor_search` must be provided. + * `monitor_search` - (Optional) The monitor query search used on the monitor search API to add monitor_ids by searching. Their tags will be auto-imported into `monitor_tags` field in the API resource. At least 1 of `monitor_ids` or `monitor_search` must be provided. + * `groups` - (Optional) A custom set of groups from the monitor(s) for which to use as the SLI instead of all the groups. + + +## Attributes Reference + +The following attributes are exported: + +* `id` - ID of the Datadog service level objective + +## Import + +Service Level Objectives can be imported using their string ID, e.g. + +``` +$ terraform import datadog_service_level_objective.bytes_received_localhost "foo" +``` From 7f78d600d255aa32315a2afebd2814ddc4c077d4 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Fri, 9 Aug 2019 13:13:04 -0500 Subject: [PATCH 02/16] fix docs --- datadog/provider.go | 1 + website/datadog.erb | 3 +++ ...ves.html.markdown => service_level_objective.html.markdown} | 0 3 files changed, 4 insertions(+) rename website/docs/r/{examples/service_level_objectives.html.markdown => service_level_objective.html.markdown} (100%) diff --git a/datadog/provider.go b/datadog/provider.go index ebb932286..c8a2ebec3 100644 --- a/datadog/provider.go +++ b/datadog/provider.go @@ -44,6 +44,7 @@ func Provider() terraform.ResourceProvider { "datadog_integration_aws": resourceDatadogIntegrationAws(), "datadog_integration_pagerduty": resourceDatadogIntegrationPagerduty(), "datadog_integration_pagerduty_service_object": resourceDatadogIntegrationPagerdutySO(), + "datadog_service_level_objective": resourceDatadogServiceLevelObjective(), }, ConfigureFunc: providerConfigure, diff --git a/website/datadog.erb b/website/datadog.erb index 9624ad53a..a3a2d405e 100644 --- a/website/datadog.erb +++ b/website/datadog.erb @@ -40,6 +40,9 @@ > datadog_screenboard + > + datadog_service_level_objective + > datadog_synthetics diff --git a/website/docs/r/examples/service_level_objectives.html.markdown b/website/docs/r/service_level_objective.html.markdown similarity index 100% rename from website/docs/r/examples/service_level_objectives.html.markdown rename to website/docs/r/service_level_objective.html.markdown From 5db3d0e3ef4b92c91a52d88776e3c8ec62ff4ff5 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Fri, 9 Aug 2019 13:13:51 -0500 Subject: [PATCH 03/16] fix docs --- website/docs/r/service_level_objective.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/r/service_level_objective.html.markdown b/website/docs/r/service_level_objective.html.markdown index 1471c6c60..f884e15a9 100644 --- a/website/docs/r/service_level_objective.html.markdown +++ b/website/docs/r/service_level_objective.html.markdown @@ -1,7 +1,7 @@ --- layout: "datadog" page_title: "Datadog: datadog_service_level_objective" -sidebar_current: "docs-datadog-resource-service-level-objective" +sidebar_current: "docs-datadog-resource-service_level_objective" description: |- Provides a Datadog service level objective resource. This can be used to create and manage service level objectives. --- From ebc3e2c32bb9f9dec369adbd36d9badf27b457a0 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Fri, 9 Aug 2019 13:16:51 -0500 Subject: [PATCH 04/16] small tweaks to formatting --- .../r/service_level_objective.html.markdown | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/website/docs/r/service_level_objective.html.markdown b/website/docs/r/service_level_objective.html.markdown index f884e15a9..e4e2dd354 100644 --- a/website/docs/r/service_level_objective.html.markdown +++ b/website/docs/r/service_level_objective.html.markdown @@ -91,31 +91,31 @@ The following arguments are supported: * `warning` - (Optional) the objective's warning value `[0,100]`. This must be `> target` value. * `warning_display` - (Optional) the string version to specify additional digits in the case of `99` but want 3 digits like `99.000` to display. * Example Usage: - ```hcl - thresholds = [ - { - timeframe = "7d" - target = 99.9 - warning = 99.95 - }, - { - timeframe = "30d" - target = 99.9 - warning = 99.95 - } - ] - ``` +```hcl +thresholds = [ + { + timeframe = "7d" + target = 99.9 + warning = 99.95 + }, + { + timeframe = "30d" + target = 99.9 + warning = 99.95 + } +] +``` * `metric` type SLOs: * `query` - (Required) The metric query configuration to use for the SLI. This is a dictionary and requires both the `numerator` and `denominator` fields which should be `count` metrics using the `sum` aggregator. * `numerator` - (Required) the sum of all the `good` events * `denominator` - (Required) the sum of the `total` events * Example Usage: - ```hcl - query = { - numerator = "sum:my.custom.count.metric{type:good}.as_count()" - denominator = "sum:my.custom.count.metric{*}.as_count()" - } - ``` +```hcl +query = { + numerator = "sum:my.custom.count.metric{type:good}.as_count()" + denominator = "sum:my.custom.count.metric{*}.as_count()" +} +``` * `monitor` type SLOs: * `monitor_ids` - (Optional) A list of numeric monitor IDs for which to use as SLIs. Their tags will be auto-imported into `monitor_tags` field in the API resource. At least 1 of `monitor_ids` or `monitor_search` must be provided. * `monitor_search` - (Optional) The monitor query search used on the monitor search API to add monitor_ids by searching. Their tags will be auto-imported into `monitor_tags` field in the API resource. At least 1 of `monitor_ids` or `monitor_search` must be provided. From c4c630b1fe98768ddbe31c59ef0cf6237b9b1b6d Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Fri, 9 Aug 2019 14:24:06 -0500 Subject: [PATCH 05/16] PR review fixes --- ...esource_datadog_service_level_objective.go | 52 +++++++++++++++---- .../r/service_level_objective.html.markdown | 23 ++------ 2 files changed, 46 insertions(+), 29 deletions(-) diff --git a/datadog/resource_datadog_service_level_objective.go b/datadog/resource_datadog_service_level_objective.go index 4c9aa5f9f..2ccf43a18 100644 --- a/datadog/resource_datadog_service_level_objective.go +++ b/datadog/resource_datadog_service_level_objective.go @@ -2,8 +2,8 @@ package datadog import ( "fmt" - "log" "sort" + "strconv" "strings" "github.com/hashicorp/terraform/helper/schema" @@ -85,7 +85,7 @@ func resourceDatadogServiceLevelObjective() *schema.Resource { "query": { Type: schema.TypeMap, Optional: true, - ConflictsWith: []string{"monitor_ids", "monitor_search"}, + ConflictsWith: []string{"monitor_ids", "monitor_search", "groups"}, Description: "The metric query of good / total events", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -148,9 +148,12 @@ func ValidateServiceLevelObjectiveTypeString(v interface{}, k string) (ws []stri func buildServiceLevelObjectiveStruct(d *schema.ResourceData) *datadog.ServiceLevelObjective { slo := datadog.ServiceLevelObjective{ - Type: datadog.String(d.Get("type").(string)), - Name: datadog.String(d.Get("name").(string)), - Description: datadog.String(d.Get("description").(string)), + Type: datadog.String(d.Get("type").(string)), + Name: datadog.String(d.Get("name").(string)), + } + + if attr, ok := d.GetOk("description"); ok { + slo.Description = datadog.String(attr.(string)) } if attr, ok := d.GetOk("tags"); ok { @@ -175,19 +178,27 @@ func buildServiceLevelObjectiveStruct(d *schema.ResourceData) *datadog.ServiceLe } if targetValue, ok := threshold["target"]; ok { - t.Target = datadog.Float64(targetValue.(float64)) + if f, ok := floatOk(targetValue); ok { + t.Target = datadog.Float64(f) + } } if warningValue, ok := threshold["warning"]; ok { - t.Warning = datadog.Float64(warningValue.(float64)) + if f, ok := floatOk(warningValue); ok { + t.Warning = datadog.Float64(f) + } } if targetDisplayValue, ok := threshold["target_display"]; ok { - t.TargetDisplay = datadog.String(targetDisplayValue.(string)) + if s, ok := targetDisplayValue.(string); ok && strings.TrimSpace(s) != "" { + t.TargetDisplay = datadog.String(strings.TrimSpace(targetDisplayValue.(string))) + } } if warningDisplayValue, ok := threshold["warning_display"]; ok { - t.WarningDisplay = datadog.String(warningDisplayValue.(string)) + if s, ok := warningDisplayValue.(string); ok && strings.TrimSpace(s) != "" { + t.WarningDisplay = datadog.String(strings.TrimSpace(warningDisplayValue.(string))) + } } sloThresholds = append(sloThresholds, &t) @@ -235,6 +246,28 @@ func buildServiceLevelObjectiveStruct(d *schema.ResourceData) *datadog.ServiceLe return &slo } +func floatOk(val interface{}) (float64, bool) { + switch val.(type) { + case float64: + return val.(float64), true + case *float64: + return *(val.(*float64)), true + case string: + f, err := strconv.ParseFloat(val.(string), 64) + if err == nil { + return f, true + } + case *string: + f, err := strconv.ParseFloat(*(val.(*string)), 64) + if err == nil { + return f, true + } + default: + return 0, false + } + return 0, false +} + func resourceDatadogServiceLevelObjectiveCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*datadog.Client) @@ -296,7 +329,6 @@ func resourceDatadogServiceLevelObjectiveRead(d *schema.ResourceData, meta inter } sort.Strings(tags) - log.Printf("[DEBUG] service level objective: %+v", slo) d.Set("name", slo.GetName()) d.Set("description", slo.GetDescription()) d.Set("type", slo.GetType()) diff --git a/website/docs/r/service_level_objective.html.markdown b/website/docs/r/service_level_objective.html.markdown index e4e2dd354..f60f1fa82 100644 --- a/website/docs/r/service_level_objective.html.markdown +++ b/website/docs/r/service_level_objective.html.markdown @@ -16,7 +16,7 @@ Provides a Datadog service level objective resource. This can be used to create ```hcl # Create a new Datadog service level objective resource "datadog_service_level_objective" "foo" { - name = "Name for SLO foo" + name = "Example Metric SLO" type = "metric" description = "My custom metric SLO" query = { @@ -48,8 +48,8 @@ resource "datadog_service_level_objective" "foo" { ### Monitor-Based SLO ```hcl # Create a new Datadog service level objective -resource "datadog_service_level_objective" "foo" { - name = "Name for SLO foo" +resource "datadog_service_level_objective" "bar" { + name = "Example Monitor SLO" type = "monitor" description = "My custom monitor SLO" monitor_ids = [1, 2, 3] @@ -90,21 +90,6 @@ The following arguments are supported: * `target_display` - (Optional) the string version to specify additional digits in the case of `99` but want 3 digits like `99.000` to display. * `warning` - (Optional) the objective's warning value `[0,100]`. This must be `> target` value. * `warning_display` - (Optional) the string version to specify additional digits in the case of `99` but want 3 digits like `99.000` to display. - * Example Usage: -```hcl -thresholds = [ - { - timeframe = "7d" - target = 99.9 - warning = 99.95 - }, - { - timeframe = "30d" - target = 99.9 - warning = 99.95 - } -] -``` * `metric` type SLOs: * `query` - (Required) The metric query configuration to use for the SLI. This is a dictionary and requires both the `numerator` and `denominator` fields which should be `count` metrics using the `sum` aggregator. * `numerator` - (Required) the sum of all the `good` events @@ -133,5 +118,5 @@ The following attributes are exported: Service Level Objectives can be imported using their string ID, e.g. ``` -$ terraform import datadog_service_level_objective.bytes_received_localhost "foo" +$ terraform import datadog_service_level_objective.12345678901234567890123456789012 "baz" ``` From 2c4264be61ed1af535ab36465eeee3bd11ce5b43 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Fri, 9 Aug 2019 14:33:15 -0500 Subject: [PATCH 06/16] add clarifying sentence --- website/docs/r/service_level_objective.html.markdown | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/docs/r/service_level_objective.html.markdown b/website/docs/r/service_level_objective.html.markdown index f60f1fa82..c1d49d6fb 100644 --- a/website/docs/r/service_level_objective.html.markdown +++ b/website/docs/r/service_level_objective.html.markdown @@ -90,6 +90,8 @@ The following arguments are supported: * `target_display` - (Optional) the string version to specify additional digits in the case of `99` but want 3 digits like `99.000` to display. * `warning` - (Optional) the objective's warning value `[0,100]`. This must be `> target` value. * `warning_display` - (Optional) the string version to specify additional digits in the case of `99` but want 3 digits like `99.000` to display. + +The following options are specific to the `type` of service level objective: * `metric` type SLOs: * `query` - (Required) The metric query configuration to use for the SLI. This is a dictionary and requires both the `numerator` and `denominator` fields which should be `count` metrics using the `sum` aggregator. * `numerator` - (Required) the sum of all the `good` events From 3444a4c61f72e207ef3306aaef1632b484ab31d2 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Fri, 9 Aug 2019 14:36:02 -0500 Subject: [PATCH 07/16] fix test --- ...ource_datadog_service_level_objective_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/datadog/resource_datadog_service_level_objective_test.go b/datadog/resource_datadog_service_level_objective_test.go index 92ff9dda9..eb383a81c 100644 --- a/datadog/resource_datadog_service_level_objective_test.go +++ b/datadog/resource_datadog_service_level_objective_test.go @@ -24,12 +24,12 @@ resource "datadog_service_level_objective" "foo" { thresholds = [ { timeframe = "7d" - slo = 99.5 + target = 99.5 warning = 99.8 }, { timeframe = "30d" - slo = 99 + target = 99 } ] @@ -50,12 +50,12 @@ resource "datadog_service_level_objective" "foo" { thresholds = [ { timeframe = "7d" - slo = 99.5 + target = 99.5 warning = 99.8 }, { timeframe = "30d" - slo = 98 + target = 98 } ] @@ -91,13 +91,13 @@ func TestAccDatadogServiceLevelObjective_Basic(t *testing.T) { resource.TestCheckResourceAttr( "datadog_service_level_objective.foo", "thresholds.0.timeframe", "7d"), resource.TestCheckResourceAttr( - "datadog_service_level_objective.foo", "thresholds.0.slo", "99.5"), + "datadog_service_level_objective.foo", "thresholds.0.target", "99.5"), resource.TestCheckResourceAttr( "datadog_service_level_objective.foo", "thresholds.0.warning", "99.8"), resource.TestCheckResourceAttr( "datadog_service_level_objective.foo", "thresholds.1.timeframe", "30d"), resource.TestCheckResourceAttr( - "datadog_service_level_objective.foo", "thresholds.1.slo", "99"), + "datadog_service_level_objective.foo", "thresholds.1.target", "99"), // Tags are a TypeSet => use a weird way to access members by their hash // TF TypeSet is internally represented as a map that maps computed hashes // to actual values. Since the hashes are always the same for one value, @@ -130,13 +130,13 @@ func TestAccDatadogServiceLevelObjective_Basic(t *testing.T) { resource.TestCheckResourceAttr( "datadog_service_level_objective.foo", "thresholds.0.timeframe", "7d"), resource.TestCheckResourceAttr( - "datadog_service_level_objective.foo", "thresholds.0.slo", "99.5"), + "datadog_service_level_objective.foo", "thresholds.0.target", "99.5"), resource.TestCheckResourceAttr( "datadog_service_level_objective.foo", "thresholds.0.warning", "99.8"), resource.TestCheckResourceAttr( "datadog_service_level_objective.foo", "thresholds.1.timeframe", "30d"), resource.TestCheckResourceAttr( - "datadog_service_level_objective.foo", "thresholds.1.slo", "98"), + "datadog_service_level_objective.foo", "thresholds.1.target", "98"), // Tags are a TypeSet => use a weird way to access members by their hash // TF TypeSet is internally represented as a map that maps computed hashes // to actual values. Since the hashes are always the same for one value, From 354e7219fa7bea696ebe4fea732d024243b9db58 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Fri, 9 Aug 2019 14:42:02 -0500 Subject: [PATCH 08/16] fix provider to Optional fields make syntax backwards compatible suppress *_display changes --- ...esource_datadog_service_level_objective.go | 45 +++++++++++----- ...ce_datadog_service_level_objective_test.go | 42 +++++++-------- .../r/service_level_objective.html.markdown | 53 +++++++++---------- 3 files changed, 78 insertions(+), 62 deletions(-) diff --git a/datadog/resource_datadog_service_level_objective.go b/datadog/resource_datadog_service_level_objective.go index 2ccf43a18..4b81f6093 100644 --- a/datadog/resource_datadog_service_level_objective.go +++ b/datadog/resource_datadog_service_level_objective.go @@ -58,8 +58,8 @@ func resourceDatadogServiceLevelObjective() *schema.Resource { }, "target_display": { Type: schema.TypeString, - Required: false, - DiffSuppressFunc: suppressDataDogFloatIntDiff, + Optional: true, + DiffSuppressFunc: suppressDataDogSLODisplayValueDiff, }, "warning": { Type: schema.TypeFloat, @@ -68,8 +68,8 @@ func resourceDatadogServiceLevelObjective() *schema.Resource { }, "warning_display": { Type: schema.TypeString, - Required: false, - DiffSuppressFunc: suppressDataDogFloatIntDiff, + Optional: true, + DiffSuppressFunc: suppressDataDogSLODisplayValueDiff, }, }, }, @@ -168,39 +168,40 @@ func buildServiceLevelObjectiveStruct(d *schema.ResourceData) *datadog.ServiceLe } } - if attr, ok := d.GetOk("thresholds"); ok { + if _, ok := d.GetOk("thresholds"); ok { + numThresholds := d.Get("thresholds.#").(int) sloThresholds := make(datadog.ServiceLevelObjectiveThresholds, 0) - for _, rawThreshold := range attr.([]interface{}) { - threshold := rawThreshold.(map[string]interface{}) + for i := 0; i < numThresholds; i++ { + prefix := fmt.Sprintf("thresholds.%d.", i) t := datadog.ServiceLevelObjectiveThreshold{} - if tf, ok := threshold["timeframe"]; ok { + + if tf, ok := d.GetOk(prefix + "timeframe"); ok { t.TimeFrame = datadog.String(tf.(string)) } - if targetValue, ok := threshold["target"]; ok { + if targetValue, ok := d.GetOk(prefix + "target"); ok { if f, ok := floatOk(targetValue); ok { t.Target = datadog.Float64(f) } } - if warningValue, ok := threshold["warning"]; ok { + if warningValue, ok := d.GetOk(prefix + "warning"); ok { if f, ok := floatOk(warningValue); ok { t.Warning = datadog.Float64(f) } } - if targetDisplayValue, ok := threshold["target_display"]; ok { + if targetDisplayValue, ok := d.GetOk(prefix + "target_display"); ok { if s, ok := targetDisplayValue.(string); ok && strings.TrimSpace(s) != "" { t.TargetDisplay = datadog.String(strings.TrimSpace(targetDisplayValue.(string))) } } - if warningDisplayValue, ok := threshold["warning_display"]; ok { + if warningDisplayValue, ok := d.GetOk(prefix + "warning_display"); ok { if s, ok := warningDisplayValue.(string); ok && strings.TrimSpace(s) != "" { t.WarningDisplay = datadog.String(strings.TrimSpace(warningDisplayValue.(string))) } } - sloThresholds = append(sloThresholds, &t) } sort.Sort(sloThresholds) @@ -463,3 +464,21 @@ func resourceDatadogServiceLevelObjectiveImport(d *schema.ResourceData, meta int } return []*schema.ResourceData{d}, nil } + +// Ignore any diff that results from the mix of *_display string values from the +// DataDog API. +func suppressDataDogSLODisplayValueDiff(k, old, new string, d *schema.ResourceData) bool { + sloType := d.Get("type") + if sloType == datadog.ServiceLevelObjectiveTypeMonitor { + // always suppress monitor type, this is controlled via API. + return false + } + + // metric type otherwise + if old == "" || new == "" { + // always suppress if not specified + return true + } + + return suppressDataDogFloatIntDiff(k, old, new, d) +} diff --git a/datadog/resource_datadog_service_level_objective_test.go b/datadog/resource_datadog_service_level_objective_test.go index eb383a81c..f107a78ba 100644 --- a/datadog/resource_datadog_service_level_objective_test.go +++ b/datadog/resource_datadog_service_level_objective_test.go @@ -21,17 +21,16 @@ resource "datadog_service_level_objective" "foo" { denominator = "sum:my.metric{*}.as_count()" } - thresholds = [ - { - timeframe = "7d" - target = 99.5 - warning = 99.8 - }, - { - timeframe = "30d" - target = 99 - } - ] + thresholds { + timeframe = "7d" + target = 99.5 + warning = 99.8 + } + + thresholds { + timeframe = "30d" + target = 99 + } tags = ["foo:bar", "baz"] } @@ -47,17 +46,16 @@ resource "datadog_service_level_objective" "foo" { denominator = "sum:my.metric{type:good}.as_count() + sum:my.metric{type:bad}.as_count()" } - thresholds = [ - { - timeframe = "7d" - target = 99.5 - warning = 99.8 - }, - { - timeframe = "30d" - target = 98 - } - ] + thresholds { + timeframe = "7d" + target = 99.5 + warning = 99.8 + } + + thresholds { + timeframe = "30d" + target = 98 + } tags = ["foo:bar", "baz"] } diff --git a/website/docs/r/service_level_objective.html.markdown b/website/docs/r/service_level_objective.html.markdown index c1d49d6fb..bd4ca06d9 100644 --- a/website/docs/r/service_level_objective.html.markdown +++ b/website/docs/r/service_level_objective.html.markdown @@ -24,21 +24,21 @@ resource "datadog_service_level_objective" "foo" { denominator = "sum:my.custom.count.metric{*}.as_count()" } - thresholds = [ - { - timeframe = "7d" - target = 99.9 - warning = 99.99 - target_display = "99.900" - warning_display = "99.990" - }, - { - timeframe = "30d" - target = 99.9 - warning = 99.99 - target_display = "99.900" - warning_display = "99.990" - } + thresholds { + timeframe = "7d" + target = 99.9 + warning = 99.99 + target_display = "99.900" + warning_display = "99.990" + } + + thresholds { + timeframe = "30d" + target = 99.9 + warning = 99.99 + target_display = "99.900" + warning_display = "99.990" + } ] tags = ["foo:bar", "baz"] @@ -54,18 +54,17 @@ resource "datadog_service_level_objective" "bar" { description = "My custom monitor SLO" monitor_ids = [1, 2, 3] - thresholds = [ - { - timeframe = "7d" - target = 99.9 - warning = 99.99 - }, - { - timeframe = "30d" - target = 99.9 - warning = 99.99 - } - ] + thresholds { + timeframe = "7d" + target = 99.9 + warning = 99.99 + } + + thresholds { + timeframe = "30d" + target = 99.9 + warning = 99.99 + } tags = ["foo:bar", "baz"] } From 1eb55bc87c52ba7c165ebf3bd6bc08b808253c08 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Tue, 13 Aug 2019 17:00:53 -0500 Subject: [PATCH 09/16] because of https://github.com/hashicorp/terraform/issues/6215/ move from TypeMap to TypeList and validate --- ...esource_datadog_service_level_objective.go | 25 +++++++++++-------- ...ce_datadog_service_level_objective_test.go | 12 ++++----- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/datadog/resource_datadog_service_level_objective.go b/datadog/resource_datadog_service_level_objective.go index 4b81f6093..cb08709b0 100644 --- a/datadog/resource_datadog_service_level_objective.go +++ b/datadog/resource_datadog_service_level_objective.go @@ -83,7 +83,9 @@ func resourceDatadogServiceLevelObjective() *schema.Resource { // Metric-Based SLO "query": { - Type: schema.TypeMap, + // we use TypeList here because of https://github.com/hashicorp/terraform/issues/6215/ + Type: schema.TypeList, + MaxItems: 1, Optional: true, ConflictsWith: []string{"monitor_ids", "monitor_search", "groups"}, Description: "The metric query of good / total events", @@ -238,9 +240,11 @@ func buildServiceLevelObjectiveStruct(d *schema.ResourceData) *datadog.ServiceLe } default: // query type - slo.Query = &datadog.ServiceLevelObjectiveMetricQuery{ - Numerator: datadog.String(d.Get("query.numerator").(string)), - Denominator: datadog.String(d.Get("query.denominator").(string)), + if _, ok := d.GetOk("query.0"); ok { + slo.Query = &datadog.ServiceLevelObjectiveMetricQuery{ + Numerator: datadog.String(d.Get("query.0.numerator").(string)), + Denominator: datadog.String(d.Get("query.0.denominator").(string)), + } } } @@ -353,7 +357,7 @@ func resourceDatadogServiceLevelObjectiveRead(d *schema.ResourceData, meta inter q := slo.GetQuery() query["numerator"] = q.GetNumerator() query["denominator"] = q.GetDenominator() - d.Set("query", query) + d.Set("query", []map[string]interface{}{query}) } return nil @@ -402,11 +406,12 @@ func resourceDatadogServiceLevelObjectiveUpdate(d *schema.ResourceData, meta int default: // metric type if attr, ok := d.GetOk("query"); ok { - query := attr.(map[string]interface{}) - slo.SetQuery(datadog.ServiceLevelObjectiveMetricQuery{ - Numerator: datadog.String(query["numerator"].(string)), - Denominator: datadog.String(query["denominator"].(string)), - }) + if query, ok := attr.([]map[string]interface{}); ok && len(query) == 1 { + slo.SetQuery(datadog.ServiceLevelObjectiveMetricQuery{ + Numerator: datadog.String(query[0]["numerator"].(string)), + Denominator: datadog.String(query[0]["denominator"].(string)), + }) + } } } diff --git a/datadog/resource_datadog_service_level_objective_test.go b/datadog/resource_datadog_service_level_objective_test.go index f107a78ba..c466aeb3a 100644 --- a/datadog/resource_datadog_service_level_objective_test.go +++ b/datadog/resource_datadog_service_level_objective_test.go @@ -16,7 +16,7 @@ resource "datadog_service_level_objective" "foo" { name = "name for metric SLO foo" type = "metric" description = "some description about foo SLO" - query = { + query { numerator = "sum:my.metric{type:good}.as_count()" denominator = "sum:my.metric{*}.as_count()" } @@ -41,7 +41,7 @@ resource "datadog_service_level_objective" "foo" { name = "updated name for metric SLO foo" type = "metric" description = "some updated description about foo SLO" - query = { + query { numerator = "sum:my.metric{type:good}.as_count()" denominator = "sum:my.metric{type:good}.as_count() + sum:my.metric{type:bad}.as_count()" } @@ -80,9 +80,9 @@ func TestAccDatadogServiceLevelObjective_Basic(t *testing.T) { resource.TestCheckResourceAttr( "datadog_service_level_objective.foo", "type", "metric"), resource.TestCheckResourceAttr( - "datadog_service_level_objective.foo", "query.numerator", "sum:my.metric{type:good}.as_count()"), + "datadog_service_level_objective.foo", "query.0.numerator", "sum:my.metric{type:good}.as_count()"), resource.TestCheckResourceAttr( - "datadog_service_level_objective.foo", "query.denominator", "sum:my.metric{*}.as_count()"), + "datadog_service_level_objective.foo", "query.0.denominator", "sum:my.metric{*}.as_count()"), // Thresholds are a TypeList, that are sorted by timeframe alphabetically. resource.TestCheckResourceAttr( "datadog_service_level_objective.foo", "thresholds.#", "2"), @@ -119,9 +119,9 @@ func TestAccDatadogServiceLevelObjective_Basic(t *testing.T) { resource.TestCheckResourceAttr( "datadog_service_level_objective.foo", "type", "metric"), resource.TestCheckResourceAttr( - "datadog_service_level_objective.foo", "query.numerator", "sum:my.metric{type:good}.as_count()"), + "datadog_service_level_objective.foo", "query.0.numerator", "sum:my.metric{type:good}.as_count()"), resource.TestCheckResourceAttr( - "datadog_service_level_objective.foo", "query.denominator", "sum:my.metric{type:good}.as_count() + sum:my.metric{type:bad}.as_count()"), + "datadog_service_level_objective.foo", "query.0.denominator", "sum:my.metric{type:good}.as_count() + sum:my.metric{type:bad}.as_count()"), // Thresholds are a TypeList, that are sorted by timeframe alphabetically. resource.TestCheckResourceAttr( "datadog_service_level_objective.foo", "thresholds.#", "2"), From 6338ed2b4594fa083eab44bd8d26fafd7ccdcb69 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Tue, 13 Aug 2019 17:06:24 -0500 Subject: [PATCH 10/16] add validation to query options --- datadog/resource_datadog_service_level_objective.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/datadog/resource_datadog_service_level_objective.go b/datadog/resource_datadog_service_level_objective.go index cb08709b0..33011f8e6 100644 --- a/datadog/resource_datadog_service_level_objective.go +++ b/datadog/resource_datadog_service_level_objective.go @@ -97,6 +97,7 @@ func resourceDatadogServiceLevelObjective() *schema.Resource { StateFunc: func(val interface{}) string { return strings.TrimSpace(val.(string)) }, + ValidateFunc: validQueryOption, }, "denominator": { Type: schema.TypeString, @@ -104,6 +105,7 @@ func resourceDatadogServiceLevelObjective() *schema.Resource { StateFunc: func(val interface{}) string { return strings.TrimSpace(val.(string)) }, + ValidateFunc: validQueryOption, }, }, }, @@ -487,3 +489,13 @@ func suppressDataDogSLODisplayValueDiff(k, old, new string, d *schema.ResourceDa return suppressDataDogFloatIntDiff(k, old, new, d) } + +// validQueryOption validates that the `query` parameter is naively valid. +func validQueryOption(v interface{}, k string) (ws []string, errors []error) { + strVal := strings.TrimSpace(v.(string)) + if strVal == "" { + errors = append(errors, fmt.Errorf("empty `query.%s` specified for SLO", k)) + return + } + return +} From 8db91962def955f529b2070dc70dd051f2131389 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Tue, 13 Aug 2019 17:13:53 -0500 Subject: [PATCH 11/16] Revert "add validation to query options" This reverts commit 6338ed2b4594fa083eab44bd8d26fafd7ccdcb69. --- datadog/resource_datadog_service_level_objective.go | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/datadog/resource_datadog_service_level_objective.go b/datadog/resource_datadog_service_level_objective.go index 33011f8e6..cb08709b0 100644 --- a/datadog/resource_datadog_service_level_objective.go +++ b/datadog/resource_datadog_service_level_objective.go @@ -97,7 +97,6 @@ func resourceDatadogServiceLevelObjective() *schema.Resource { StateFunc: func(val interface{}) string { return strings.TrimSpace(val.(string)) }, - ValidateFunc: validQueryOption, }, "denominator": { Type: schema.TypeString, @@ -105,7 +104,6 @@ func resourceDatadogServiceLevelObjective() *schema.Resource { StateFunc: func(val interface{}) string { return strings.TrimSpace(val.(string)) }, - ValidateFunc: validQueryOption, }, }, }, @@ -489,13 +487,3 @@ func suppressDataDogSLODisplayValueDiff(k, old, new string, d *schema.ResourceDa return suppressDataDogFloatIntDiff(k, old, new, d) } - -// validQueryOption validates that the `query` parameter is naively valid. -func validQueryOption(v interface{}, k string) (ws []string, errors []error) { - strVal := strings.TrimSpace(v.(string)) - if strVal == "" { - errors = append(errors, fmt.Errorf("empty `query.%s` specified for SLO", k)) - return - } - return -} From 1db15fbf0251f54f9d4dc83e482e7c51fdc4b326 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Wed, 14 Aug 2019 15:37:42 -0500 Subject: [PATCH 12/16] fix docs address possible difference in state value for thresholds --- .../resource_datadog_service_level_objective.go | 17 +++++++++++++++-- .../r/service_level_objective.html.markdown | 4 ++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/datadog/resource_datadog_service_level_objective.go b/datadog/resource_datadog_service_level_objective.go index cb08709b0..7e92d1491 100644 --- a/datadog/resource_datadog_service_level_objective.go +++ b/datadog/resource_datadog_service_level_objective.go @@ -426,7 +426,20 @@ func resourceDatadogServiceLevelObjectiveUpdate(d *schema.ResourceData, meta int if attr, ok := d.GetOk("thresholds"); ok { sloThresholds := make(datadog.ServiceLevelObjectiveThresholds, 0) - thresholds := attr.([]map[string]interface{}) + thresholds := make([]map[string]interface{}, 0) + switch attr.(type) { + case []interface{}: + raw := attr.([]interface{}) + for _, a := range raw { + if a, ok := a.(map[string]interface{}); ok { + thresholds = append(thresholds, a) + } + } + case []map[string]interface{}: + thresholds = attr.([]map[string]interface{}) + default: + // ignore + } for _, threshold := range thresholds { t := datadog.ServiceLevelObjectiveThreshold{ TimeFrame: datadog.String(threshold["timeframe"].(string)), @@ -476,7 +489,7 @@ func suppressDataDogSLODisplayValueDiff(k, old, new string, d *schema.ResourceDa sloType := d.Get("type") if sloType == datadog.ServiceLevelObjectiveTypeMonitor { // always suppress monitor type, this is controlled via API. - return false + return true } // metric type otherwise diff --git a/website/docs/r/service_level_objective.html.markdown b/website/docs/r/service_level_objective.html.markdown index bd4ca06d9..550464f79 100644 --- a/website/docs/r/service_level_objective.html.markdown +++ b/website/docs/r/service_level_objective.html.markdown @@ -19,7 +19,7 @@ resource "datadog_service_level_objective" "foo" { name = "Example Metric SLO" type = "metric" description = "My custom metric SLO" - query = { + query { numerator = "sum:my.custom.count.metric{type:good_events}.as_count()" denominator = "sum:my.custom.count.metric{*}.as_count()" } @@ -97,7 +97,7 @@ The following options are specific to the `type` of service level objective: * `denominator` - (Required) the sum of the `total` events * Example Usage: ```hcl -query = { +query { numerator = "sum:my.custom.count.metric{type:good}.as_count()" denominator = "sum:my.custom.count.metric{*}.as_count()" } From 5d383c0a40e0bc6f5db7933cc55c486d248d9e91 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Wed, 14 Aug 2019 16:09:53 -0500 Subject: [PATCH 13/16] apply same logic to query since it is now TypeList --- ...esource_datadog_service_level_objective.go | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/datadog/resource_datadog_service_level_objective.go b/datadog/resource_datadog_service_level_objective.go index 7e92d1491..39384117c 100644 --- a/datadog/resource_datadog_service_level_objective.go +++ b/datadog/resource_datadog_service_level_objective.go @@ -406,6 +406,20 @@ func resourceDatadogServiceLevelObjectiveUpdate(d *schema.ResourceData, meta int default: // metric type if attr, ok := d.GetOk("query"); ok { + queries := make([]map[string]interface{}, 0) + switch attr.(type) { + case []interface{}: + raw := attr.([]interface{}) + for _, rawQuery := range raw { + if query, ok := rawQuery.(map[string]interface{}); ok { + queries = append(queries, query) + } + } + case []map[string]interface{}: + queries = attr.([]map[string]interface{}) + default: + // ignore + } if query, ok := attr.([]map[string]interface{}); ok && len(query) == 1 { slo.SetQuery(datadog.ServiceLevelObjectiveMetricQuery{ Numerator: datadog.String(query[0]["numerator"].(string)), @@ -430,9 +444,9 @@ func resourceDatadogServiceLevelObjectiveUpdate(d *schema.ResourceData, meta int switch attr.(type) { case []interface{}: raw := attr.([]interface{}) - for _, a := range raw { - if a, ok := a.(map[string]interface{}); ok { - thresholds = append(thresholds, a) + for _, rawThreshold := range raw { + if threshold, ok := rawThreshold.(map[string]interface{}); ok { + thresholds = append(thresholds, threshold) } } case []map[string]interface{}: From cbfe4484c9e6a5fbf5916a5cbe69eefef8ce4dcf Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Wed, 14 Aug 2019 16:10:34 -0500 Subject: [PATCH 14/16] fix --- datadog/resource_datadog_service_level_objective.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/datadog/resource_datadog_service_level_objective.go b/datadog/resource_datadog_service_level_objective.go index 39384117c..93c8e51a5 100644 --- a/datadog/resource_datadog_service_level_objective.go +++ b/datadog/resource_datadog_service_level_objective.go @@ -420,7 +420,8 @@ func resourceDatadogServiceLevelObjectiveUpdate(d *schema.ResourceData, meta int default: // ignore } - if query, ok := attr.([]map[string]interface{}); ok && len(query) == 1 { + if query, ok := attr.([]map[string]interface{}); ok && len(query) >= 1 { + // only use the first defined query slo.SetQuery(datadog.ServiceLevelObjectiveMetricQuery{ Numerator: datadog.String(query[0]["numerator"].(string)), Denominator: datadog.String(query[0]["denominator"].(string)), From 8bf9857400979de66dee866b120e0eedb550ac84 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Wed, 14 Aug 2019 16:19:09 -0500 Subject: [PATCH 15/16] ValueType casts as []interface{} for TypeList --- ...esource_datadog_service_level_objective.go | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/datadog/resource_datadog_service_level_objective.go b/datadog/resource_datadog_service_level_objective.go index 93c8e51a5..08c17b665 100644 --- a/datadog/resource_datadog_service_level_objective.go +++ b/datadog/resource_datadog_service_level_objective.go @@ -407,24 +407,17 @@ func resourceDatadogServiceLevelObjectiveUpdate(d *schema.ResourceData, meta int // metric type if attr, ok := d.GetOk("query"); ok { queries := make([]map[string]interface{}, 0) - switch attr.(type) { - case []interface{}: - raw := attr.([]interface{}) - for _, rawQuery := range raw { - if query, ok := rawQuery.(map[string]interface{}); ok { - queries = append(queries, query) - } + raw := attr.([]interface{}) + for _, rawQuery := range raw { + if query, ok := rawQuery.(map[string]interface{}); ok { + queries = append(queries, query) } - case []map[string]interface{}: - queries = attr.([]map[string]interface{}) - default: - // ignore } - if query, ok := attr.([]map[string]interface{}); ok && len(query) >= 1 { + if len(queries) >= 1 { // only use the first defined query slo.SetQuery(datadog.ServiceLevelObjectiveMetricQuery{ - Numerator: datadog.String(query[0]["numerator"].(string)), - Denominator: datadog.String(query[0]["denominator"].(string)), + Numerator: datadog.String(queries[0]["numerator"].(string)), + Denominator: datadog.String(queries[0]["denominator"].(string)), }) } } @@ -442,18 +435,11 @@ func resourceDatadogServiceLevelObjectiveUpdate(d *schema.ResourceData, meta int if attr, ok := d.GetOk("thresholds"); ok { sloThresholds := make(datadog.ServiceLevelObjectiveThresholds, 0) thresholds := make([]map[string]interface{}, 0) - switch attr.(type) { - case []interface{}: - raw := attr.([]interface{}) - for _, rawThreshold := range raw { - if threshold, ok := rawThreshold.(map[string]interface{}); ok { - thresholds = append(thresholds, threshold) - } + raw := attr.([]interface{}) + for _, rawThreshold := range raw { + if threshold, ok := rawThreshold.(map[string]interface{}); ok { + thresholds = append(thresholds, threshold) } - case []map[string]interface{}: - thresholds = attr.([]map[string]interface{}) - default: - // ignore } for _, threshold := range thresholds { t := datadog.ServiceLevelObjectiveThreshold{ From d02d40f600e66d9f48a07b863022ac42f951edf4 Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Thu, 15 Aug 2019 10:19:16 -0500 Subject: [PATCH 16/16] fix dangling character --- website/docs/r/service_level_objective.html.markdown | 1 - 1 file changed, 1 deletion(-) diff --git a/website/docs/r/service_level_objective.html.markdown b/website/docs/r/service_level_objective.html.markdown index 550464f79..37c79563f 100644 --- a/website/docs/r/service_level_objective.html.markdown +++ b/website/docs/r/service_level_objective.html.markdown @@ -39,7 +39,6 @@ resource "datadog_service_level_objective" "foo" { target_display = "99.900" warning_display = "99.990" } - ] tags = ["foo:bar", "baz"] }