diff --git a/.go-version b/.go-version index 82eab727..f288d111 100644 --- a/.go-version +++ b/.go-version @@ -1 +1 @@ -1.84.4 +1.85.0 diff --git a/client/metric_dashboards.go b/client/metric_dashboards.go index 113dac0e..038a747e 100644 --- a/client/metric_dashboards.go +++ b/client/metric_dashboards.go @@ -30,6 +30,7 @@ type UnifiedGroup struct { VisibilityType string `json:"visibility_type"` Charts []UnifiedChart `json:"charts"` Labels []Label `json:"labels"` + Panels []Panel `json:"panels"` } type UnifiedPosition struct { @@ -57,6 +58,14 @@ type Label struct { Value string `json:"label_value"` } +type Panel struct { + ID string `json:"id"` + Title string `json:"title"` + Type string `json:"type"` + Position UnifiedPosition `json:"position"` + Body map[string]any `json:"body"` +} + type YAxis struct { Min float64 `json:"min"` Max float64 `json:"max"` diff --git a/docs/resources/dashboard.md b/docs/resources/dashboard.md index 541a7efc..b43c1e00 100644 --- a/docs/resources/dashboard.md +++ b/docs/resources/dashboard.md @@ -160,6 +160,7 @@ Required: Optional: - `chart` (Block Set) (see [below for nested schema](#nestedblock--group--chart)) +- `service_health_panel` (Block Set) A dashboard panel to view the health of your services (see [below for nested schema](#nestedblock--group--service_health_panel)) - `text_panel` (Block List) (see [below for nested schema](#nestedblock--group--text_panel)) - `title` (String) @@ -240,6 +241,34 @@ Required: + +### Nested Schema for `group.service_health_panel` + +Optional: + +- `height` (Number) +- `name` (String) +- `panel_options` (Block Set, Max: 1) custom options for the service health panel (see [below for nested schema](#nestedblock--group--service_health_panel--panel_options)) +- `width` (Number) +- `x_pos` (Number) +- `y_pos` (Number) + +Read-Only: + +- `id` (String) The ID of this resource. + + +### Nested Schema for `group.service_health_panel.panel_options` + +Optional: + +- `change_since` (String) +- `percentile` (String) +- `sort_by` (String) +- `sort_direction` (String) + + + ### Nested Schema for `group.text_panel` diff --git a/docs/resources/metric_dashboard.md b/docs/resources/metric_dashboard.md index da01b075..69d249a9 100644 --- a/docs/resources/metric_dashboard.md +++ b/docs/resources/metric_dashboard.md @@ -200,6 +200,7 @@ Required: Optional: - `chart` (Block Set) (see [below for nested schema](#nestedblock--group--chart)) +- `service_health_panel` (Block Set) A dashboard panel to view the health of your services (see [below for nested schema](#nestedblock--group--service_health_panel)) - `text_panel` (Block List) (see [below for nested schema](#nestedblock--group--text_panel)) - `title` (String) @@ -297,6 +298,34 @@ Required: + +### Nested Schema for `group.service_health_panel` + +Optional: + +- `height` (Number) +- `name` (String) +- `panel_options` (Block Set, Max: 1) custom options for the service health panel (see [below for nested schema](#nestedblock--group--service_health_panel--panel_options)) +- `width` (Number) +- `x_pos` (Number) +- `y_pos` (Number) + +Read-Only: + +- `id` (String) The ID of this resource. + + +### Nested Schema for `group.service_health_panel.panel_options` + +Optional: + +- `change_since` (String) +- `percentile` (String) +- `sort_by` (String) +- `sort_direction` (String) + + + ### Nested Schema for `group.text_panel` diff --git a/lightstep/resource_dashboard.go b/lightstep/resource_dashboard.go index b57ed024..daa55967 100644 --- a/lightstep/resource_dashboard.go +++ b/lightstep/resource_dashboard.go @@ -131,7 +131,7 @@ func getQueriesFromUnifiedDashboardResourceData( qs := map[string]interface{}{ "hidden": q.Hidden, "display": q.Display, - "display_type_options": displayTypeOptionsFromResourceData(q.DisplayTypeOptions), + "display_type_options": convertNestedMapToSchemaSet(q.DisplayTypeOptions), "query_name": q.Name, "query_string": q.TQLQuery, "dependency_map_options": getDependencyMapOptions(q.DependencyMapOptions), @@ -155,15 +155,6 @@ func getQueriesFromUnifiedDashboardResourceData( return queries, nil } -func displayTypeOptionsFromResourceData(opts map[string]interface{}) *schema.Set { - // "display_type_options" is a set that always has at most one element, so - // the hash function is trivial - f := func(i interface{}) int { - return 1 - } - return schema.NewSet(f, []interface{}{opts}) -} - func getDependencyMapOptions(options *client.DependencyMapOptions) []interface{} { if options == nil { return nil diff --git a/lightstep/resource_metric_dashboard.go b/lightstep/resource_metric_dashboard.go index 24105303..96104d77 100644 --- a/lightstep/resource_metric_dashboard.go +++ b/lightstep/resource_metric_dashboard.go @@ -6,11 +6,11 @@ import ( "net/http" "strings" - "github.com/lightstep/terraform-provider-lightstep/client" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + "github.com/lightstep/terraform-provider-lightstep/client" ) type ChartSchemaType int @@ -129,11 +129,12 @@ func getGroupSchema(chartSchemaType ChartSchemaType) map[string]*schema.Schema { "text_panel": { Type: schema.TypeList, Optional: true, - Computed: true, // the panels can be mutated individually; chart mutations should not trigger group updates + Computed: true, // the panels can be mutated individually; text panel mutations should not trigger group updates Elem: &schema.Resource{ Schema: getTextPanelSchema(), }, }, + ServiceHealthPanel: getServiceHealthPanelSchema(), } } @@ -168,14 +169,26 @@ func getPanelSchema(isNameRequired bool) map[string]*schema.Schema { } } - return map[string]*schema.Schema{ - // Alias for what we refer to as title elsewhere - "name": nameSchema(), - "description": { - Type: schema.TypeString, - Optional: true, - Default: "", + return mergeSchemas( + getPositionSchema(), + map[string]*schema.Schema{ + // Alias for what we refer to as title elsewhere + "name": nameSchema(), + "description": { + Type: schema.TypeString, + Optional: true, + Default: "", + }, + "id": { + Type: schema.TypeString, + Computed: true, + }, }, + ) +} + +func getPositionSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ "x_pos": { Type: schema.TypeInt, ValidateFunc: validation.IntAtLeast(0), @@ -200,10 +213,6 @@ func getPanelSchema(isNameRequired bool) map[string]*schema.Schema { Default: 0, Optional: true, }, - "id": { - Type: schema.TypeString, - Computed: true, - }, } } @@ -487,6 +496,10 @@ func buildGroups(groupsIn []interface{}, legacyChartsIn []interface{}) ([]client if err != nil { return nil, hasLegacyChartsIn, err } + serviceHealthPanels, err := convertServiceHealthFromResourceToApiRequest(group[ServiceHealthPanel]) + if err != nil { + return nil, hasLegacyChartsIn, err + } g := client.UnifiedGroup{ ID: group["id"].(string), @@ -494,6 +507,7 @@ func buildGroups(groupsIn []interface{}, legacyChartsIn []interface{}) ([]client Title: group["title"].(string), VisibilityType: group["visibility_type"].(string), Charts: append(chartPanels, textPanels...), + Panels: serviceHealthPanels, } newGroups = append(newGroups, g) } @@ -663,6 +677,8 @@ func (p *resourceUnifiedDashboardImp) setResourceDataFromUnifiedDashboard(projec group["chart"] = groupCharts group["text_panel"] = groupTextPanels + group[ServiceHealthPanel] = convertServiceHealthfromApiRequestToResource(g.Panels) + groups = append(groups, group) } if err := d.Set("group", groups); err != nil { diff --git a/lightstep/resource_metric_dashboard_test.go b/lightstep/resource_metric_dashboard_test.go index a0430105..e1a08f4a 100644 --- a/lightstep/resource_metric_dashboard_test.go +++ b/lightstep/resource_metric_dashboard_test.go @@ -402,6 +402,68 @@ resource "lightstep_metric_dashboard" "test" { }) } +func TestAccDashboardServiceHealthPanel(t *testing.T) { + var dashboard client.UnifiedDashboard + + dashboardConfig := ` +resource "lightstep_metric_dashboard" "test" { + project_name = "` + testProject + `" + dashboard_name = "Acceptance Test Dashboard (TestAccDashboardServiceHealthPanel)" + dashboard_description = "Dashboard to test the service health panel" + + group { + rank = 0 + visibility_type = "implicit" + + service_health_panel { + name = "test_service_health_panel" + + x_pos = 0 + y_pos = 0 + width = 10 + height = 10 + + panel_options { + sort_direction = "asc" + sort_by = "latency" + } + } + } +} +` + // Change the chart name and metric name + updatedConfig := strings.Replace(dashboardConfig, "asc", "desc", -1) + + 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 service health panel. verify the name and panel_options.percentile + Config: dashboardConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckMetricDashboardExists(resourceName, &dashboard), + resource.TestCheckResourceAttr(resourceName, "group.0.service_health_panel.0.name", "test_service_health_panel"), + resource.TestCheckResourceAttr(resourceName, "group.0.service_health_panel.0.panel_options.0.sort_by", "latency"), + resource.TestCheckResourceAttr(resourceName, "group.0.service_health_panel.0.panel_options.0.sort_direction", "asc"), + ), + }, + { + // Updated config will contain the new metric and chart name + Config: updatedConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckMetricDashboardExists(resourceName, &dashboard), + resource.TestCheckResourceAttr(resourceName, "group.0.service_health_panel.0.name", "test_service_health_panel"), + resource.TestCheckResourceAttr(resourceName, "group.0.service_health_panel.0.panel_options.0.sort_direction", "desc"), + ), + }, + }, + }) +} + func Test_buildLabels(t *testing.T) { tests := []struct { name string diff --git a/lightstep/service_health_panel.go b/lightstep/service_health_panel.go new file mode 100644 index 00000000..3be11ae0 --- /dev/null +++ b/lightstep/service_health_panel.go @@ -0,0 +1,163 @@ +package lightstep + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + "github.com/lightstep/terraform-provider-lightstep/client" +) + +const ( + ServiceHealthPanel = "service_health_panel" + ServiceHealthType = "service_health" + ServiceHealthPanelDesc = "A dashboard panel to view the health of your services" +) + +func getServiceHealthPanelSchema() *schema.Schema { + elements := mergeSchemas( + getPositionSchema(), + map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + Default: "Service Health Panel", + }, + "id": { + Type: schema.TypeString, + Computed: true, + }, + "panel_options": { + Type: schema.TypeSet, + Optional: true, + Description: "custom options for the service health panel", + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "sort_by": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + "service", + "latency", + "error", + "rate", + }, false), + }, + "sort_direction": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + "asc", + "desc", + "error", + "rate", + }, false), + }, + "percentile": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + "p50", + "p90", + "p95", + "p99", + }, false), + }, + "change_since": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + "1h", + "1d", + "3d", + "p99", + }, false), + }, + }, + }, + }, + }, + ) + + return &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Computed: true, // the panels can be mutated individually; panel mutations should not trigger group updates + Description: ServiceHealthPanelDesc, + Elem: &schema.Resource{Schema: elements}, + } +} + +func convertServiceHealthFromResourceToApiRequest(serviceHealthPanelsIn interface{}) ([]client.Panel, error) { + in := serviceHealthPanelsIn.(*schema.Set).List() + var serviceHealthPanels []client.Panel + + for _, s := range in { + serviceHealthPanel, ok := s.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("bad format, %v", s) + } + p := client.Panel{ + ID: serviceHealthPanel["id"].(string), + Title: serviceHealthPanel["name"].(string), + Type: ServiceHealthType, + Position: buildPosition(serviceHealthPanel), + } + + p.Body = map[string]interface{}{} + // N.B. panel_options are optional, so we don't return an error if not found + if opts, ok := serviceHealthPanel["panel_options"].(*schema.Set); ok { + list := opts.List() + count := len(list) + if count > 1 { + return nil, fmt.Errorf("panel_options must be defined only once") + } else if count == 1 { + resourceDisplayOptions, ok := list[0].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("unexpected format for panel_options") + } + for k, v := range resourceDisplayOptions { + // don't want to send 'sort_by: ""' to the API etc. + if v == "" { + delete(resourceDisplayOptions, k) + } + } + if len(resourceDisplayOptions) > 0 { + p.Body["display_options"] = resourceDisplayOptions + } + } + } + serviceHealthPanels = append(serviceHealthPanels, p) + } + + return serviceHealthPanels, nil +} + +func convertServiceHealthfromApiRequestToResource(apiPanels []client.Panel) []interface{} { + var serviceHealthPanelResources []interface{} + for _, p := range apiPanels { + if p.Type == ServiceHealthType { + resource := map[string]interface{}{} + // Alias for what we refer to as title elsewhere + resource["name"] = p.Title + resource["x_pos"] = p.Position.XPos + resource["y_pos"] = p.Position.YPos + resource["width"] = p.Position.Width + resource["height"] = p.Position.Height + resource["id"] = p.ID + // N.B. the panel body might be nil. panel_options are optional for the service health panel. + if p.Body != nil { + if maybeDisplayOptions, ok := p.Body["display_options"]; ok { + displayOptions, ok := maybeDisplayOptions.(map[string]interface{}) + if ok { + resource["panel_options"] = convertNestedMapToSchemaSet(displayOptions) + } + } + } + serviceHealthPanelResources = append(serviceHealthPanelResources, resource) + } + } + return serviceHealthPanelResources +} diff --git a/lightstep/util.go b/lightstep/util.go index 5f8064d1..c832dff1 100644 --- a/lightstep/util.go +++ b/lightstep/util.go @@ -11,3 +11,13 @@ func mergeSchemas(arr ...map[string]*schema.Schema) map[string]*schema.Schema { } return dst } + +// takes a nested map (such as display_type_options or panel_options) and converts to a schema set +func convertNestedMapToSchemaSet(opts map[string]interface{}) *schema.Set { + // nested maps contain a set that always has at most one element, so + // the hash function is trivial + f := func(i interface{}) int { + return 1 + } + return schema.NewSet(f, []interface{}{opts}) +}