diff --git a/.go-version b/.go-version index 00bce9c..9141007 100644 --- a/.go-version +++ b/.go-version @@ -1 +1 @@ -1.95.8 +1.96.0 diff --git a/client/metric_dashboards.go b/client/metric_dashboards.go index b6dce75..177d648 100644 --- a/client/metric_dashboards.go +++ b/client/metric_dashboards.go @@ -51,6 +51,7 @@ type UnifiedChart struct { YAxis *YAxis `json:"y-axis"` MetricQueries []MetricQueryWithAttributes `json:"metric-queries"` Text string `json:"text"` + Thresholds []Threshold `json:"thresholds"` Subtitle *string `json:"subtitle,omitempty"` } @@ -67,6 +68,15 @@ type Panel struct { Body map[string]any `json:"body"` } +type Threshold struct { + // enum: E,GE,GT,LE,LT + Operator string `json:"operator"` + Value float64 `json:"value"` + // An alpha hex color + Color string `json:"color"` + Label string `json:"label"` +} + type YAxis struct { Min float64 `json:"min"` Max float64 `json:"max"` diff --git a/docs/resources/dashboard.md b/docs/resources/dashboard.md index 0860c52..0261009 100644 --- a/docs/resources/dashboard.md +++ b/docs/resources/dashboard.md @@ -92,6 +92,7 @@ Optional: - `description` (String) - `height` (Number) - `subtitle` (String) Subtitle to show beneath big number, unused in other chart types +- `threshold` (Block List) (see [below for nested schema](#nestedblock--chart--threshold)) - `width` (Number) - `x_pos` (Number) - `y_axis` (Block List, Max: 1, Deprecated) (see [below for nested schema](#nestedblock--chart--y_axis)) @@ -143,6 +144,20 @@ Optional: + +### Nested Schema for `chart.threshold` + +Required: + +- `color` (String) +- `operator` (String) +- `value` (Number) + +Optional: + +- `label` (String) + + ### Nested Schema for `chart.y_axis` @@ -244,6 +259,7 @@ Optional: - `description` (String) - `height` (Number) - `subtitle` (String) Subtitle to show beneath big number, unused in other chart types +- `threshold` (Block List) (see [below for nested schema](#nestedblock--group--chart--threshold)) - `width` (Number) - `x_pos` (Number) - `y_axis` (Block List, Max: 1, Deprecated) (see [below for nested schema](#nestedblock--group--chart--y_axis)) @@ -295,6 +311,20 @@ Optional: + +### Nested Schema for `group.chart.threshold` + +Required: + +- `color` (String) +- `operator` (String) +- `value` (Number) + +Optional: + +- `label` (String) + + ### Nested Schema for `group.chart.y_axis` diff --git a/docs/resources/metric_dashboard.md b/docs/resources/metric_dashboard.md index 60f08fe..d6d099c 100644 --- a/docs/resources/metric_dashboard.md +++ b/docs/resources/metric_dashboard.md @@ -115,6 +115,7 @@ Optional: - `description` (String) - `height` (Number) - `subtitle` (String) Subtitle to show beneath big number, unused in other chart types +- `threshold` (Block List) (see [below for nested schema](#nestedblock--chart--threshold)) - `width` (Number) - `x_pos` (Number) - `y_axis` (Block List, Max: 1, Deprecated) (see [below for nested schema](#nestedblock--chart--y_axis)) @@ -180,6 +181,20 @@ Optional: + +### Nested Schema for `chart.threshold` + +Required: + +- `color` (String) +- `operator` (String) +- `value` (Number) + +Optional: + +- `label` (String) + + ### Nested Schema for `chart.y_axis` @@ -281,6 +296,7 @@ Optional: - `description` (String) - `height` (Number) - `subtitle` (String) Subtitle to show beneath big number, unused in other chart types +- `threshold` (Block List) (see [below for nested schema](#nestedblock--group--chart--threshold)) - `width` (Number) - `x_pos` (Number) - `y_axis` (Block List, Max: 1, Deprecated) (see [below for nested schema](#nestedblock--group--chart--y_axis)) @@ -346,6 +362,20 @@ Optional: + +### Nested Schema for `group.chart.threshold` + +Required: + +- `color` (String) +- `operator` (String) +- `value` (Number) + +Optional: + +- `label` (String) + + ### Nested Schema for `group.chart.y_axis` diff --git a/lightstep/resource_dashboard.go b/lightstep/resource_dashboard.go index 45bd33f..f32424f 100644 --- a/lightstep/resource_dashboard.go +++ b/lightstep/resource_dashboard.go @@ -9,6 +9,34 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) +func getThresholdSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "color": { + Type: schema.TypeString, + Required: true, + }, + "label": { + Type: schema.TypeString, + Optional: true, + }, + "operator": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + "E", + "GE", + "GT", + "LE", + "LT", + }, false), + }, + "value": { + Type: schema.TypeFloat, + Required: true, + }, + } +} + func getUnifiedQuerySchemaMap() map[string]*schema.Schema { sma := map[string]*schema.Schema{ "hidden": { diff --git a/lightstep/resource_metric_dashboard.go b/lightstep/resource_metric_dashboard.go index caec8a3..c42db3e 100644 --- a/lightstep/resource_metric_dashboard.go +++ b/lightstep/resource_metric_dashboard.go @@ -294,6 +294,13 @@ func getChartSchema(chartSchemaType ChartSchemaType) map[string]*schema.Schema { Optional: true, ValidateFunc: validation.StringLenBetween(0, 256), }, + "threshold": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: getThresholdSchema(), + }, + }, }, ) } @@ -580,6 +587,11 @@ func buildCharts(chartsIn []interface{}) ([]client.UnifiedChart, error) { return nil, err } c.MetricQueries = queries + thresholds, err := buildChartThresholds(chart["threshold"].([]interface{})) + if err != nil { + return nil, err + } + c.Thresholds = thresholds yaxis, err := buildYAxis(chart["y_axis"].([]interface{})) if err != nil { @@ -599,6 +611,42 @@ func buildCharts(chartsIn []interface{}) ([]client.UnifiedChart, error) { return newCharts, nil } +func buildChartThresholds(thresholdsIn []interface{}) ([]client.Threshold, error) { + var thresholds []client.Threshold + if len(thresholdsIn) < 1 { + return thresholds, nil + } + for _, t := range thresholdsIn { + threshold := t.(map[string]interface{}) + color, ok := threshold["color"].(string) + + if !ok { + return []client.Threshold{}, fmt.Errorf("missing required attribute 'color' for threshold") + } + + operator, ok := threshold["operator"].(string) + if !ok { + return []client.Threshold{}, fmt.Errorf("missing required attribute 'operator' for threshold") + } + + label := threshold["label"].(string) + + value, ok := threshold["value"].(float64) + + if !ok { + return []client.Threshold{}, fmt.Errorf("missing required attribute 'value' for threshold") + } + + thresholds = append(thresholds, client.Threshold{ + Color: color, + Label: label, + Operator: operator, + Value: value, + }) + } + return thresholds, nil +} + func buildYAxis(yAxisIn []interface{}) (*client.YAxis, error) { if len(yAxisIn) < 1 { return nil, nil @@ -845,11 +893,26 @@ func assembleCharts( resource["query"] = queries } + resource["threshold"] = getThresholdsFromResourceData(c.Thresholds) + chartResources = append(chartResources, resource) } return chartResources, nil } +func getThresholdsFromResourceData(thresholdsIn []client.Threshold) []interface{} { + var thresholds []interface{} + for _, t := range thresholdsIn { + thresholds = append(thresholds, map[string]interface{}{ + "color": t.Color, + "label": t.Label, + "operator": t.Operator, + "value": t.Value, + }) + } + return thresholds +} + // isLegacyImplicitGroup defines the logic for determining if the charts in this dashboard need to be unwrapped to // maintain backwards compatibility with the pre group definition func isLegacyImplicitGroup(groups []client.UnifiedGroup, hasLegacyChartsIn bool) bool { diff --git a/lightstep/resource_metric_dashboard_test.go b/lightstep/resource_metric_dashboard_test.go index 55e7862..e47381e 100644 --- a/lightstep/resource_metric_dashboard_test.go +++ b/lightstep/resource_metric_dashboard_test.go @@ -464,6 +464,139 @@ resource "lightstep_metric_dashboard" "test" { }) } +func TestAccDashboardChartThresholds(t *testing.T) { + var dashboard client.UnifiedDashboard + + dashboardConfig := ` +resource "lightstep_metric_dashboard" "test" { + project_name = "` + testProject + `" + dashboard_name = "Acceptance Test Dashboard (TestAccDashboardChartThresholds)" + dashboard_description = "Dashboard to test thresholds in charts" + + group { + rank = 0 + visibility_type = "implicit" + + chart { + name = "cpu" + rank = 1 + type = "timeseries" + + query { + display = "line" + hidden = false + query_name = "a" + tql = "metric cpu.utilization | latest | group_by [], sum" + } + + threshold { + color = "#AA3018" + label = "critical" + operator = "GT" + value = 99 + } + + threshold { + color = "#6699CC" + operator = "GT" + value = 199 + } + } + } +} +` + updatedDashboardConfig := ` +resource "lightstep_metric_dashboard" "test" { + project_name = "` + testProject + `" + dashboard_name = "Acceptance Test Dashboard (TestAccDashboardChartThresholds)" + dashboard_description = "Dashboard to test thresholds in charts" + + group { + rank = 0 + visibility_type = "implicit" + + chart { + name = "cpu" + rank = 1 + type = "timeseries" + + query { + display = "line" + hidden = false + query_name = "a" + tql = "metric cpu.utilization | latest | group_by [], sum" + } + + threshold { + color = "#AA3018" + label = "critical" + operator = "GT" + value = 99 + } + + threshold { + color = "#6699CC" + label = "extra critical" + operator = "GT" + value = 199 + } + } + } +} +` + resourceName := "lightstep_metric_dashboard.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testGetMetricDashboardDestroy, + Steps: []resource.TestStep{ + { + // Create the initial dashboard with a chart and make sure it has thresholds + Config: dashboardConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckMetricDashboardExists(resourceName, &dashboard), + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.name", "cpu"), + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.threshold.#", "2"), + + // First threshold in chart + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.threshold.0.color", "#AA3018"), + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.threshold.0.label", "critical"), + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.threshold.0.operator", "GT"), + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.threshold.0.value", "99"), + + // Second threshold in chart + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.threshold.1.color", "#6699CC"), + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.threshold.1.operator", "GT"), + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.threshold.1.value", "199"), + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.threshold.1.label", ""), + ), + }, + { + // Updated config will contain the a label for the second threshold + Config: updatedDashboardConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckMetricDashboardExists(resourceName, &dashboard), + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.name", "cpu"), + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.threshold.#", "2"), + + // First threshold in chart + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.threshold.0.color", "#AA3018"), + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.threshold.0.label", "critical"), + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.threshold.0.operator", "GT"), + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.threshold.0.value", "99"), + + // Second threshold in chart + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.threshold.1.color", "#6699CC"), + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.threshold.1.operator", "GT"), + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.threshold.1.value", "199"), + resource.TestCheckResourceAttr(resourceName, "group.0.chart.0.threshold.1.label", "extra critical"), + ), + }, + }, + }) +} + func TestAccDashboardEventQueries(t *testing.T) { var dashboard client.UnifiedDashboard var eventQuery client.EventQueryAttributes