diff --git a/GNUmakefile b/GNUmakefile
index 9b750d402..26e04fbd4 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -60,3 +60,4 @@ golangci-lint:
linkcheck:
docker run -it --entrypoint sh -v "$$PWD:$$PWD" -w "$$PWD" python:3.11-alpine -c "pip3 install linkchecker && linkchecker --config .linkcheckerrc docs"
+
diff --git a/docs/data-sources/slos.md b/docs/data-sources/slos.md
new file mode 100644
index 000000000..157cf1c18
--- /dev/null
+++ b/docs/data-sources/slos.md
@@ -0,0 +1,203 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_slos Data Source - terraform-provider-grafana"
+subcategory: "SLO"
+description: |-
+ Datasource for retrieving all SLOs.
+ Official documentation https://grafana.com/docs/grafana-cloud/slo/API documentation https://grafana.com/docs/grafana-cloud/slo/api/
+---
+
+# grafana_slos (Data Source)
+
+Datasource for retrieving all SLOs.
+
+* [Official documentation](https://grafana.com/docs/grafana-cloud/slo/)
+* [API documentation](https://grafana.com/docs/grafana-cloud/slo/api/)
+
+## Example Usage
+
+```terraform
+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"
+ }
+ label {
+ key = "custom"
+ value = "value"
+ }
+ alerting {
+ fastburn {
+ annotation {
+ key = "name"
+ value = "Critical - SLO Burn Rate Alert"
+ }
+ label {
+ key = "type"
+ value = "slo"
+ }
+ }
+
+ slowburn {
+ annotation {
+ key = "name"
+ value = "Warning - SLO Burn Rate Alert"
+ }
+ label {
+ key = "type"
+ value = "slo"
+ }
+ }
+ }
+}
+
+data "grafana_slos" "slos" {}
+```
+
+
+## Schema
+
+### Read-Only
+
+- `id` (String) The ID of this resource.
+- `slos` (List of Object) Returns a list of all SLOs" (see [below for nested schema](#nestedatt--slos))
+
+
+### Nested Schema for `slos`
+
+Read-Only:
+
+- `alerting` (List of Object) (see [below for nested schema](#nestedobjatt--slos--alerting))
+- `dashboard_uid` (String)
+- `description` (String)
+- `label` (List of Object) (see [below for nested schema](#nestedobjatt--slos--label))
+- `name` (String)
+- `objectives` (List of Object) (see [below for nested schema](#nestedobjatt--slos--objectives))
+- `query` (List of Object) (see [below for nested schema](#nestedobjatt--slos--query))
+- `uuid` (String)
+
+
+### Nested Schema for `slos.alerting`
+
+Read-Only:
+
+- `annotation` (List of Object) (see [below for nested schema](#nestedobjatt--slos--alerting--annotation))
+- `fastburn` (List of Object) (see [below for nested schema](#nestedobjatt--slos--alerting--fastburn))
+- `label` (List of Object) (see [below for nested schema](#nestedobjatt--slos--alerting--label))
+- `slowburn` (List of Object) (see [below for nested schema](#nestedobjatt--slos--alerting--slowburn))
+
+
+### Nested Schema for `slos.alerting.annotation`
+
+Read-Only:
+
+- `key` (String)
+- `value` (String)
+
+
+
+### Nested Schema for `slos.alerting.fastburn`
+
+Read-Only:
+
+- `annotation` (List of Object) (see [below for nested schema](#nestedobjatt--slos--alerting--fastburn--annotation))
+- `label` (List of Object) (see [below for nested schema](#nestedobjatt--slos--alerting--fastburn--label))
+
+
+### Nested Schema for `slos.alerting.fastburn.label`
+
+Read-Only:
+
+- `key` (String)
+- `value` (String)
+
+
+
+### Nested Schema for `slos.alerting.fastburn.label`
+
+Read-Only:
+
+- `key` (String)
+- `value` (String)
+
+
+
+
+### Nested Schema for `slos.alerting.label`
+
+Read-Only:
+
+- `key` (String)
+- `value` (String)
+
+
+
+### Nested Schema for `slos.alerting.slowburn`
+
+Read-Only:
+
+- `annotation` (List of Object) (see [below for nested schema](#nestedobjatt--slos--alerting--slowburn--annotation))
+- `label` (List of Object) (see [below for nested schema](#nestedobjatt--slos--alerting--slowburn--label))
+
+
+### Nested Schema for `slos.alerting.slowburn.label`
+
+Read-Only:
+
+- `key` (String)
+- `value` (String)
+
+
+
+### Nested Schema for `slos.alerting.slowburn.label`
+
+Read-Only:
+
+- `key` (String)
+- `value` (String)
+
+
+
+
+
+### Nested Schema for `slos.label`
+
+Read-Only:
+
+- `key` (String)
+- `value` (String)
+
+
+
+### Nested Schema for `slos.objectives`
+
+Read-Only:
+
+- `value` (Number)
+- `window` (String)
+
+
+
+### Nested Schema for `slos.query`
+
+Read-Only:
+
+- `freeform` (List of Object) (see [below for nested schema](#nestedobjatt--slos--query--freeform))
+- `type` (String)
+
+
+### Nested Schema for `slos.query.freeform`
+
+Read-Only:
+
+- `query` (String)
+
+
diff --git a/docs/resources/slo.md b/docs/resources/slo.md
new file mode 100644
index 000000000..590bbb8cd
--- /dev/null
+++ b/docs/resources/slo.md
@@ -0,0 +1,204 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_slo Resource - terraform-provider-grafana"
+subcategory: "SLO"
+description: |-
+ Resource manages Grafana SLOs.
+ Official documentation https://grafana.com/docs/grafana-cloud/slo/API documentation https://grafana.com/docs/grafana-cloud/slo/api/
+---
+
+# grafana_slo (Resource)
+
+Resource manages Grafana SLOs.
+
+* [Official documentation](https://grafana.com/docs/grafana-cloud/slo/)
+* [API documentation](https://grafana.com/docs/grafana-cloud/slo/api/)
+
+## Example Usage
+
+```terraform
+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"
+ }
+ label {
+ key = "custom"
+ value = "value"
+ }
+ alerting {
+ fastburn {
+ annotation {
+ key = "name"
+ value = "Critical - SLO Burn Rate Alert"
+ }
+ label {
+ key = "type"
+ value = "slo"
+ }
+ }
+
+ slowburn {
+ annotation {
+ key = "name"
+ value = "Warning - SLO Burn Rate Alert"
+ }
+ label {
+ key = "type"
+ value = "slo"
+ }
+ }
+ }
+}
+```
+
+
+## Schema
+
+### Required
+
+- `description` (String) Description is a free-text field that can provide more context to an SLO.
+- `name` (String) Name should be a short description of your indicator. Consider names like "API Availability"
+- `objectives` (Block List, Min: 1) Over each rolling time window, the remaining error budget will be calculated, and separate alerts can be generated for each time window based on the SLO burn rate or remaining error budget. (see [below for nested schema](#nestedblock--objectives))
+- `query` (Block List, Min: 1) Query describes the indicator that will be measured against the objective. Freeform Query types are currently supported. (see [below for nested schema](#nestedblock--query))
+
+### Optional
+
+- `alerting` (Block List) Configures the alerting rules that will be generated for each
+ time window associated with the SLO. Grafana SLOs can generate
+ alerts when the short-term error budget burn is very high, the
+ long-term error budget burn rate is high, or when the remaining
+ error budget is below a certain threshold. (see [below for nested schema](#nestedblock--alerting))
+- `label` (Block List) Additional labels that will be attached to all metrics generated from the query. These labels are useful for grouping SLOs in dashboard views that you create by hand. (see [below for nested schema](#nestedblock--label))
+
+### Read-Only
+
+- `dashboard_uid` (String) A reference to a dashboard that the plugin has installed in Grafana based on this SLO. This field is read-only, it is generated by the Grafana SLO Plugin.
+- `id` (String) The ID of this resource.
+
+
+### Nested Schema for `objectives`
+
+Required:
+
+- `value` (Number) Value between 0 and 1. If the value of the query is above the objective, the SLO is met.
+- `window` (String) A Prometheus-parsable time duration string like 24h, 60m. This is the time window the objective is measured over.
+
+
+
+### Nested Schema for `query`
+
+Required:
+
+- `freeform` (Block List, Min: 1, Max: 1) (see [below for nested schema](#nestedblock--query--freeform))
+- `type` (String) Query type must be one of: "freeform", "query", "ratio", or "threshold"
+
+
+### Nested Schema for `query.freeform`
+
+Optional:
+
+- `query` (String) Freeform Query Field
+
+
+
+
+### Nested Schema for `alerting`
+
+Optional:
+
+- `annotation` (Block List) Annotations will be attached to all alerts generated by any of these rules. (see [below for nested schema](#nestedblock--alerting--annotation))
+- `fastburn` (Block List) Alerting Rules generated for Fast Burn alerts (see [below for nested schema](#nestedblock--alerting--fastburn))
+- `label` (Block List) Labels will be attached to all alerts generated by any of these rules. (see [below for nested schema](#nestedblock--alerting--label))
+- `slowburn` (Block List) Alerting Rules generated for Slow Burn alerts (see [below for nested schema](#nestedblock--alerting--slowburn))
+
+
+### Nested Schema for `alerting.annotation`
+
+Required:
+
+- `key` (String)
+- `value` (String)
+
+
+
+### Nested Schema for `alerting.fastburn`
+
+Optional:
+
+- `annotation` (Block List) Annotations to attach only to Fast Burn alerts. (see [below for nested schema](#nestedblock--alerting--fastburn--annotation))
+- `label` (Block List) Labels to attach only to Fast Burn alerts. (see [below for nested schema](#nestedblock--alerting--fastburn--label))
+
+
+### Nested Schema for `alerting.fastburn.annotation`
+
+Required:
+
+- `key` (String)
+- `value` (String)
+
+
+
+### Nested Schema for `alerting.fastburn.label`
+
+Required:
+
+- `key` (String)
+- `value` (String)
+
+
+
+
+### Nested Schema for `alerting.label`
+
+Required:
+
+- `key` (String)
+- `value` (String)
+
+
+
+### Nested Schema for `alerting.slowburn`
+
+Optional:
+
+- `annotation` (Block List) Annotations to attach only to Slow Burn alerts. (see [below for nested schema](#nestedblock--alerting--slowburn--annotation))
+- `label` (Block List) Labels to attach only to Slow Burn alerts. (see [below for nested schema](#nestedblock--alerting--slowburn--label))
+
+
+### Nested Schema for `alerting.slowburn.annotation`
+
+Required:
+
+- `key` (String)
+- `value` (String)
+
+
+
+### Nested Schema for `alerting.slowburn.label`
+
+Required:
+
+- `key` (String)
+- `value` (String)
+
+
+
+
+
+### Nested Schema for `label`
+
+Required:
+
+- `key` (String)
+- `value` (String)
+
+
diff --git a/examples/data-sources/grafana_slos/data-source.tf b/examples/data-sources/grafana_slos/data-source.tf
new file mode 100644
index 000000000..bde83bbfc
--- /dev/null
+++ b/examples/data-sources/grafana_slos/data-source.tf
@@ -0,0 +1,43 @@
+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"
+ }
+ label {
+ key = "custom"
+ value = "value"
+ }
+ alerting {
+ fastburn {
+ annotation {
+ key = "name"
+ value = "Critical - SLO Burn Rate Alert"
+ }
+ label {
+ key = "type"
+ value = "slo"
+ }
+ }
+
+ slowburn {
+ annotation {
+ key = "name"
+ value = "Warning - SLO Burn Rate Alert"
+ }
+ label {
+ key = "type"
+ value = "slo"
+ }
+ }
+ }
+}
+
+data "grafana_slos" "slos" {}
\ No newline at end of file
diff --git a/examples/resources/grafana_slo/resource.tf b/examples/resources/grafana_slo/resource.tf
new file mode 100644
index 000000000..d57c5d99d
--- /dev/null
+++ b/examples/resources/grafana_slo/resource.tf
@@ -0,0 +1,41 @@
+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"
+ }
+ label {
+ key = "custom"
+ value = "value"
+ }
+ alerting {
+ fastburn {
+ annotation {
+ key = "name"
+ value = "Critical - SLO Burn Rate Alert"
+ }
+ label {
+ key = "type"
+ value = "slo"
+ }
+ }
+
+ slowburn {
+ annotation {
+ key = "name"
+ value = "Warning - SLO Burn Rate Alert"
+ }
+ label {
+ key = "type"
+ value = "slo"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/resources/grafana_slo/resource_complex.tf b/examples/resources/grafana_slo/resource_complex.tf
new file mode 100644
index 000000000..8804b1867
--- /dev/null
+++ b/examples/resources/grafana_slo/resource_complex.tf
@@ -0,0 +1,54 @@
+resource "grafana_slo" "test" {
+ name = "Complex Resource - Terraform Testing"
+ description = "Complex Resource - 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"
+ }
+ label {
+ key = "slokey"
+ value = "slokey"
+ }
+ alerting {
+ label {
+ key = "alertingkey"
+ value = "alertingvalue"
+ }
+
+ fastburn {
+ annotation {
+ key = "name"
+ value = "Critical - SLO Burn Rate Alert"
+ }
+ annotation {
+ key = "description"
+ value = "Error Budget is burning at a rate greater than 14.4x."
+ }
+ label {
+ key = "type"
+ value = "slo"
+ }
+ }
+
+ slowburn {
+ annotation {
+ key = "name"
+ value = "Warning - SLO Burn Rate Alert"
+ }
+ annotation {
+ key = "description"
+ value = "Error Budget is burning at a rate greater than 1x."
+ }
+ label {
+ key = "type"
+ value = "slo"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/resources/grafana_slo/resource_update.tf b/examples/resources/grafana_slo/resource_update.tf
new file mode 100644
index 000000000..aedc50227
--- /dev/null
+++ b/examples/resources/grafana_slo/resource_update.tf
@@ -0,0 +1,41 @@
+resource "grafana_slo" "update" {
+ name = "Updated - Terraform Testing"
+ description = "Updated - 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.9995
+ window = "7d"
+ }
+ label {
+ key = "customkey"
+ value = "customvalue"
+ }
+ alerting {
+ fastburn {
+ annotation {
+ key = "name"
+ value = "Critical - SLO Burn Rate Alert"
+ }
+ label {
+ key = "type"
+ value = "slo"
+ }
+ }
+
+ slowburn {
+ annotation {
+ key = "name"
+ value = "Warning - SLO Burn Rate Alert"
+ }
+ label {
+ key = "type"
+ value = "slo"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 9c7649b8f..66874fea7 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.20
require (
github.com/Masterminds/semver/v3 v3.2.1
github.com/grafana/amixr-api-go-client v0.0.7
- github.com/grafana/grafana-api-golang-client v0.20.1
+ github.com/grafana/grafana-api-golang-client v0.21.0
github.com/grafana/machine-learning-go-client v0.5.0
github.com/grafana/synthetic-monitoring-agent v0.14.5
github.com/grafana/synthetic-monitoring-api-go-client v0.7.0
diff --git a/go.sum b/go.sum
index 9312c49e1..731dc0b59 100644
--- a/go.sum
+++ b/go.sum
@@ -72,8 +72,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/amixr-api-go-client v0.0.7 h1:U6W6yKxMMybI+Qz4zl+Vih48o6CczLaU/vjk2m7omvU=
github.com/grafana/amixr-api-go-client v0.0.7/go.mod h1:N6x26XUrM5zGtK5zL5vNJnAn2JFMxLFPPLTw/6pDkFE=
-github.com/grafana/grafana-api-golang-client v0.20.1 h1:xadfMY9PDcWd2ppU/AgYvMeBeWqioed7cetPhBYoNSk=
-github.com/grafana/grafana-api-golang-client v0.20.1/go.mod h1:24W29gPe9yl0/3A9X624TPkAOR8DpHno490cPwnkv8E=
+github.com/grafana/grafana-api-golang-client v0.21.0 h1:PQ2Wfo9jMMiftC4VRMlJxbUNvYCXMV1YFDKm7Ny3SaM=
+github.com/grafana/grafana-api-golang-client v0.21.0/go.mod h1:24W29gPe9yl0/3A9X624TPkAOR8DpHno490cPwnkv8E=
github.com/grafana/machine-learning-go-client v0.5.0 h1:Q1K+MPSy8vfMm2jsk3WQ7O77cGr2fM5hxwtPSoPc5NU=
github.com/grafana/machine-learning-go-client v0.5.0/go.mod h1:QFfZz8NkqVF8++skjkKQXJEZfpCYd8S0yTWJUpsLLTA=
github.com/grafana/synthetic-monitoring-agent v0.14.5 h1:zYuzieZeDNczPZAMIqCNh8QKJ28V571iCKiOheic/g8=
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index fbca4d052..0ccf8d7c3 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -25,6 +25,7 @@ import (
"github.com/grafana/terraform-provider-grafana/internal/resources/grafana"
"github.com/grafana/terraform-provider-grafana/internal/resources/machinelearning"
"github.com/grafana/terraform-provider-grafana/internal/resources/oncall"
+ "github.com/grafana/terraform-provider-grafana/internal/resources/slo"
"github.com/grafana/terraform-provider-grafana/internal/resources/syntheticmonitoring"
)
@@ -77,6 +78,9 @@ func Provider(version string) func() *schema.Provider {
"grafana_machine_learning_job": machinelearning.ResourceJob(),
"grafana_machine_learning_holiday": machinelearning.ResourceHoliday(),
"grafana_machine_learning_outlier_detector": machinelearning.ResourceOutlierDetector(),
+
+ // SLO
+ "grafana_slo": slo.ResourceSlo(),
})
// Resources that require the Synthetic Monitoring client to exist.
@@ -121,6 +125,9 @@ func Provider(version string) func() *schema.Provider {
"grafana_team": grafana.DatasourceTeam(),
"grafana_organization": grafana.DatasourceOrganization(),
"grafana_organization_preferences": grafana.DatasourceOrganizationPreferences(),
+
+ // SLO
+ "grafana_slos": slo.DatasourceSlo(),
})
// Datasources that require the Synthetic Monitoring client to exist.
@@ -439,7 +446,10 @@ func createOnCallClient(d *schema.ResourceData) (*onCallAPI.Client, error) {
return onCallAPI.New(baseURL, aToken)
}
+// Sets a custom HTTP Header on all requests coming from the Grafana Terraform Provider to Grafana-Terraform-Provider: true
+// in addition to any headers set within the `http_headers` field or the `GRAFANA_HTTP_HEADERS` environment variable
func getHTTPHeadersMap(d *schema.ResourceData) (map[string]string, error) {
+ headers := map[string]string{"Grafana-Terraform-Provider": "true"}
headersMap := d.Get("http_headers").(map[string]interface{})
if len(headersMap) == 0 {
// We cannot use a DefaultFunc because they do not work on maps
@@ -449,16 +459,16 @@ func getHTTPHeadersMap(d *schema.ResourceData) (map[string]string, error) {
return nil, fmt.Errorf("invalid http_headers config: %w", err)
}
}
+
if len(headersMap) > 0 {
- headers := make(map[string]string)
for k, v := range headersMap {
if v, ok := v.(string); ok {
headers[k] = v
}
}
- return headers, nil
}
- return map[string]string{}, nil
+
+ return headers, nil
}
// getJSONMap is a helper function that parses the given environment variable as a JSON object
diff --git a/internal/resources/slo/data_source_slo.go b/internal/resources/slo/data_source_slo.go
new file mode 100644
index 000000000..afd13153f
--- /dev/null
+++ b/internal/resources/slo/data_source_slo.go
@@ -0,0 +1,176 @@
+package slo
+
+import (
+ "context"
+
+ gapi "github.com/grafana/grafana-api-golang-client"
+ "github.com/grafana/terraform-provider-grafana/internal/common"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+)
+
+func DatasourceSlo() *schema.Resource {
+ return &schema.Resource{
+ Description: `
+Datasource for retrieving all SLOs.
+
+* [Official documentation](https://grafana.com/docs/grafana-cloud/slo/)
+* [API documentation](https://grafana.com/docs/grafana-cloud/slo/api/)
+ `,
+ ReadContext: datasourceSloRead,
+ Schema: map[string]*schema.Schema{
+ "slos": &schema.Schema{
+ Type: schema.TypeList,
+ Computed: true,
+ Description: `Returns a list of all SLOs"`,
+ Elem: &schema.Resource{
+ Schema: common.CloneResourceSchemaForDatasource(ResourceSlo(), map[string]*schema.Schema{
+ "uuid": &schema.Schema{
+ Type: schema.TypeString,
+ Description: `A unique, random identifier. This value will also be the name of the resource stored in the API server. This value is read-only.`,
+ Computed: true,
+ },
+ }),
+ },
+ },
+ },
+ }
+}
+
+// Function sends a GET request to the SLO API Endpoint which returns a list of all SLOs
+// Maps the API Response body to the Terraform Schema and displays as a READ in the terminal
+func datasourceSloRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+ var diags diag.Diagnostics
+
+ client := m.(*common.Client).GrafanaAPI
+ apiSlos, _ := client.ListSlos()
+
+ terraformSlos := []interface{}{}
+
+ if len(apiSlos.Slos) == 0 {
+ d.SetId("slos")
+ d.Set("slos", terraformSlos)
+ return diags
+ }
+
+ for _, slo := range apiSlos.Slos {
+ terraformSlo := convertDatasourceSlo(slo)
+ terraformSlos = append(terraformSlos, terraformSlo)
+ }
+
+ d.SetId("slos")
+ d.Set("slos", terraformSlos)
+
+ return diags
+}
+
+func convertDatasourceSlo(slo gapi.Slo) map[string]interface{} {
+ ret := make(map[string]interface{})
+
+ ret["uuid"] = slo.UUID
+ ret["name"] = slo.Name
+ ret["description"] = slo.Description
+
+ if slo.DrillDownDashboardRef != nil {
+ ret["dashboard_uid"] = slo.DrillDownDashboardRef.UID
+ }
+
+ ret["query"] = unpackQuery(slo.Query)
+
+ retLabels := unpackLabels(&slo.Labels)
+ ret["label"] = retLabels
+
+ retObjectives := unpackObjectives(slo.Objectives)
+ ret["objectives"] = retObjectives
+
+ retAlerting := unpackAlerting(slo.Alerting)
+ ret["alerting"] = retAlerting
+
+ return ret
+}
+
+func unpackQuery(apiquery gapi.Query) []map[string]interface{} {
+ retQuery := []map[string]interface{}{}
+ if apiquery.Freeform.Query != "" {
+ query := map[string]interface{}{"type": "freeform"}
+
+ freeformquerystring := map[string]interface{}{"query": apiquery.Freeform.Query}
+ freeform := []map[string]interface{}{}
+ freeform = append(freeform, freeformquerystring)
+ query["freeform"] = freeform
+
+ retQuery = append(retQuery, query)
+ }
+
+ return retQuery
+}
+
+func unpackObjectives(objectives []gapi.Objective) []map[string]interface{} {
+ retObjectives := []map[string]interface{}{}
+
+ for _, objective := range objectives {
+ retObjective := make(map[string]interface{})
+ retObjective["value"] = objective.Value
+ retObjective["window"] = objective.Window
+ retObjectives = append(retObjectives, retObjective)
+ }
+
+ return retObjectives
+}
+
+func unpackLabels(labels *[]gapi.Label) []map[string]interface{} {
+ retLabels := []map[string]interface{}{}
+
+ if labels != nil {
+ for _, label := range *labels {
+ retLabel := make(map[string]interface{})
+ retLabel["key"] = label.Key
+ retLabel["value"] = label.Value
+ retLabels = append(retLabels, retLabel)
+ }
+ return retLabels
+ }
+
+ return nil
+}
+
+func unpackAlerting(alertData *gapi.Alerting) []map[string]interface{} {
+ retAlertData := []map[string]interface{}{}
+
+ if alertData == nil {
+ return retAlertData
+ }
+
+ alertObject := make(map[string]interface{})
+ alertObject["label"] = unpackLabels(&alertData.Labels)
+ alertObject["annotation"] = unpackLabels(&alertData.Annotations)
+
+ if alertData.FastBurn != nil {
+ alertObject["fastburn"] = unpackAlertingMetadata(*alertData.FastBurn)
+ }
+
+ if alertData.SlowBurn != nil {
+ alertObject["slowburn"] = unpackAlertingMetadata(*alertData.SlowBurn)
+ }
+
+ retAlertData = append(retAlertData, alertObject)
+ return retAlertData
+}
+
+func unpackAlertingMetadata(metaData gapi.AlertingMetadata) []map[string]interface{} {
+ retAlertMetaData := []map[string]interface{}{}
+ labelsAnnotsStruct := make(map[string]interface{})
+
+ if metaData.Annotations != nil {
+ retAnnotations := unpackLabels(&metaData.Annotations)
+ labelsAnnotsStruct["annotation"] = retAnnotations
+ }
+
+ if metaData.Labels != nil {
+ retLabels := unpackLabels(&metaData.Labels)
+ labelsAnnotsStruct["label"] = retLabels
+ }
+
+ retAlertMetaData = append(retAlertMetaData, labelsAnnotsStruct)
+ return retAlertMetaData
+}
diff --git a/internal/resources/slo/data_source_slo_test.go b/internal/resources/slo/data_source_slo_test.go
new file mode 100644
index 000000000..c723ac8e4
--- /dev/null
+++ b/internal/resources/slo/data_source_slo_test.go
@@ -0,0 +1,41 @@
+package slo_test
+
+import (
+ "testing"
+
+ gapi "github.com/grafana/grafana-api-golang-client"
+ "github.com/grafana/terraform-provider-grafana/internal/testutils"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+)
+
+func TestAccDataSourceSlo(t *testing.T) {
+ testutils.CheckCloudInstanceTestsEnabled(t)
+
+ var slo gapi.Slo
+ resource.ParallelTest(t, resource.TestCase{
+ ProviderFactories: testutils.ProviderFactories,
+ CheckDestroy: testAccSloCheckDestroy(&slo),
+ Steps: []resource.TestStep{
+ {
+ // Creates a SLO Resource
+ Config: testutils.TestAccExample(t, "resources/grafana_slo/resource.tf"),
+ Check: resource.ComposeTestCheckFunc(
+ testAccSloCheckExists("grafana_slo.test", &slo),
+ resource.TestCheckResourceAttrSet("grafana_slo.test", "id"),
+ resource.TestCheckResourceAttr("grafana_slo.test", "name", "Terraform Testing"),
+ resource.TestCheckResourceAttr("grafana_slo.test", "description", "Terraform Description"),
+ ),
+ },
+ {
+ // Verifies that the created SLO Resource is read by the Datasource Read Method
+ Config: testutils.TestAccExample(t, "data-sources/grafana_slos/data-source.tf"),
+ Check: resource.ComposeTestCheckFunc(
+
+ resource.TestCheckResourceAttrSet("data.grafana_slos.slos", "slos.0.uuid"),
+ resource.TestCheckResourceAttr("data.grafana_slos.slos", "slos.0.name", "Terraform Testing"),
+ resource.TestCheckResourceAttr("data.grafana_slos.slos", "slos.0.description", "Terraform Description"),
+ ),
+ },
+ },
+ })
+}
diff --git a/internal/resources/slo/resource_slo.go b/internal/resources/slo/resource_slo.go
new file mode 100644
index 000000000..3327b29b0
--- /dev/null
+++ b/internal/resources/slo/resource_slo.go
@@ -0,0 +1,430 @@
+package slo
+
+import (
+ "context"
+ "fmt"
+ "regexp"
+
+ gapi "github.com/grafana/grafana-api-golang-client"
+
+ "github.com/grafana/terraform-provider-grafana/internal/common"
+ "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"
+)
+
+func ResourceSlo() *schema.Resource {
+ return &schema.Resource{
+ Description: `
+Resource manages Grafana SLOs.
+
+* [Official documentation](https://grafana.com/docs/grafana-cloud/slo/)
+* [API documentation](https://grafana.com/docs/grafana-cloud/slo/api/)
+ `,
+ CreateContext: resourceSloCreate,
+ ReadContext: resourceSloRead,
+ UpdateContext: resourceSloUpdate,
+ DeleteContext: resourceSloDelete,
+ Importer: &schema.ResourceImporter{
+ StateContext: schema.ImportStatePassthroughContext,
+ },
+ Schema: map[string]*schema.Schema{
+ "name": &schema.Schema{
+ Type: schema.TypeString,
+ Required: true,
+ Description: `Name should be a short description of your indicator. Consider names like "API Availability"`,
+ ValidateFunc: validation.StringLenBetween(0, 128),
+ },
+ "description": &schema.Schema{
+ Type: schema.TypeString,
+ Required: true,
+ Description: `Description is a free-text field that can provide more context to an SLO.`,
+ ValidateFunc: validation.StringLenBetween(0, 1024),
+ },
+ "query": &schema.Schema{
+ Type: schema.TypeList,
+ Required: true,
+ Description: `Query describes the indicator that will be measured against the objective. Freeform Query types are currently supported.`,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "type": &schema.Schema{
+ Type: schema.TypeString,
+ Description: `Query type must be one of: "freeform", "query", "ratio", or "threshold"`,
+ ValidateFunc: validation.StringInSlice([]string{"freeform", "query", "ratio", "threshold"}, false),
+ Required: true,
+ },
+ "freeform": &schema.Schema{
+ Type: schema.TypeList,
+ MaxItems: 1,
+ Required: true,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "query": &schema.Schema{
+ Type: schema.TypeString,
+ Optional: true,
+ Description: "Freeform Query Field",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "label": &schema.Schema{
+ Type: schema.TypeList,
+ Optional: true,
+ Description: `Additional labels that will be attached to all metrics generated from the query. These labels are useful for grouping SLOs in dashboard views that you create by hand.`,
+ Elem: keyvalueSchema,
+ },
+ "objectives": &schema.Schema{
+ Type: schema.TypeList,
+ Required: true,
+ Description: `Over each rolling time window, the remaining error budget will be calculated, and separate alerts can be generated for each time window based on the SLO burn rate or remaining error budget.`,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "value": &schema.Schema{
+ Type: schema.TypeFloat,
+ Required: true,
+ ValidateFunc: validation.FloatBetween(0, 1),
+ Description: `Value between 0 and 1. If the value of the query is above the objective, the SLO is met.`,
+ },
+ "window": &schema.Schema{
+ Type: schema.TypeString,
+ Required: true,
+ Description: `A Prometheus-parsable time duration string like 24h, 60m. This is the time window the objective is measured over.`,
+ ValidateFunc: validation.StringMatch(regexp.MustCompile(`^\d+(ms|s|m|h|d|w|y)$`), "Objective window must be a Prometheus-parsable time duration"),
+ },
+ },
+ },
+ },
+ "dashboard_uid": &schema.Schema{
+ Type: schema.TypeString,
+ Computed: true,
+ Description: `A reference to a dashboard that the plugin has installed in Grafana based on this SLO. This field is read-only, it is generated by the Grafana SLO Plugin.`,
+ },
+ "alerting": &schema.Schema{
+ Type: schema.TypeList,
+ Optional: true,
+ Description: `Configures the alerting rules that will be generated for each
+ time window associated with the SLO. Grafana SLOs can generate
+ alerts when the short-term error budget burn is very high, the
+ long-term error budget burn rate is high, or when the remaining
+ error budget is below a certain threshold.`,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "label": &schema.Schema{
+ Type: schema.TypeList,
+ Optional: true,
+ Description: `Labels will be attached to all alerts generated by any of these rules.`,
+ Elem: keyvalueSchema,
+ },
+ "annotation": &schema.Schema{
+ Type: schema.TypeList,
+ Optional: true,
+ Description: `Annotations will be attached to all alerts generated by any of these rules.`,
+ Elem: keyvalueSchema,
+ },
+ "fastburn": &schema.Schema{
+ Type: schema.TypeList,
+ Optional: true,
+ Description: "Alerting Rules generated for Fast Burn alerts",
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "label": &schema.Schema{
+ Type: schema.TypeList,
+ Optional: true,
+ Description: "Labels to attach only to Fast Burn alerts.",
+ Elem: keyvalueSchema,
+ },
+ "annotation": &schema.Schema{
+ Type: schema.TypeList,
+ Optional: true,
+ Description: "Annotations to attach only to Fast Burn alerts.",
+ Elem: keyvalueSchema,
+ },
+ },
+ },
+ },
+ "slowburn": &schema.Schema{
+ Type: schema.TypeList,
+ Optional: true,
+ Description: "Alerting Rules generated for Slow Burn alerts",
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "label": &schema.Schema{
+ Type: schema.TypeList,
+ Optional: true,
+ Description: "Labels to attach only to Slow Burn alerts.",
+ Elem: keyvalueSchema,
+ },
+ "annotation": &schema.Schema{
+ Type: schema.TypeList,
+ Optional: true,
+ Description: "Annotations to attach only to Slow Burn alerts.",
+ Elem: keyvalueSchema,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+var keyvalueSchema = &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "key": &schema.Schema{
+ Type: schema.TypeString,
+ Required: true,
+ },
+ "value": &schema.Schema{
+ Type: schema.TypeString,
+ Required: true,
+ },
+ },
+}
+
+func resourceSloCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+ var diags diag.Diagnostics
+
+ slo, err := packSloResource(d)
+ if err != nil {
+ diags = append(diags, diag.Diagnostic{
+ Severity: diag.Error,
+ Summary: "Unable to Pack SLO",
+ Detail: err.Error(),
+ })
+ return diags
+ }
+
+ client := m.(*common.Client).GrafanaAPI
+ response, err := client.CreateSlo(slo)
+
+ if err != nil {
+ diags = append(diags, diag.Diagnostic{
+ Severity: diag.Error,
+ Summary: "Unable to create SLO - API",
+ Detail: fmt.Sprintf("API Error Message:%s", err.Error()),
+ })
+ return diags
+ }
+
+ d.SetId(response.UUID)
+ resourceSloRead(ctx, d, m)
+
+ return resourceSloRead(ctx, d, m)
+}
+
+// resourceSloRead - sends a GET Request to the single SLO Endpoint
+func resourceSloRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+ var diags diag.Diagnostics
+
+ sloID := d.Id()
+
+ client := m.(*common.Client).GrafanaAPI
+ slo, err := client.GetSlo(sloID)
+
+ if err != nil {
+ diags = append(diags, diag.Diagnostic{
+ Severity: diag.Error,
+ Summary: fmt.Sprintf("Unable to Fetch Slo with ID: %s", sloID),
+ Detail: fmt.Sprintf("API Error Message:%s", err.Error()),
+ })
+ return diags
+ }
+
+ setTerraformState(d, slo)
+
+ return diags
+}
+
+func resourceSloUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+ var diags diag.Diagnostics
+ sloID := d.Id()
+
+ if d.HasChange("name") || d.HasChange("description") || d.HasChange("query") || d.HasChange("label") || d.HasChange("objectives") || d.HasChange("alerting") {
+ slo, err := packSloResource(d)
+ if err != nil {
+ diags = append(diags, diag.Diagnostic{
+ Severity: diag.Error,
+ Summary: "Unable to Pack SLO",
+ Detail: err.Error(),
+ })
+ return diags
+ }
+
+ client := m.(*common.Client).GrafanaAPI
+
+ err = client.UpdateSlo(sloID, slo)
+ if err != nil {
+ diags = append(diags, diag.Diagnostic{
+ Severity: diag.Error,
+ Summary: fmt.Sprintf("Unable to Update Slo with ID: %s", sloID),
+ Detail: fmt.Sprintf("API Error Message:%s", err.Error()),
+ })
+ return diags
+ }
+ }
+
+ return resourceSloRead(ctx, d, m)
+}
+
+func resourceSloDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+ sloID := d.Id()
+
+ client := m.(*common.Client).GrafanaAPI
+
+ return diag.FromErr(client.DeleteSlo(sloID))
+}
+
+// Fetches all the Properties defined on the Terraform SLO State Object and converts it
+// to a Slo so that it can be converted to JSON and sent to the API
+func packSloResource(d *schema.ResourceData) (gapi.Slo, error) {
+ var (
+ tfalerting gapi.Alerting
+ tflabels []gapi.Label
+ )
+
+ tfname := d.Get("name").(string)
+ tfdescription := d.Get("description").(string)
+ query := d.Get("query").([]interface{})[0].(map[string]interface{})
+ tfquery, err := packQuery(query)
+ if err != nil {
+ return gapi.Slo{}, err
+ }
+
+ objectives := d.Get("objectives").([]interface{})
+ tfobjective := packObjectives(objectives)
+
+ labels := d.Get("label").([]interface{})
+ if labels != nil {
+ tflabels = packLabels(labels)
+ }
+
+ alerting := d.Get("alerting").([]interface{})
+ if len(alerting) > 0 {
+ alert := alerting[0].(map[string]interface{})
+ tfalerting = packAlerting(alert)
+ }
+
+ slo := gapi.Slo{
+ UUID: d.Id(),
+ Name: tfname,
+ Description: tfdescription,
+ Objectives: tfobjective,
+ Query: tfquery,
+ Alerting: &tfalerting,
+ Labels: tflabels,
+ }
+
+ return slo, nil
+}
+
+func packQuery(query map[string]interface{}) (gapi.Query, error) {
+ if query["type"] == "freeform" {
+ freeformquery := query["freeform"].([]interface{})[0].(map[string]interface{})
+ querystring := freeformquery["query"].(string)
+
+ sloQuery := gapi.Query{
+ Freeform: &gapi.FreeformQuery{Query: querystring},
+ Type: gapi.QueryTypeFreeform,
+ }
+
+ return sloQuery, nil
+ }
+
+ return gapi.Query{}, fmt.Errorf("%s query type not implemented", query["type"])
+}
+
+func packObjectives(tfobjectives []interface{}) []gapi.Objective {
+ objectives := []gapi.Objective{}
+
+ for ind := range tfobjectives {
+ tfobjective := tfobjectives[ind].(map[string]interface{})
+ objective := gapi.Objective{
+ Value: tfobjective["value"].(float64),
+ Window: tfobjective["window"].(string),
+ }
+ objectives = append(objectives, objective)
+ }
+
+ return objectives
+}
+
+func packLabels(tfLabels []interface{}) []gapi.Label {
+ labelSlice := []gapi.Label{}
+
+ for ind := range tfLabels {
+ currLabel := tfLabels[ind].(map[string]interface{})
+ curr := gapi.Label{
+ Key: currLabel["key"].(string),
+ Value: currLabel["value"].(string),
+ }
+
+ labelSlice = append(labelSlice, curr)
+ }
+
+ return labelSlice
+}
+
+func packAlerting(tfAlerting map[string]interface{}) gapi.Alerting {
+ annots := tfAlerting["annotation"].([]interface{})
+ tfAnnots := packLabels(annots)
+
+ labels := tfAlerting["label"].([]interface{})
+ tfLabels := packLabels(labels)
+
+ fastBurn := tfAlerting["fastburn"].([]interface{})
+ tfFastBurn := packAlertMetadata(fastBurn)
+
+ slowBurn := tfAlerting["slowburn"].([]interface{})
+ tfSlowBurn := packAlertMetadata(slowBurn)
+
+ alerting := gapi.Alerting{
+ Annotations: tfAnnots,
+ Labels: tfLabels,
+ FastBurn: &tfFastBurn,
+ SlowBurn: &tfSlowBurn,
+ }
+
+ return alerting
+}
+
+func packAlertMetadata(metadata []interface{}) gapi.AlertingMetadata {
+ meta := metadata[0].(map[string]interface{})
+
+ labels := meta["label"].([]interface{})
+ tflabels := packLabels(labels)
+
+ annots := meta["annotation"].([]interface{})
+ tfannots := packLabels(annots)
+
+ apiMetadata := gapi.AlertingMetadata{
+ Labels: tflabels,
+ Annotations: tfannots,
+ }
+
+ return apiMetadata
+}
+
+func setTerraformState(d *schema.ResourceData, slo gapi.Slo) {
+ d.Set("name", slo.Name)
+ d.Set("description", slo.Description)
+
+ if slo.DrillDownDashboardRef != nil {
+ d.Set("dashboard_uid", slo.DrillDownDashboardRef.UID)
+ }
+
+ d.Set("query", unpackQuery(slo.Query))
+
+ retLabels := unpackLabels(&slo.Labels)
+ d.Set("label", retLabels)
+
+ retObjectives := unpackObjectives(slo.Objectives)
+ d.Set("objectives", retObjectives)
+
+ retAlerting := unpackAlerting(slo.Alerting)
+ d.Set("alerting", retAlerting)
+}
diff --git a/internal/resources/slo/resource_slo_test.go b/internal/resources/slo/resource_slo_test.go
new file mode 100644
index 000000000..c4dfde612
--- /dev/null
+++ b/internal/resources/slo/resource_slo_test.go
@@ -0,0 +1,121 @@
+package slo_test
+
+import (
+ "fmt"
+ "regexp"
+ "testing"
+
+ gapi "github.com/grafana/grafana-api-golang-client"
+ "github.com/grafana/terraform-provider-grafana/internal/common"
+ "github.com/grafana/terraform-provider-grafana/internal/testutils"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+)
+
+func TestAccResourceSlo(t *testing.T) {
+ testutils.CheckCloudInstanceTestsEnabled(t)
+
+ var slo gapi.Slo
+ resource.ParallelTest(t, resource.TestCase{
+ ProviderFactories: testutils.ProviderFactories,
+ CheckDestroy: testAccSloCheckDestroy(&slo),
+ Steps: []resource.TestStep{
+ {
+ Config: testutils.TestAccExample(t, "resources/grafana_slo/resource.tf"),
+ Check: resource.ComposeTestCheckFunc(
+ testAccSloCheckExists("grafana_slo.test", &slo),
+ resource.TestCheckResourceAttrSet("grafana_slo.test", "id"),
+ resource.TestCheckResourceAttrSet("grafana_slo.test", "dashboard_uid"),
+ resource.TestCheckResourceAttr("grafana_slo.test", "name", "Terraform Testing"),
+ resource.TestCheckResourceAttr("grafana_slo.test", "description", "Terraform Description"),
+ resource.TestCheckResourceAttr("grafana_slo.test", "query.0.type", "freeform"),
+ resource.TestCheckResourceAttr("grafana_slo.test", "query.0.freeform.0.query", "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))"),
+ resource.TestCheckResourceAttr("grafana_slo.test", "objectives.0.value", "0.995"),
+ resource.TestCheckResourceAttr("grafana_slo.test", "objectives.0.window", "30d"),
+ ),
+ },
+ {
+ Config: testutils.TestAccExample(t, "resources/grafana_slo/resource_update.tf"),
+ Check: resource.ComposeTestCheckFunc(
+ testAccSloCheckExists("grafana_slo.update", &slo),
+ resource.TestCheckResourceAttrSet("grafana_slo.update", "id"),
+ resource.TestCheckResourceAttrSet("grafana_slo.update", "dashboard_uid"),
+ resource.TestCheckResourceAttr("grafana_slo.update", "name", "Updated - Terraform Testing"),
+ resource.TestCheckResourceAttr("grafana_slo.update", "description", "Updated - Terraform Description"),
+ resource.TestCheckResourceAttr("grafana_slo.update", "query.0.type", "freeform"),
+ resource.TestCheckResourceAttr("grafana_slo.update", "query.0.freeform.0.query", "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))"),
+ resource.TestCheckResourceAttr("grafana_slo.update", "objectives.0.value", "0.9995"),
+ resource.TestCheckResourceAttr("grafana_slo.update", "objectives.0.window", "7d"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccSloCheckExists(rn string, slo *gapi.Slo) resource.TestCheckFunc {
+ return func(s *terraform.State) error {
+ rs, ok := s.RootModule().Resources[rn]
+ if !ok {
+ return fmt.Errorf("resource not found: %s\n %#v", rn, s.RootModule().Resources)
+ }
+
+ if rs.Primary.ID == "" {
+ return fmt.Errorf("resource id not set")
+ }
+
+ client := testutils.Provider.Meta().(*common.Client).GrafanaAPI
+ gotSlo, err := client.GetSlo(rs.Primary.ID)
+
+ if err != nil {
+ return fmt.Errorf("error getting SLO: %s", err)
+ }
+
+ *slo = gotSlo
+
+ return nil
+ }
+}
+
+func testAccSloCheckDestroy(slo *gapi.Slo) resource.TestCheckFunc {
+ return func(s *terraform.State) error {
+ client := testutils.Provider.Meta().(*common.Client).GrafanaAPI
+ err := client.DeleteSlo(slo.UUID)
+
+ if err == nil {
+ return fmt.Errorf("SLO with a UUID %s still exists after destroy", slo.UUID)
+ }
+
+ return nil
+ }
+}
+
+const sloObjectivesInvalid = `
+resource "grafana_slo" "invalid" {
+ name = "Test SLO"
+ description = "Description Test SLO"
+ query {
+ freeform {
+ query = "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))"
+ }
+ type = "freeform"
+ }
+ objectives {
+ value = 1.5
+ window = "1m"
+ }
+}
+`
+
+func TestAccResourceInvalidSlo(t *testing.T) {
+ testutils.CheckCloudInstanceTestsEnabled(t)
+
+ resource.ParallelTest(t, resource.TestCase{
+ ProviderFactories: testutils.ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: sloObjectivesInvalid,
+ ExpectError: regexp.MustCompile("Error:"),
+ },
+ },
+ })
+}
diff --git a/tools/subcategories.json b/tools/subcategories.json
index fb4073dd5..b23638e20 100644
--- a/tools/subcategories.json
+++ b/tools/subcategories.json
@@ -47,6 +47,7 @@
"resources/oncall_outgoing_webhook": "OnCall",
"resources/oncall_route": "OnCall",
"resources/oncall_schedule": "OnCall",
+ "resources/slo": "SLO",
"resources/synthetic_monitoring_check": "Synthetic Monitoring",
"resources/synthetic_monitoring_installation": "Synthetic Monitoring",
"resources/synthetic_monitoring_probe": "Synthetic Monitoring",
@@ -72,6 +73,7 @@
"data-sources/oncall_team": "OnCall",
"data-sources/oncall_user": "OnCall",
"data-sources/oncall_user_group": "OnCall",
+ "data-sources/slos": "SLO",
"data-sources/synthetic_monitoring_probe": "Synthetic Monitoring",
"data-sources/synthetic_monitoring_probes": "Synthetic Monitoring"
}