From 97b18b93d7c954c0c19ab456aeced6cae94db3d0 Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Fri, 31 Jan 2025 14:51:20 -0800 Subject: [PATCH 01/22] feat(bigTent slo): Custom Validate function for big tent SLO schema --- internal/resources/slo/resource_slo.go | 68 ++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/internal/resources/slo/resource_slo.go b/internal/resources/slo/resource_slo.go index d8714b957..b9710eea1 100644 --- a/internal/resources/slo/resource_slo.go +++ b/internal/resources/slo/resource_slo.go @@ -2,7 +2,9 @@ package slo import ( "context" + "encoding/json" "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" "regexp" "github.com/grafana/slo-openapi-client/go/slo" @@ -90,9 +92,10 @@ Resource manages Grafana SLOs. Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "query": { - Type: schema.TypeString, - Required: true, - Description: "Freeform Query Field", + Type: schema.TypeString, + Required: true, + Description: "Freeform Query Field", + ValidateFunc: ValidateBigTent(), }, }, }, @@ -104,14 +107,16 @@ Resource manages Grafana SLOs. Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "success_metric": { - Type: schema.TypeString, - Description: `Counter metric for success events (numerator)`, - Required: true, + Type: schema.TypeString, + Description: `Counter metric for success events (numerator)`, + Required: true, + ValidateFunc: ValidateBigTent(), }, "total_metric": { - Type: schema.TypeString, - Description: `Metric for total events (denominator)`, - Required: true, + Type: schema.TypeString, + Description: `Metric for total events (denominator)`, + Required: true, + ValidateFunc: ValidateBigTent(), }, "group_by_labels": { Type: schema.TypeList, @@ -661,3 +666,48 @@ func apiError(action string, err error) diag.Diagnostics { }, } } + +func ValidateBigTent() schema.SchemaValidateFunc { + return func(i interface{}, k string) (warnings []string, errors []error) { + + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %s to be string", k)) + return warnings, errors + } + + normString, err := structure.NormalizeJsonString(v) + if err != nil { + warnings = append(warnings, fmt.Sprintf("%q contains an invalid JSON: %s", k, err)) + return warnings, errors + } + + var gmrQuery []map[string]any + err = json.Unmarshal([]byte(normString), &gmrQuery) + if err != nil { + warnings = append(warnings, fmt.Sprintf("%v", err)) + // If queries aren't valid JSON, we'll attempt to validate as promQL + return warnings, errors + } + + for _, queryObj := range gmrQuery { + source := queryObj["datasource"] + s, ok := source.(map[string]interface{}) + if !ok { + errors = append(errors, fmt.Errorf("expected Big Tent Query %v to have a datasource", queryObj)) + return warnings, errors + } + + _, ok = s["type"] + if !ok { + errors = append(errors, fmt.Errorf("expected Big Tent Query datasource %v to have a type", s)) + } + _, ok = s["uid"] + if !ok { + errors = append(errors, fmt.Errorf("expected Big Tent Query datasource %v to have a uid", s)) + } + } + + return warnings, errors + } +} From 4231bd0266877d71226850e81667be41c7587ac8 Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Fri, 31 Jan 2025 16:00:10 -0800 Subject: [PATCH 02/22] feat(bigTent slo): Tests for custom validate functions --- internal/resources/slo/resource_slo.go | 1 - internal/resources/slo/resource_slo_test.go | 134 ++++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/internal/resources/slo/resource_slo.go b/internal/resources/slo/resource_slo.go index b9710eea1..2cd9518eb 100644 --- a/internal/resources/slo/resource_slo.go +++ b/internal/resources/slo/resource_slo.go @@ -669,7 +669,6 @@ func apiError(action string, err error) diag.Diagnostics { func ValidateBigTent() schema.SchemaValidateFunc { return func(i interface{}, k string) (warnings []string, errors []error) { - v, ok := i.(string) if !ok { errors = append(errors, fmt.Errorf("expected type of %s to be string", k)) diff --git a/internal/resources/slo/resource_slo_test.go b/internal/resources/slo/resource_slo_test.go index 8623c0fec..b08158fef 100644 --- a/internal/resources/slo/resource_slo_test.go +++ b/internal/resources/slo/resource_slo_test.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + slo2 "github.com/grafana/terraform-provider-grafana/v3/internal/resources/slo" + "github.com/stretchr/testify/assert" "regexp" "strings" "testing" @@ -412,3 +414,135 @@ func TestAccResourceInvalidSlo(t *testing.T) { }, }) } + +const bigTentQuery = ` + [ + { + "aggregation": "Sum", + "alias": "", + "application": "57831", + "applicationName": "petclinic", + "datasource": { + "type": "dlopes7-appdynamics-datasource", + "uid": "appdynamics_localdev" + }, + "delimiter": "|", + "isRawQuery": false, + "metric": "Overall Application Performance|Calls per Minute", + "queryType": "metrics", + "refId": "total", + "rollUp": true, + "schemaVersion": "3.9.5", + "transformLegend": "Segments", + "transformLegendText": "" + }, + { + "aggregation": "Sum", + "alias": "", + "application": "57831", + "applicationName": "petclinic", + "datasource": { + "type": "dlopes7-appdynamics-datasource", + "uid": "appdynamics_localdev" + }, + "intervalMs": 1000, + "maxDataPoints": 43200, + "delimiter": "|", + "isRawQuery": false, + "metric": "Overall Application Performance|Calls per Minute", + "queryType": "metrics", + "refId": "also_total", + "rollUp": true, + "schemaVersion": "3.9.5", + "transformLegend": "Segments", + "transformLegendText": "" + }, + { + "conditions": [ + { + "evaluator": { + "params": [ + 0, + 0 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "datasource": { + "name": "Expression", + "type": "__expr__", + "uid": "__expr__" + }, + "expression": "($total / $also_total)", + "intervalMs": 1000, + "maxDataPoints": 43200, + "refId": "C", + "type": "math" + } + ]` + +const bigTentNoDataSource = `[{}]` +const bigTentMissingFields = `[{"datasource":{}}]` + +func TestValidateBigTent(t *testing.T) { + tests := map[string]struct { + query string + expectedWarnings []string + expectedErrors []error + }{ + "prometheus": { + query: "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))", + expectedWarnings: []string{ + "\"Test\" contains an invalid JSON: invalid character 's' looking for beginning of value", + }, + expectedErrors: []error{}, + }, + "bigTent_success": { + query: bigTentQuery, + expectedWarnings: []string{}, + expectedErrors: []error{}, + }, + "bigTent_noDatasource": { + query: bigTentNoDataSource, + expectedWarnings: []string{}, + expectedErrors: []error{fmt.Errorf("expected Big Tent Query map[] to have a datasource")}, + }, + "bigTent_missingFields": { + query: bigTentMissingFields, + expectedWarnings: []string{}, + expectedErrors: []error{fmt.Errorf("expected Big Tent Query datasource map[] to have a type"), + fmt.Errorf("expected Big Tent Query datasource map[] to have a uid")}, + }, + } + testFunc := slo2.ValidateBigTent() + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + warnings, errs := testFunc(tc.query, "Test") + + require.Len(t, warnings, len(tc.expectedWarnings)) + for i, w := range tc.expectedWarnings { + assert.Equal(t, w, warnings[i]) + } + + require.Len(t, errs, len(tc.expectedErrors)) + for i, e := range tc.expectedErrors { + assert.Equal(t, e, errs[i]) + } + + }) + } + +} From f514eb0f15d22fd3d8db3efd97a93fcdd4cb9d5c Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Mon, 3 Feb 2025 09:26:12 -0800 Subject: [PATCH 03/22] feat(bigTent slo): fmt --- internal/resources/slo/resource_slo_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/resources/slo/resource_slo_test.go b/internal/resources/slo/resource_slo_test.go index b08158fef..14f7d37c8 100644 --- a/internal/resources/slo/resource_slo_test.go +++ b/internal/resources/slo/resource_slo_test.go @@ -541,8 +541,6 @@ func TestValidateBigTent(t *testing.T) { for i, e := range tc.expectedErrors { assert.Equal(t, e, errs[i]) } - }) } - } From 1efd39b752d0436c6f261af31e8e71b8bd1675b0 Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Mon, 3 Feb 2025 12:00:30 -0800 Subject: [PATCH 04/22] feat(bigTent slo): removing derpecated reliance --- internal/resources/slo/resource_slo.go | 99 ++++++++++----- internal/resources/slo/resource_slo_test.go | 133 ++++++++++++-------- 2 files changed, 150 insertions(+), 82 deletions(-) diff --git a/internal/resources/slo/resource_slo.go b/internal/resources/slo/resource_slo.go index 2cd9518eb..82e215c76 100644 --- a/internal/resources/slo/resource_slo.go +++ b/internal/resources/slo/resource_slo.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" + "github.com/hashicorp/go-cty/cty" "regexp" "github.com/grafana/slo-openapi-client/go/slo" @@ -92,10 +92,10 @@ Resource manages Grafana SLOs. Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "query": { - Type: schema.TypeString, - Required: true, - Description: "Freeform Query Field", - ValidateFunc: ValidateBigTent(), + Type: schema.TypeString, + Required: true, + Description: "Freeform Query Field", + ValidateDiagFunc: ValidateBigTent(), }, }, }, @@ -107,16 +107,16 @@ Resource manages Grafana SLOs. Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "success_metric": { - Type: schema.TypeString, - Description: `Counter metric for success events (numerator)`, - Required: true, - ValidateFunc: ValidateBigTent(), + Type: schema.TypeString, + Description: `Counter metric for success events (numerator)`, + Required: true, + ValidateDiagFunc: ValidateBigTent(), }, "total_metric": { - Type: schema.TypeString, - Description: `Metric for total events (denominator)`, - Required: true, - ValidateFunc: ValidateBigTent(), + Type: schema.TypeString, + Description: `Metric for total events (denominator)`, + Required: true, + ValidateDiagFunc: ValidateBigTent(), }, "group_by_labels": { Type: schema.TypeList, @@ -667,46 +667,79 @@ func apiError(action string, err error) diag.Diagnostics { } } -func ValidateBigTent() schema.SchemaValidateFunc { - return func(i interface{}, k string) (warnings []string, errors []error) { +func ValidateBigTent() schema.SchemaValidateDiagFunc { + return func(i interface{}, path cty.Path) diag.Diagnostics { + var diags diag.Diagnostics + v, ok := i.(string) if !ok { - errors = append(errors, fmt.Errorf("expected type of %s to be string", k)) - return warnings, errors - } - - normString, err := structure.NormalizeJsonString(v) - if err != nil { - warnings = append(warnings, fmt.Sprintf("%q contains an invalid JSON: %s", k, err)) - return warnings, errors + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Bad Format", + Detail: fmt.Sprintf("expected type of %s to be string", path), + AttributePath: path, + }) } var gmrQuery []map[string]any - err = json.Unmarshal([]byte(normString), &gmrQuery) + err := json.Unmarshal([]byte(v), &gmrQuery) if err != nil { - warnings = append(warnings, fmt.Sprintf("%v", err)) - // If queries aren't valid JSON, we'll attempt to validate as promQL - return warnings, errors + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Bad JSON format", + Detail: "If this is a big tent query, this should be valid JSON. If this is a prometheus query, ignore this.", + AttributePath: path, + }) + return diags } for _, queryObj := range gmrQuery { + currentPath := path.Copy() + + refId, ok := queryObj["refId"] + if !ok { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Missing Required Field", + Detail: fmt.Sprintf("expected Big Tent Query %v to have a refId", queryObj), + AttributePath: append(currentPath, cty.IndexStep{Key: cty.StringVal("refId")}), + }) + return diags + } + source := queryObj["datasource"] s, ok := source.(map[string]interface{}) if !ok { - errors = append(errors, fmt.Errorf("expected Big Tent Query %v to have a datasource", queryObj)) - return warnings, errors + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Missing Required Field", + Detail: fmt.Sprintf("expected Big Tent Query (refId:%v) to have a datasource", refId), + AttributePath: append(currentPath, cty.IndexStep{Key: cty.StringVal("datasource")}), + }) + return diags } + currentPath = append(currentPath, cty.IndexStep{Key: cty.StringVal("datasource")}) _, ok = s["type"] if !ok { - errors = append(errors, fmt.Errorf("expected Big Tent Query datasource %v to have a type", s)) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Missing Required Field", + Detail: fmt.Sprintf("expected Big Tent Query (refId:%v) to have a type", refId), + AttributePath: append(currentPath.Copy(), cty.IndexStep{Key: cty.StringVal("type")}), + }) } _, ok = s["uid"] if !ok { - errors = append(errors, fmt.Errorf("expected Big Tent Query datasource %v to have a uid", s)) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Missing Required Field", + Detail: fmt.Sprintf("expected Big Tent Query (refId:%v) to have a uid", refId), + AttributePath: append(currentPath.Copy(), cty.IndexStep{Key: cty.StringVal("uid")}), + }) } - } - return warnings, errors + } + return diags } } diff --git a/internal/resources/slo/resource_slo_test.go b/internal/resources/slo/resource_slo_test.go index 14f7d37c8..3f533577a 100644 --- a/internal/resources/slo/resource_slo_test.go +++ b/internal/resources/slo/resource_slo_test.go @@ -2,9 +2,13 @@ package slo_test import ( "context" + "encoding/json" "errors" "fmt" + "github.com/grafana/grafana-openapi-client-go/models" slo2 "github.com/grafana/terraform-provider-grafana/v3/internal/resources/slo" + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/stretchr/testify/assert" "regexp" "strings" @@ -415,7 +419,82 @@ func TestAccResourceInvalidSlo(t *testing.T) { }) } -const bigTentQuery = ` +func TestValidateBigTent(t *testing.T) { + tests := map[string]struct { + query string + expectedDiags diag.Diagnostics + }{ + "prometheus": { + query: "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))", + expectedDiags: diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Bad JSON format", + Detail: "If this is a big tent query, this should be valid JSON. If this is a prometheus query, ignore this.", + AttributePath: cty.IndexPath(cty.Value{}), + }, + }, + }, + "bigTent_success": { + query: createBigTent(true, []map[string]any{}), + expectedDiags: diag.Diagnostics{}, + }, + "bigTent_noRefId": { + query: createBigTent(false, []map[string]any{{}}), + expectedDiags: diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Missing Required Field", + Detail: fmt.Sprintf("expected Big Tent Query %v to have a refId", "map[]"), + AttributePath: append(cty.IndexPath(cty.Value{}), cty.IndexStep{Key: cty.StringVal("refId")}), + }, + }, + }, + "bigTent_noDatasource": { + query: createBigTent(false, []map[string]any{{"refId": "A"}}), + expectedDiags: diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Missing Required Field", + Detail: "expected Big Tent Query (refId:A) to have a datasource", + AttributePath: append(cty.IndexPath(cty.Value{}), cty.IndexStep{Key: cty.StringVal("datasource")}), + }, + }, + }, + "bigTent_missingFields": { + query: createBigTent(false, []map[string]any{{"refId": "A", "datasource": models.DataSource{}}}), + expectedDiags: diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Missing Required Field", + Detail: "expected Big Tent Query (refId:A) to have a type", + AttributePath: append(cty.IndexPath(cty.Value{}), cty.IndexStep{Key: cty.StringVal("datasource")}, cty.IndexStep{Key: cty.StringVal("type")}), + }, + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Missing Required Field", + Detail: "expected Big Tent Query (refId:A) to have a uid", + AttributePath: append(cty.IndexPath(cty.Value{}), cty.IndexStep{Key: cty.StringVal("datasource")}, cty.IndexStep{Key: cty.StringVal("uid")}), + }, + }, + }, + } + testFunc := slo2.ValidateBigTent() + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + diags := testFunc(tc.query, cty.IndexPath(cty.Value{})) + + require.Len(t, diags, len(tc.expectedDiags)) + for i, w := range tc.expectedDiags { + assert.Equal(t, w, diags[i]) + } + }) + } +} + +func createBigTent(useDefault bool, input []map[string]any) string { + const bigTentQuery = ` [ { "aggregation": "Sum", @@ -493,54 +572,10 @@ const bigTentQuery = ` } ]` -const bigTentNoDataSource = `[{}]` -const bigTentMissingFields = `[{"datasource":{}}]` - -func TestValidateBigTent(t *testing.T) { - tests := map[string]struct { - query string - expectedWarnings []string - expectedErrors []error - }{ - "prometheus": { - query: "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))", - expectedWarnings: []string{ - "\"Test\" contains an invalid JSON: invalid character 's' looking for beginning of value", - }, - expectedErrors: []error{}, - }, - "bigTent_success": { - query: bigTentQuery, - expectedWarnings: []string{}, - expectedErrors: []error{}, - }, - "bigTent_noDatasource": { - query: bigTentNoDataSource, - expectedWarnings: []string{}, - expectedErrors: []error{fmt.Errorf("expected Big Tent Query map[] to have a datasource")}, - }, - "bigTent_missingFields": { - query: bigTentMissingFields, - expectedWarnings: []string{}, - expectedErrors: []error{fmt.Errorf("expected Big Tent Query datasource map[] to have a type"), - fmt.Errorf("expected Big Tent Query datasource map[] to have a uid")}, - }, + if useDefault { + return bigTentQuery } - testFunc := slo2.ValidateBigTent() - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - warnings, errs := testFunc(tc.query, "Test") - - require.Len(t, warnings, len(tc.expectedWarnings)) - for i, w := range tc.expectedWarnings { - assert.Equal(t, w, warnings[i]) - } - require.Len(t, errs, len(tc.expectedErrors)) - for i, e := range tc.expectedErrors { - assert.Equal(t, e, errs[i]) - } - }) - } + output, _ := json.Marshal(input) + return string(output) } From 2c6b7c9e681b96cb638ccaa9e570c3f4f9c34a41 Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Mon, 3 Feb 2025 12:04:14 -0800 Subject: [PATCH 05/22] feat(bigTent slo): fmt --- internal/resources/slo/resource_slo.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/internal/resources/slo/resource_slo.go b/internal/resources/slo/resource_slo.go index 82e215c76..e3d1b0600 100644 --- a/internal/resources/slo/resource_slo.go +++ b/internal/resources/slo/resource_slo.go @@ -4,12 +4,12 @@ import ( "context" "encoding/json" "fmt" - "github.com/hashicorp/go-cty/cty" "regexp" "github.com/grafana/slo-openapi-client/go/slo" "github.com/grafana/terraform-provider-grafana/v3/internal/common" + "github.com/hashicorp/go-cty/cty" "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" @@ -696,13 +696,13 @@ func ValidateBigTent() schema.SchemaValidateDiagFunc { for _, queryObj := range gmrQuery { currentPath := path.Copy() - refId, ok := queryObj["refId"] + refID, ok := queryObj["refID"] if !ok { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, Summary: "Missing Required Field", - Detail: fmt.Sprintf("expected Big Tent Query %v to have a refId", queryObj), - AttributePath: append(currentPath, cty.IndexStep{Key: cty.StringVal("refId")}), + Detail: fmt.Sprintf("expected Big Tent Query %v to have a refID", queryObj), + AttributePath: append(currentPath, cty.IndexStep{Key: cty.StringVal("refID")}), }) return diags } @@ -713,7 +713,7 @@ func ValidateBigTent() schema.SchemaValidateDiagFunc { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, Summary: "Missing Required Field", - Detail: fmt.Sprintf("expected Big Tent Query (refId:%v) to have a datasource", refId), + Detail: fmt.Sprintf("expected Big Tent Query (refID:%v) to have a datasource", refID), AttributePath: append(currentPath, cty.IndexStep{Key: cty.StringVal("datasource")}), }) return diags @@ -725,7 +725,7 @@ func ValidateBigTent() schema.SchemaValidateDiagFunc { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, Summary: "Missing Required Field", - Detail: fmt.Sprintf("expected Big Tent Query (refId:%v) to have a type", refId), + Detail: fmt.Sprintf("expected Big Tent Query (refID:%v) to have a type", refID), AttributePath: append(currentPath.Copy(), cty.IndexStep{Key: cty.StringVal("type")}), }) } @@ -734,11 +734,10 @@ func ValidateBigTent() schema.SchemaValidateDiagFunc { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, Summary: "Missing Required Field", - Detail: fmt.Sprintf("expected Big Tent Query (refId:%v) to have a uid", refId), + Detail: fmt.Sprintf("expected Big Tent Query (refID:%v) to have a uid", refID), AttributePath: append(currentPath.Copy(), cty.IndexStep{Key: cty.StringVal("uid")}), }) } - } return diags } From 7da58a26595f9c46f7cba8d8eff45ce81bc6bb13 Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Mon, 3 Feb 2025 12:08:18 -0800 Subject: [PATCH 06/22] feat(bigTent slo): fmt --- internal/resources/slo/resource_slo_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/resources/slo/resource_slo_test.go b/internal/resources/slo/resource_slo_test.go index 3f533577a..03bc4893b 100644 --- a/internal/resources/slo/resource_slo_test.go +++ b/internal/resources/slo/resource_slo_test.go @@ -5,15 +5,16 @@ import ( "encoding/json" "errors" "fmt" + "regexp" + "strings" + "testing" + "time" + "github.com/grafana/grafana-openapi-client-go/models" slo2 "github.com/grafana/terraform-provider-grafana/v3/internal/resources/slo" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/stretchr/testify/assert" - "regexp" - "strings" - "testing" - "time" "github.com/grafana/slo-openapi-client/go/slo" "github.com/grafana/terraform-provider-grafana/v3/internal/common" From 9697dc642122b882f06dfcf6a49a3f798e6a5f56 Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Mon, 3 Feb 2025 12:21:46 -0800 Subject: [PATCH 07/22] feat(bigTent slo): fix overzealous id refactor --- internal/resources/slo/resource_slo.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/resources/slo/resource_slo.go b/internal/resources/slo/resource_slo.go index e3d1b0600..57ad63ea9 100644 --- a/internal/resources/slo/resource_slo.go +++ b/internal/resources/slo/resource_slo.go @@ -696,13 +696,13 @@ func ValidateBigTent() schema.SchemaValidateDiagFunc { for _, queryObj := range gmrQuery { currentPath := path.Copy() - refID, ok := queryObj["refID"] + refID, ok := queryObj["refId"] if !ok { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, Summary: "Missing Required Field", - Detail: fmt.Sprintf("expected Big Tent Query %v to have a refID", queryObj), - AttributePath: append(currentPath, cty.IndexStep{Key: cty.StringVal("refID")}), + Detail: fmt.Sprintf("expected Big Tent Query %v to have a refId", queryObj), + AttributePath: append(currentPath, cty.IndexStep{Key: cty.StringVal("refId")}), }) return diags } @@ -713,7 +713,7 @@ func ValidateBigTent() schema.SchemaValidateDiagFunc { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, Summary: "Missing Required Field", - Detail: fmt.Sprintf("expected Big Tent Query (refID:%v) to have a datasource", refID), + Detail: fmt.Sprintf("expected Big Tent Query (refId:%v) to have a datasource", refID), AttributePath: append(currentPath, cty.IndexStep{Key: cty.StringVal("datasource")}), }) return diags @@ -725,7 +725,7 @@ func ValidateBigTent() schema.SchemaValidateDiagFunc { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, Summary: "Missing Required Field", - Detail: fmt.Sprintf("expected Big Tent Query (refID:%v) to have a type", refID), + Detail: fmt.Sprintf("expected Big Tent Query (refId:%v) to have a type", refID), AttributePath: append(currentPath.Copy(), cty.IndexStep{Key: cty.StringVal("type")}), }) } @@ -734,7 +734,7 @@ func ValidateBigTent() schema.SchemaValidateDiagFunc { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, Summary: "Missing Required Field", - Detail: fmt.Sprintf("expected Big Tent Query (refID:%v) to have a uid", refID), + Detail: fmt.Sprintf("expected Big Tent Query (refId:%v) to have a uid", refID), AttributePath: append(currentPath.Copy(), cty.IndexStep{Key: cty.StringVal("uid")}), }) } From 6b77ed84182d50a36dc6d6c274d511e028c2d70f Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Tue, 4 Feb 2025 07:50:13 -0800 Subject: [PATCH 08/22] feat(bigTent slo): ignore http redirect for link checker --- .linkcheckerrc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.linkcheckerrc b/.linkcheckerrc index 666e68c97..61ec0b1df 100644 --- a/.linkcheckerrc +++ b/.linkcheckerrc @@ -3,6 +3,8 @@ checkextern=1 ignore= # https://regex101.com/r/Pl0jCn/1 \/\*\!sc\*\/ +ignorewarningsforurls= + ^https://jqlang\.github\.io/jq/ ^http-redirected [MarkdownCheck] filename_re=.*\.md From 9dcb2f628424f29aad58e9ea1368eab6657b68c1 Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Fri, 7 Feb 2025 14:05:05 -0800 Subject: [PATCH 09/22] feat(bigTent slo): Drop warning on Prometheus queries --- internal/resources/slo/resource_slo.go | 7 +------ internal/resources/slo/resource_slo_test.go | 11 ++--------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/internal/resources/slo/resource_slo.go b/internal/resources/slo/resource_slo.go index 57ad63ea9..a4befce4c 100644 --- a/internal/resources/slo/resource_slo.go +++ b/internal/resources/slo/resource_slo.go @@ -679,17 +679,12 @@ func ValidateBigTent() schema.SchemaValidateDiagFunc { Detail: fmt.Sprintf("expected type of %s to be string", path), AttributePath: path, }) + return diags } var gmrQuery []map[string]any err := json.Unmarshal([]byte(v), &gmrQuery) if err != nil { - diags = append(diags, diag.Diagnostic{ - Severity: diag.Warning, - Summary: "Bad JSON format", - Detail: "If this is a big tent query, this should be valid JSON. If this is a prometheus query, ignore this.", - AttributePath: path, - }) return diags } diff --git a/internal/resources/slo/resource_slo_test.go b/internal/resources/slo/resource_slo_test.go index 03bc4893b..0df84dcec 100644 --- a/internal/resources/slo/resource_slo_test.go +++ b/internal/resources/slo/resource_slo_test.go @@ -426,15 +426,8 @@ func TestValidateBigTent(t *testing.T) { expectedDiags diag.Diagnostics }{ "prometheus": { - query: "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))", - expectedDiags: diag.Diagnostics{ - diag.Diagnostic{ - Severity: diag.Warning, - Summary: "Bad JSON format", - Detail: "If this is a big tent query, this should be valid JSON. If this is a prometheus query, ignore this.", - AttributePath: cty.IndexPath(cty.Value{}), - }, - }, + query: "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))", + expectedDiags: diag.Diagnostics{}, }, "bigTent_success": { query: createBigTent(true, []map[string]any{}), From e60b74177d71f939708cd98d1160a6ef2c71387c Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Mon, 10 Feb 2025 15:00:42 -0800 Subject: [PATCH 10/22] chore(bigTent slo): WIP update documentation --- .../grafana_slo/resource_nonprometheus.tf | 44 +++++++++++++++++++ templates/resources/slo.md.tmpl | 4 ++ 2 files changed, 48 insertions(+) create mode 100644 examples/resources/grafana_slo/resource_nonprometheus.tf diff --git a/examples/resources/grafana_slo/resource_nonprometheus.tf b/examples/resources/grafana_slo/resource_nonprometheus.tf new file mode 100644 index 000000000..97cd715a2 --- /dev/null +++ b/examples/resources/grafana_slo/resource_nonprometheus.tf @@ -0,0 +1,44 @@ +resource "grafana_slo" "test" { + name = "Terraform Testing" + description = "Terraform Description" + query { + freeform { + query = "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))" + } + type = "freeform" + } + objectives { + value = 0.995 + window = "30d" + } + destination_datasource { + uid = "grafanacloud-prom" + } + label { + key = "slo" + value = "terraform" + } + alerting { + fastburn { + annotation { + key = "name" + value = "SLO Burn Rate Very High" + } + annotation { + key = "description" + value = "Error budget is burning too fast" + } + } + + slowburn { + annotation { + key = "name" + value = "SLO Burn Rate High" + } + annotation { + key = "description" + value = "Error budget is burning too fast" + } + } + } +} \ No newline at end of file diff --git a/templates/resources/slo.md.tmpl b/templates/resources/slo.md.tmpl index 330801ef3..e77ceef1d 100644 --- a/templates/resources/slo.md.tmpl +++ b/templates/resources/slo.md.tmpl @@ -20,6 +20,10 @@ description: |- {{ tffile "examples/resources/grafana_slo/resource_complex.tf" }} +## Non Prometheus + +{{ tffile "examples/resources/grafana_slo/resource_nonprometheus.tf" }} + {{ .SchemaMarkdown | trimspace }} ## Import From 4881fefec912d0988d9e7b0838cd5145d98ca855 Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Tue, 11 Feb 2025 11:03:22 -0800 Subject: [PATCH 11/22] chore(bigTent slo): update documentation --- docs/resources/slo.md | 107 +++++++++++++++++- .../grafana_slo/resource_nonprometheus.tf | 54 ++++++++- internal/resources/slo/resource_slo.go | 2 +- templates/resources/slo.md.tmpl | 4 + 4 files changed, 164 insertions(+), 3 deletions(-) diff --git a/docs/resources/slo.md b/docs/resources/slo.md index fc1f25b8b..74e3769f2 100644 --- a/docs/resources/slo.md +++ b/docs/resources/slo.md @@ -125,6 +125,111 @@ resource "grafana_slo" "test" { } ``` +## Non Prometheus + +Non prometheus queries use the freeform query field. It expects a JSON string list of valid grafana query JSON objects. +You can see more examples of correct JSON structure from using the export button on a Grafana-managed rule definition +created by the SLO app, under the model field. + +```terraform +resource "grafana_slo" "test" { + name = "Terraform Testing" + description = "Terraform Description" + query { + freeform { + query = jsonencode([ + { + aggregation: "Sum", + alias: "", + application: "57831", + applicationName: "petclinic", + datasource: { + "type": "dlopes7-appdynamics-datasource", + "uid": "appdynamics_localdev" + }, + delimiter: "|", + isRawQuery: false, + metric: "Service Endpoints|PetClinicEastTier1|/petclinic/api_SERVLET|Errors per Minute", + queryType: "metrics", + refId: "errors", + rollUp: true, + schemaVersion: "3.9.5", + transformLegend: "Segments", + transformLegendText: "" + }, + { + aggregation: "Sum", + alias: "", + application: "57831", + applicationName: "petclinic", + datasource: { + "type": "dlopes7-appdynamics-datasource", + "uid": "appdynamics_localdev" + }, + intervalMs: 1000, + maxDataPoints:43200, + delimiter: "|", + isRawQuery: false, + metric: "Service Endpoints|PetClinicEastTier1|/petclinic/api_SERVLET|Calls per Minute", + queryType: "metrics", + refId: "total", + rollUp: true, + schemaVersion: "3.9.5", + transformLegend: "Segments", + transformLegendText: "" + }, + { + datasource: { + "type": "__expr__", + "uid": "__expr__" + }, + expression: "($total - $errors) / $total", + intervalMs: 1000, + maxDataPoints: 43200, + refId: "C", + type: "math" + } + ]) + } + type = "freeform" + } + objectives { + value = 0.995 + window = "30d" + } + destination_datasource { + uid = "grafanacloud-prom" + } + label { + key = "slo" + value = "terraform" + } + alerting { + fastburn { + annotation { + key = "name" + value = "SLO Burn Rate Very High" + } + annotation { + key = "description" + value = "Error budget is burning too fast" + } + } + + slowburn { + annotation { + key = "name" + value = "SLO Burn Rate High" + } + annotation { + key = "description" + value = "Error budget is burning too fast" + } + } + } +} +``` + ## Schema @@ -185,7 +290,7 @@ Optional: Required: -- `query` (String) Freeform Query Field +- `query` (String) Freeform Query Field - valid promQl or JSON formatted string diff --git a/examples/resources/grafana_slo/resource_nonprometheus.tf b/examples/resources/grafana_slo/resource_nonprometheus.tf index 97cd715a2..0644cfae8 100644 --- a/examples/resources/grafana_slo/resource_nonprometheus.tf +++ b/examples/resources/grafana_slo/resource_nonprometheus.tf @@ -3,7 +3,59 @@ resource "grafana_slo" "test" { description = "Terraform Description" query { freeform { - query = "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))" + query = jsonencode([ + { + aggregation: "Sum", + alias: "", + application: "57831", + applicationName: "petclinic", + datasource: { + "type": "dlopes7-appdynamics-datasource", + "uid": "appdynamics_localdev" + }, + delimiter: "|", + isRawQuery: false, + metric: "Service Endpoints|PetClinicEastTier1|/petclinic/api_SERVLET|Errors per Minute", + queryType: "metrics", + refId: "errors", + rollUp: true, + schemaVersion: "3.9.5", + transformLegend: "Segments", + transformLegendText: "" + }, + { + aggregation: "Sum", + alias: "", + application: "57831", + applicationName: "petclinic", + datasource: { + "type": "dlopes7-appdynamics-datasource", + "uid": "appdynamics_localdev" + }, + intervalMs: 1000, + maxDataPoints:43200, + delimiter: "|", + isRawQuery: false, + metric: "Service Endpoints|PetClinicEastTier1|/petclinic/api_SERVLET|Calls per Minute", + queryType: "metrics", + refId: "total", + rollUp: true, + schemaVersion: "3.9.5", + transformLegend: "Segments", + transformLegendText: "" + }, + { + datasource: { + "type": "__expr__", + "uid": "__expr__" + }, + expression: "($total - $errors) / $total", + intervalMs: 1000, + maxDataPoints: 43200, + refId: "C", + type: "math" + } + ]) } type = "freeform" } diff --git a/internal/resources/slo/resource_slo.go b/internal/resources/slo/resource_slo.go index a4befce4c..dc5f30b0e 100644 --- a/internal/resources/slo/resource_slo.go +++ b/internal/resources/slo/resource_slo.go @@ -94,7 +94,7 @@ Resource manages Grafana SLOs. "query": { Type: schema.TypeString, Required: true, - Description: "Freeform Query Field", + Description: "Freeform Query Field - valid promQl or JSON formatted string", ValidateDiagFunc: ValidateBigTent(), }, }, diff --git a/templates/resources/slo.md.tmpl b/templates/resources/slo.md.tmpl index e77ceef1d..4b9752f97 100644 --- a/templates/resources/slo.md.tmpl +++ b/templates/resources/slo.md.tmpl @@ -22,6 +22,10 @@ description: |- ## Non Prometheus +Non prometheus queries use the freeform query field. It expects a JSON string list of valid grafana query JSON objects. +You can see more examples of correct JSON structure from using the export button on a Grafana-managed rule definition +created by the SLO app, under the model field. + {{ tffile "examples/resources/grafana_slo/resource_nonprometheus.tf" }} {{ .SchemaMarkdown | trimspace }} From a392c846cf35b75507847f53f43e5d1a1c1d3fff Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Tue, 11 Feb 2025 11:24:05 -0800 Subject: [PATCH 12/22] chore(bigTent slo): update documentation --- docs/resources/slo.md | 84 +++++++++---------- .../grafana_slo/resource_nonprometheus.tf | 84 +++++++++---------- 2 files changed, 84 insertions(+), 84 deletions(-) diff --git a/docs/resources/slo.md b/docs/resources/slo.md index 74e3769f2..264fb02b3 100644 --- a/docs/resources/slo.md +++ b/docs/resources/slo.md @@ -139,55 +139,55 @@ resource "grafana_slo" "test" { freeform { query = jsonencode([ { - aggregation: "Sum", - alias: "", - application: "57831", - applicationName: "petclinic", - datasource: { - "type": "dlopes7-appdynamics-datasource", - "uid": "appdynamics_localdev" + aggregation : "Sum", + alias : "", + application : "57831", + applicationName : "petclinic", + datasource : { + "type" : "dlopes7-appdynamics-datasource", + "uid" : "appdynamics_localdev" }, - delimiter: "|", - isRawQuery: false, - metric: "Service Endpoints|PetClinicEastTier1|/petclinic/api_SERVLET|Errors per Minute", - queryType: "metrics", - refId: "errors", - rollUp: true, - schemaVersion: "3.9.5", - transformLegend: "Segments", - transformLegendText: "" + delimiter : "|", + isRawQuery : false, + metric : "Service Endpoints|PetClinicEastTier1|/petclinic/api_SERVLET|Errors per Minute", + queryType : "metrics", + refId : "errors", + rollUp : true, + schemaVersion : "3.9.5", + transformLegend : "Segments", + transformLegendText : "" }, { - aggregation: "Sum", - alias: "", - application: "57831", - applicationName: "petclinic", - datasource: { - "type": "dlopes7-appdynamics-datasource", - "uid": "appdynamics_localdev" + aggregation : "Sum", + alias : "", + application : "57831", + applicationName : "petclinic", + datasource : { + "type" : "dlopes7-appdynamics-datasource", + "uid" : "appdynamics_localdev" }, - intervalMs: 1000, - maxDataPoints:43200, - delimiter: "|", - isRawQuery: false, - metric: "Service Endpoints|PetClinicEastTier1|/petclinic/api_SERVLET|Calls per Minute", - queryType: "metrics", - refId: "total", - rollUp: true, - schemaVersion: "3.9.5", - transformLegend: "Segments", - transformLegendText: "" + intervalMs : 1000, + maxDataPoints : 43200, + delimiter : "|", + isRawQuery : false, + metric : "Service Endpoints|PetClinicEastTier1|/petclinic/api_SERVLET|Calls per Minute", + queryType : "metrics", + refId : "total", + rollUp : true, + schemaVersion : "3.9.5", + transformLegend : "Segments", + transformLegendText : "" }, { - datasource: { - "type": "__expr__", - "uid": "__expr__" + datasource : { + "type" : "__expr__", + "uid" : "__expr__" }, - expression: "($total - $errors) / $total", - intervalMs: 1000, - maxDataPoints: 43200, - refId: "C", - type: "math" + expression : "($total - $errors) / $total", + intervalMs : 1000, + maxDataPoints : 43200, + refId : "C", + type : "math" } ]) } diff --git a/examples/resources/grafana_slo/resource_nonprometheus.tf b/examples/resources/grafana_slo/resource_nonprometheus.tf index 0644cfae8..404bf7bab 100644 --- a/examples/resources/grafana_slo/resource_nonprometheus.tf +++ b/examples/resources/grafana_slo/resource_nonprometheus.tf @@ -5,55 +5,55 @@ resource "grafana_slo" "test" { freeform { query = jsonencode([ { - aggregation: "Sum", - alias: "", - application: "57831", - applicationName: "petclinic", - datasource: { - "type": "dlopes7-appdynamics-datasource", - "uid": "appdynamics_localdev" + aggregation : "Sum", + alias : "", + application : "57831", + applicationName : "petclinic", + datasource : { + "type" : "dlopes7-appdynamics-datasource", + "uid" : "appdynamics_localdev" }, - delimiter: "|", - isRawQuery: false, - metric: "Service Endpoints|PetClinicEastTier1|/petclinic/api_SERVLET|Errors per Minute", - queryType: "metrics", - refId: "errors", - rollUp: true, - schemaVersion: "3.9.5", - transformLegend: "Segments", - transformLegendText: "" + delimiter : "|", + isRawQuery : false, + metric : "Service Endpoints|PetClinicEastTier1|/petclinic/api_SERVLET|Errors per Minute", + queryType : "metrics", + refId : "errors", + rollUp : true, + schemaVersion : "3.9.5", + transformLegend : "Segments", + transformLegendText : "" }, { - aggregation: "Sum", - alias: "", - application: "57831", - applicationName: "petclinic", - datasource: { - "type": "dlopes7-appdynamics-datasource", - "uid": "appdynamics_localdev" + aggregation : "Sum", + alias : "", + application : "57831", + applicationName : "petclinic", + datasource : { + "type" : "dlopes7-appdynamics-datasource", + "uid" : "appdynamics_localdev" }, - intervalMs: 1000, - maxDataPoints:43200, - delimiter: "|", - isRawQuery: false, - metric: "Service Endpoints|PetClinicEastTier1|/petclinic/api_SERVLET|Calls per Minute", - queryType: "metrics", - refId: "total", - rollUp: true, - schemaVersion: "3.9.5", - transformLegend: "Segments", - transformLegendText: "" + intervalMs : 1000, + maxDataPoints : 43200, + delimiter : "|", + isRawQuery : false, + metric : "Service Endpoints|PetClinicEastTier1|/petclinic/api_SERVLET|Calls per Minute", + queryType : "metrics", + refId : "total", + rollUp : true, + schemaVersion : "3.9.5", + transformLegend : "Segments", + transformLegendText : "" }, { - datasource: { - "type": "__expr__", - "uid": "__expr__" + datasource : { + "type" : "__expr__", + "uid" : "__expr__" }, - expression: "($total - $errors) / $total", - intervalMs: 1000, - maxDataPoints: 43200, - refId: "C", - type: "math" + expression : "($total - $errors) / $total", + intervalMs : 1000, + maxDataPoints : 43200, + refId : "C", + type : "math" } ]) } From ac83149602816af22686b62dfad90ac201a189ef Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Tue, 11 Feb 2025 11:25:12 -0800 Subject: [PATCH 13/22] chore(bigTent slo): update documentation --- .linkcheckerrc | 2 -- 1 file changed, 2 deletions(-) diff --git a/.linkcheckerrc b/.linkcheckerrc index 61ec0b1df..666e68c97 100644 --- a/.linkcheckerrc +++ b/.linkcheckerrc @@ -3,8 +3,6 @@ checkextern=1 ignore= # https://regex101.com/r/Pl0jCn/1 \/\*\!sc\*\/ -ignorewarningsforurls= - ^https://jqlang\.github\.io/jq/ ^http-redirected [MarkdownCheck] filename_re=.*\.md From ffd495ba82e7ff32bfa83c04a651ff466ff949b1 Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Tue, 11 Feb 2025 12:33:16 -0800 Subject: [PATCH 14/22] chore(doc links): fixing terraform provider doc links --- templates/resources/slo.md.tmpl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/templates/resources/slo.md.tmpl b/templates/resources/slo.md.tmpl index 4b9752f97..64d8ae779 100644 --- a/templates/resources/slo.md.tmpl +++ b/templates/resources/slo.md.tmpl @@ -20,9 +20,11 @@ description: |- {{ tffile "examples/resources/grafana_slo/resource_complex.tf" }} -## Non Prometheus +## Enterprise Datasources -Non prometheus queries use the freeform query field. It expects a JSON string list of valid grafana query JSON objects. +Currently supported datasources: AppDynamics, Splunk, Graphite + +Enterprise Datasource queries use the freeform query field. It expects a JSON string list of valid grafana query JSON objects. You can see more examples of correct JSON structure from using the export button on a Grafana-managed rule definition created by the SLO app, under the model field. From 6fab993421bfc7352827ef6eb2960fa951b0c6a8 Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Tue, 11 Feb 2025 12:50:37 -0800 Subject: [PATCH 15/22] chore(doc links): fixing terraform provider doc links --- docs/resources/slo.md | 9 +++++---- templates/resources/slo.md.tmpl | 3 +-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/resources/slo.md b/docs/resources/slo.md index 264fb02b3..3ca8e5ec8 100644 --- a/docs/resources/slo.md +++ b/docs/resources/slo.md @@ -125,11 +125,12 @@ resource "grafana_slo" "test" { } ``` -## Non Prometheus +## Enterprise Datasources -Non prometheus queries use the freeform query field. It expects a JSON string list of valid grafana query JSON objects. -You can see more examples of correct JSON structure from using the export button on a Grafana-managed rule definition -created by the SLO app, under the model field. +Currently supported datasources: AppDynamics, Splunk, Graphite + +Enterprise Datasource queries use the freeform query field. It expects a JSON string list of valid grafana query JSON objects. +For additional help, view our [documentation](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/). ```terraform resource "grafana_slo" "test" { diff --git a/templates/resources/slo.md.tmpl b/templates/resources/slo.md.tmpl index 64d8ae779..31441d29a 100644 --- a/templates/resources/slo.md.tmpl +++ b/templates/resources/slo.md.tmpl @@ -25,8 +25,7 @@ description: |- Currently supported datasources: AppDynamics, Splunk, Graphite Enterprise Datasource queries use the freeform query field. It expects a JSON string list of valid grafana query JSON objects. -You can see more examples of correct JSON structure from using the export button on a Grafana-managed rule definition -created by the SLO app, under the model field. +For additional help, view our [documentation](https://grafana.com/docs/grafana-cloud/alerting-and-irm/slo/). {{ tffile "examples/resources/grafana_slo/resource_nonprometheus.tf" }} From 7341ebe2024ca22f8512a89bfd7150a49b0dd123 Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Tue, 11 Feb 2025 12:55:09 -0800 Subject: [PATCH 16/22] chore(doc links): fixing terraform provider doc links --- docs/resources/slo.md | 12 ++++++------ .../resources/grafana_slo/resource_nonprometheus.tf | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/resources/slo.md b/docs/resources/slo.md index 3ca8e5ec8..1e5735b43 100644 --- a/docs/resources/slo.md +++ b/docs/resources/slo.md @@ -145,8 +145,8 @@ resource "grafana_slo" "test" { application : "57831", applicationName : "petclinic", datasource : { - "type" : "dlopes7-appdynamics-datasource", - "uid" : "appdynamics_localdev" + type : "dlopes7-appdynamics-datasource", + uid : "appdynamics_localdev" }, delimiter : "|", isRawQuery : false, @@ -164,8 +164,8 @@ resource "grafana_slo" "test" { application : "57831", applicationName : "petclinic", datasource : { - "type" : "dlopes7-appdynamics-datasource", - "uid" : "appdynamics_localdev" + type : "dlopes7-appdynamics-datasource", + uid : "appdynamics_localdev" }, intervalMs : 1000, maxDataPoints : 43200, @@ -181,8 +181,8 @@ resource "grafana_slo" "test" { }, { datasource : { - "type" : "__expr__", - "uid" : "__expr__" + type : "__expr__", + uid : "__expr__" }, expression : "($total - $errors) / $total", intervalMs : 1000, diff --git a/examples/resources/grafana_slo/resource_nonprometheus.tf b/examples/resources/grafana_slo/resource_nonprometheus.tf index 404bf7bab..a19d3478b 100644 --- a/examples/resources/grafana_slo/resource_nonprometheus.tf +++ b/examples/resources/grafana_slo/resource_nonprometheus.tf @@ -10,8 +10,8 @@ resource "grafana_slo" "test" { application : "57831", applicationName : "petclinic", datasource : { - "type" : "dlopes7-appdynamics-datasource", - "uid" : "appdynamics_localdev" + type : "dlopes7-appdynamics-datasource", + uid : "appdynamics_localdev" }, delimiter : "|", isRawQuery : false, @@ -29,8 +29,8 @@ resource "grafana_slo" "test" { application : "57831", applicationName : "petclinic", datasource : { - "type" : "dlopes7-appdynamics-datasource", - "uid" : "appdynamics_localdev" + type : "dlopes7-appdynamics-datasource", + uid : "appdynamics_localdev" }, intervalMs : 1000, maxDataPoints : 43200, @@ -46,8 +46,8 @@ resource "grafana_slo" "test" { }, { datasource : { - "type" : "__expr__", - "uid" : "__expr__" + type : "__expr__", + uid : "__expr__" }, expression : "($total - $errors) / $total", intervalMs : 1000, From e8877fae81e4fc1240c944f91dbc3c7bb10902aa Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Tue, 18 Feb 2025 10:57:02 -0800 Subject: [PATCH 17/22] chore(slo provider): update to new query type --- .../grafana_slo/resource_nonprometheus.tf | 4 +- go.mod | 2 +- go.sum | 2 + internal/resources/slo/resource_slo.go | 49 ++++++++++++++++--- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/examples/resources/grafana_slo/resource_nonprometheus.tf b/examples/resources/grafana_slo/resource_nonprometheus.tf index a19d3478b..ba46ed9e7 100644 --- a/examples/resources/grafana_slo/resource_nonprometheus.tf +++ b/examples/resources/grafana_slo/resource_nonprometheus.tf @@ -2,8 +2,8 @@ resource "grafana_slo" "test" { name = "Terraform Testing" description = "Terraform Description" query { - freeform { - query = jsonencode([ + grafanaQueries { + grafanaQueries = jsonencode([ { aggregation : "Sum", alias : "", diff --git a/go.mod b/go.mod index d6c5b444c..8b5afba8a 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/grafana/grafana-com-public-clients/go/gcom v0.0.0-20240807172819-ac10800522a3 github.com/grafana/grafana-openapi-client-go v0.0.0-20241113095943-9cb2bbfeb8a3 github.com/grafana/machine-learning-go-client v0.8.2 - github.com/grafana/slo-openapi-client/go/slo v0.0.0-20240807172758-1b7d00838fc7 + github.com/grafana/slo-openapi-client/go/slo v0.0.0-20250218172929-ab9cae090da6 github.com/grafana/synthetic-monitoring-agent v0.30.2 github.com/grafana/synthetic-monitoring-api-go-client v0.9.2 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 diff --git a/go.sum b/go.sum index 9c3a04538..03f3c31b1 100644 --- a/go.sum +++ b/go.sum @@ -153,6 +153,8 @@ github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKt github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/grafana/slo-openapi-client/go/slo v0.0.0-20240807172758-1b7d00838fc7 h1:t7zAFX0rMu868n85zRHLgmAjLJgWbkxUekGquZmovjA= github.com/grafana/slo-openapi-client/go/slo v0.0.0-20240807172758-1b7d00838fc7/go.mod h1:MVsmQi3lkhNnRExmke6Ug6HFG4Dycd+oRgzC3Rz+vOs= +github.com/grafana/slo-openapi-client/go/slo v0.0.0-20250218172929-ab9cae090da6 h1:7jbuz2MCqHlIc+vC3geOEzMZolm30v3K1bFTaTJ4mGI= +github.com/grafana/slo-openapi-client/go/slo v0.0.0-20250218172929-ab9cae090da6/go.mod h1:a9idYds6valDrBwBdcFTIp+QzlERgf2CFDZgEfmp9pA= github.com/grafana/synthetic-monitoring-agent v0.30.2 h1:wredFHXxXXO5Mg/ucEe3+Yud1OSL51LwMneC28nAGbg= github.com/grafana/synthetic-monitoring-agent v0.30.2/go.mod h1:JEH8a3hLOFRgkl+dlTj3gerkGHkfX+WIhdmIvP7RGSI= github.com/grafana/synthetic-monitoring-api-go-client v0.9.2 h1:Q2sIkvE8ekeGGroWqh3pEj9mJ/hisS41ezNrFCjrTj4= diff --git a/internal/resources/slo/resource_slo.go b/internal/resources/slo/resource_slo.go index dc5f30b0e..c06b31a70 100644 --- a/internal/resources/slo/resource_slo.go +++ b/internal/resources/slo/resource_slo.go @@ -16,10 +16,11 @@ import ( ) const ( - QueryTypeFreeform string = "freeform" - QueryTypeHistogram string = "histogram" - QueryTypeRatio string = "ratio" - QueryTypeThreshold string = "threshold" + QueryTypeFreeform string = "freeform" + QueryTypeHistogram string = "histogram" + QueryTypeRatio string = "ratio" + QueryTypeThreshold string = "threshold" + QueryTypeGrafanaQueries string = "grafanaQueries" ) var resourceSloID = common.NewResourceID(common.StringIDField("uuid")) @@ -81,8 +82,8 @@ Resource manages Grafana SLOs. Schema: map[string]*schema.Schema{ "type": { Type: schema.TypeString, - Description: `Query type must be one of: "freeform", "query", "ratio", or "threshold"`, - ValidateFunc: validation.StringInSlice([]string{"freeform", "query", "ratio", "threshold"}, false), + Description: `Query type must be one of: "freeform", "query", "ratio", "grafanaQueries" or "threshold"`, + ValidateFunc: validation.StringInSlice([]string{"freeform", "query", "ratio", "threshold", "grafanaQueries"}, false), Required: true, }, "freeform": { @@ -92,9 +93,23 @@ Resource manages Grafana SLOs. Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "query": { + Type: schema.TypeString, + Required: true, + Description: "Freeform Query Field - valid promQl", + }, + }, + }, + }, + "grafanaQueries": { + Type: schema.TypeList, + Optional: true, + Description: "Array for holding a set of grafana queries", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "grafanaQueries": { Type: schema.TypeString, Required: true, - Description: "Freeform Query Field - valid promQl or JSON formatted string", + Description: "Query Object - JSON formatted string", ValidateDiagFunc: ValidateBigTent(), }, }, @@ -516,6 +531,26 @@ func packQuery(query map[string]interface{}) (slo.SloV00Query, error) { return sloQuery, nil } + if query["type"] == "grafanaQueries" { + freeformquery := query["grafanaQueries"].([]interface{})[0].(map[string]interface{}) + querystring := freeformquery["grafanaQueries"].(string) + + var queryMapList []map[string]interface{} + err := json.Unmarshal([]byte(querystring), &queryMapList) + + // We validate the JSON structure this should never occur + if err != nil { + return slo.SloV00Query{}, err + } + + sloQuery := slo.SloV00Query{ + GrafanaQueries: &slo.SloV00GrafanaQueries{GrafanaQueries: queryMapList}, + Type: QueryTypeGrafanaQueries, + } + + return sloQuery, nil + } + return slo.SloV00Query{}, fmt.Errorf("%s query type not implemented", query["type"]) } From c1c19e9048af9deee35c1204be73618ea971afb4 Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Tue, 18 Feb 2025 11:18:41 -0800 Subject: [PATCH 18/22] chore(slo provider): fixing grafana_queries for tf format --- .../resources/grafana_slo/resource_nonprometheus.tf | 6 +++--- internal/resources/slo/resource_slo.go | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/resources/grafana_slo/resource_nonprometheus.tf b/examples/resources/grafana_slo/resource_nonprometheus.tf index ba46ed9e7..b1d77b622 100644 --- a/examples/resources/grafana_slo/resource_nonprometheus.tf +++ b/examples/resources/grafana_slo/resource_nonprometheus.tf @@ -2,8 +2,8 @@ resource "grafana_slo" "test" { name = "Terraform Testing" description = "Terraform Description" query { - grafanaQueries { - grafanaQueries = jsonencode([ + grafana_queries { + grafana_queries = jsonencode([ { aggregation : "Sum", alias : "", @@ -57,7 +57,7 @@ resource "grafana_slo" "test" { } ]) } - type = "freeform" + type = "grafana_queries" } objectives { value = 0.995 diff --git a/internal/resources/slo/resource_slo.go b/internal/resources/slo/resource_slo.go index c06b31a70..a282d8b8a 100644 --- a/internal/resources/slo/resource_slo.go +++ b/internal/resources/slo/resource_slo.go @@ -20,7 +20,7 @@ const ( QueryTypeHistogram string = "histogram" QueryTypeRatio string = "ratio" QueryTypeThreshold string = "threshold" - QueryTypeGrafanaQueries string = "grafanaQueries" + QueryTypeGrafanaQueries string = "grafana_queries" ) var resourceSloID = common.NewResourceID(common.StringIDField("uuid")) @@ -100,13 +100,13 @@ Resource manages Grafana SLOs. }, }, }, - "grafanaQueries": { + "grafana_queries": { Type: schema.TypeList, Optional: true, Description: "Array for holding a set of grafana queries", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "grafanaQueries": { + "grafana_queries": { Type: schema.TypeString, Required: true, Description: "Query Object - JSON formatted string", @@ -531,9 +531,9 @@ func packQuery(query map[string]interface{}) (slo.SloV00Query, error) { return sloQuery, nil } - if query["type"] == "grafanaQueries" { - freeformquery := query["grafanaQueries"].([]interface{})[0].(map[string]interface{}) - querystring := freeformquery["grafanaQueries"].(string) + if query["type"] == "grafana_queries" { + freeformquery := query["grafana_queries"].([]interface{})[0].(map[string]interface{}) + querystring := freeformquery["grafana_queries"].(string) var queryMapList []map[string]interface{} err := json.Unmarshal([]byte(querystring), &queryMapList) From 3c90dc71540a62c6d4c8ac390adf192309ac7a6d Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Tue, 18 Feb 2025 11:23:09 -0800 Subject: [PATCH 19/22] chore(slo provider): docs --- docs/data-sources/slos.md | 9 +++++++++ docs/resources/slo.md | 19 ++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/docs/data-sources/slos.md b/docs/data-sources/slos.md index c37a8d5a2..a96ac6296 100644 --- a/docs/data-sources/slos.md +++ b/docs/data-sources/slos.md @@ -214,6 +214,7 @@ Read-Only: Read-Only: - `freeform` (List of Object) (see [below for nested schema](#nestedobjatt--slos--query--freeform)) +- `grafana_queries` (List of Object) (see [below for nested schema](#nestedobjatt--slos--query--grafana_queries)) - `ratio` (List of Object) (see [below for nested schema](#nestedobjatt--slos--query--ratio)) - `type` (String) @@ -225,6 +226,14 @@ Read-Only: - `query` (String) + +### Nested Schema for `slos.query.grafana_queries` + +Read-Only: + +- `grafana_queries` (String) + + ### Nested Schema for `slos.query.ratio` diff --git a/docs/resources/slo.md b/docs/resources/slo.md index 1e5735b43..39f93ccb1 100644 --- a/docs/resources/slo.md +++ b/docs/resources/slo.md @@ -137,8 +137,8 @@ resource "grafana_slo" "test" { name = "Terraform Testing" description = "Terraform Description" query { - freeform { - query = jsonencode([ + grafana_queries { + grafana_queries = jsonencode([ { aggregation : "Sum", alias : "", @@ -192,7 +192,7 @@ resource "grafana_slo" "test" { } ]) } - type = "freeform" + type = "grafana_queries" } objectives { value = 0.995 @@ -279,11 +279,12 @@ Required: Required: -- `type` (String) Query type must be one of: "freeform", "query", "ratio", or "threshold" +- `type` (String) Query type must be one of: "freeform", "query", "ratio", "grafanaQueries" or "threshold" Optional: - `freeform` (Block List, Max: 1) (see [below for nested schema](#nestedblock--query--freeform)) +- `grafana_queries` (Block List) Array for holding a set of grafana queries (see [below for nested schema](#nestedblock--query--grafana_queries)) - `ratio` (Block List, Max: 1) (see [below for nested schema](#nestedblock--query--ratio)) @@ -291,7 +292,15 @@ Optional: Required: -- `query` (String) Freeform Query Field - valid promQl or JSON formatted string +- `query` (String) Freeform Query Field - valid promQl + + + +### Nested Schema for `query.grafana_queries` + +Required: + +- `grafana_queries` (String) Query Object - JSON formatted string From b0ee9a46a78f89e4be3dbfe19f2c5db87d21ef29 Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Tue, 18 Feb 2025 14:31:23 -0800 Subject: [PATCH 20/22] chore(slo provider): update failure for prometheus queries in grafanaQueries --- internal/resources/slo/resource_slo.go | 6 ++++++ internal/resources/slo/resource_slo_test.go | 9 +++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/internal/resources/slo/resource_slo.go b/internal/resources/slo/resource_slo.go index a282d8b8a..f51988f2f 100644 --- a/internal/resources/slo/resource_slo.go +++ b/internal/resources/slo/resource_slo.go @@ -720,6 +720,12 @@ func ValidateBigTent() schema.SchemaValidateDiagFunc { var gmrQuery []map[string]any err := json.Unmarshal([]byte(v), &gmrQuery) if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Missing Required Field", + Detail: fmt.Sprintf("expected grafana queries to be valid JSON format"), + AttributePath: path, + }) return diags } diff --git a/internal/resources/slo/resource_slo_test.go b/internal/resources/slo/resource_slo_test.go index 0df84dcec..5832a8af0 100644 --- a/internal/resources/slo/resource_slo_test.go +++ b/internal/resources/slo/resource_slo_test.go @@ -426,8 +426,13 @@ func TestValidateBigTent(t *testing.T) { expectedDiags diag.Diagnostics }{ "prometheus": { - query: "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))", - expectedDiags: diag.Diagnostics{}, + query: "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))", + expectedDiags: diag.Diagnostics{diag.Diagnostic{ + Severity: diag.Error, + Summary: "Missing Required Field", + Detail: fmt.Sprintf("expected grafana queries to be valid JSON format"), + AttributePath: cty.IndexPath(cty.Value{}), + }}, }, "bigTent_success": { query: createBigTent(true, []map[string]any{}), From 6a566c4899d7e987727f64e5098ba2285b1f2425 Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Tue, 18 Feb 2025 14:40:24 -0800 Subject: [PATCH 21/22] chore(slo provider): fmt --- internal/resources/slo/resource_slo.go | 2 +- internal/resources/slo/resource_slo_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/resources/slo/resource_slo.go b/internal/resources/slo/resource_slo.go index f51988f2f..e00235b55 100644 --- a/internal/resources/slo/resource_slo.go +++ b/internal/resources/slo/resource_slo.go @@ -723,7 +723,7 @@ func ValidateBigTent() schema.SchemaValidateDiagFunc { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, Summary: "Missing Required Field", - Detail: fmt.Sprintf("expected grafana queries to be valid JSON format"), + Detail: "expected grafana queries to be valid JSON format", AttributePath: path, }) return diags diff --git a/internal/resources/slo/resource_slo_test.go b/internal/resources/slo/resource_slo_test.go index 5832a8af0..2fc5205d1 100644 --- a/internal/resources/slo/resource_slo_test.go +++ b/internal/resources/slo/resource_slo_test.go @@ -430,7 +430,7 @@ func TestValidateBigTent(t *testing.T) { expectedDiags: diag.Diagnostics{diag.Diagnostic{ Severity: diag.Error, Summary: "Missing Required Field", - Detail: fmt.Sprintf("expected grafana queries to be valid JSON format"), + Detail: "expected grafana queries to be valid JSON format", AttributePath: cty.IndexPath(cty.Value{}), }}, }, From 100c49a58eeb48d22b52d044019de1c77bba5ef5 Mon Sep 17 00:00:00 2001 From: Leo DiCara Date: Thu, 20 Feb 2025 10:21:45 -0800 Subject: [PATCH 22/22] chore(slo provider): remove from ratio queries --- internal/resources/slo/resource_slo.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/internal/resources/slo/resource_slo.go b/internal/resources/slo/resource_slo.go index e00235b55..cce7f76ab 100644 --- a/internal/resources/slo/resource_slo.go +++ b/internal/resources/slo/resource_slo.go @@ -122,16 +122,14 @@ Resource manages Grafana SLOs. Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "success_metric": { - Type: schema.TypeString, - Description: `Counter metric for success events (numerator)`, - Required: true, - ValidateDiagFunc: ValidateBigTent(), + Type: schema.TypeString, + Description: `Counter metric for success events (numerator)`, + Required: true, }, "total_metric": { - Type: schema.TypeString, - Description: `Metric for total events (denominator)`, - Required: true, - ValidateDiagFunc: ValidateBigTent(), + Type: schema.TypeString, + Description: `Metric for total events (denominator)`, + Required: true, }, "group_by_labels": { Type: schema.TypeList,