diff --git a/docs/data-sources/slos.md b/docs/data-sources/slos.md new file mode 100644 index 000000000..58518f806 --- /dev/null +++ b/docs/data-sources/slos.md @@ -0,0 +1,141 @@ +--- +# 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/) + + + + +## 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) +- `labels` (List of Object) (see [below for nested schema](#nestedobjatt--slos--labels)) +- `name` (String) +- `objectives` (List of Object) (see [below for nested schema](#nestedobjatt--slos--objectives)) +- `query` (String) +- `uuid` (String) + + +### Nested Schema for `slos.alerting` + +Read-Only: + +- `annotations` (List of Object) (see [below for nested schema](#nestedobjatt--slos--alerting--annotations)) +- `fastburn` (List of Object) (see [below for nested schema](#nestedobjatt--slos--alerting--fastburn)) +- `labels` (List of Object) (see [below for nested schema](#nestedobjatt--slos--alerting--labels)) +- `slowburn` (List of Object) (see [below for nested schema](#nestedobjatt--slos--alerting--slowburn)) + + +### Nested Schema for `slos.alerting.annotations` + +Read-Only: + +- `key` (String) +- `value` (String) + + + +### Nested Schema for `slos.alerting.fastburn` + +Read-Only: + +- `annotations` (List of Object) (see [below for nested schema](#nestedobjatt--slos--alerting--fastburn--annotations)) +- `labels` (List of Object) (see [below for nested schema](#nestedobjatt--slos--alerting--fastburn--labels)) + + +### Nested Schema for `slos.alerting.fastburn.labels` + +Read-Only: + +- `key` (String) +- `value` (String) + + + +### Nested Schema for `slos.alerting.fastburn.labels` + +Read-Only: + +- `key` (String) +- `value` (String) + + + + +### Nested Schema for `slos.alerting.labels` + +Read-Only: + +- `key` (String) +- `value` (String) + + + +### Nested Schema for `slos.alerting.slowburn` + +Read-Only: + +- `annotations` (List of Object) (see [below for nested schema](#nestedobjatt--slos--alerting--slowburn--annotations)) +- `labels` (List of Object) (see [below for nested schema](#nestedobjatt--slos--alerting--slowburn--labels)) + + +### Nested Schema for `slos.alerting.slowburn.labels` + +Read-Only: + +- `key` (String) +- `value` (String) + + + +### Nested Schema for `slos.alerting.slowburn.labels` + +Read-Only: + +- `key` (String) +- `value` (String) + + + + + +### Nested Schema for `slos.labels` + +Read-Only: + +- `key` (String) +- `value` (String) + + + +### Nested Schema for `slos.objectives` + +Read-Only: + +- `value` (Number) +- `window` (String) + + diff --git a/docs/resources/slo.md b/docs/resources/slo.md new file mode 100644 index 000000000..1b24ba45d --- /dev/null +++ b/docs/resources/slo.md @@ -0,0 +1,273 @@ +--- +# 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 { + query_type = "freeform" + freeform_query = "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))" + } + objectives { + value = 0.995 + window = "30d" + } + labels { + key = "custom" + value = "value" + } + alerting { + fastburn { + annotations { + key = "name" + value = "Critical - SLO Burn Rate Alert" + } + labels { + key = "type" + value = "slo" + } + } + + slowburn { + annotations { + key = "name" + value = "Warning - SLO Burn Rate Alert" + } + labels { + 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, Max: 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. (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)) +- `labels` (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--labels)) + +### 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: + +- `query_type` (String) Query type must be one of freeform, ratio, percentile, or threshold Queries. + +Optional: + +- `freeform_query` (String) +- `group_by_labels` (List of String) +- `percentile_query` (Block List) (see [below for nested schema](#nestedblock--query--percentile_query)) +- `ratio_query` (Block List) (see [below for nested schema](#nestedblock--query--ratio_query)) +- `threshold` (Block List) (see [below for nested schema](#nestedblock--query--threshold)) +- `threshold_query` (Block List) (see [below for nested schema](#nestedblock--query--threshold_query)) + + +### Nested Schema for `query.percentile_query` + +Optional: + +- `histogram_metric` (Block List) (see [below for nested schema](#nestedblock--query--percentile_query--histogram_metric)) +- `percentile` (Number) + + +### Nested Schema for `query.percentile_query.histogram_metric` + +Optional: + +- `metric` (String) +- `type` (String) + + + + +### Nested Schema for `query.ratio_query` + +Optional: + +- `success_metric` (Block List) (see [below for nested schema](#nestedblock--query--ratio_query--success_metric)) +- `total_metric` (Block List) (see [below for nested schema](#nestedblock--query--ratio_query--total_metric)) + + +### Nested Schema for `query.ratio_query.success_metric` + +Optional: + +- `metric` (String) +- `type` (String) + + + +### Nested Schema for `query.ratio_query.total_metric` + +Optional: + +- `metric` (String) +- `type` (String) + + + + +### Nested Schema for `query.threshold` + +Optional: + +- `operator` (String) +- `value` (Number) + + + +### Nested Schema for `query.threshold_query` + +Optional: + +- `threshold_metric` (Block List) (see [below for nested schema](#nestedblock--query--threshold_query--threshold_metric)) + + +### Nested Schema for `query.threshold_query.threshold_metric` + +Required: + +- `metric` (String) +- `type` (String) + + + + + +### Nested Schema for `alerting` + +Optional: + +- `annotations` (Block List) Annotations will be attached to all alerts generated by any of these rules. (see [below for nested schema](#nestedblock--alerting--annotations)) +- `fastburn` (Block List) Alerting Rules generated for Fast Burn alerts (see [below for nested schema](#nestedblock--alerting--fastburn)) +- `labels` (Block List) Labels will be attached to all alerts generated by any of these rules. (see [below for nested schema](#nestedblock--alerting--labels)) +- `slowburn` (Block List) Alerting Rules generated for Slow Burn alerts (see [below for nested schema](#nestedblock--alerting--slowburn)) + + +### Nested Schema for `alerting.annotations` + +Required: + +- `key` (String) +- `value` (String) + + + +### Nested Schema for `alerting.fastburn` + +Optional: + +- `annotations` (Block List) Annotations to attach only to Fast Burn alerts. (see [below for nested schema](#nestedblock--alerting--fastburn--annotations)) +- `labels` (Block List) Labels to attach only to Fast Burn alerts. (see [below for nested schema](#nestedblock--alerting--fastburn--labels)) + + +### Nested Schema for `alerting.fastburn.annotations` + +Required: + +- `key` (String) +- `value` (String) + + + +### Nested Schema for `alerting.fastburn.labels` + +Required: + +- `key` (String) +- `value` (String) + + + + +### Nested Schema for `alerting.labels` + +Required: + +- `key` (String) +- `value` (String) + + + +### Nested Schema for `alerting.slowburn` + +Optional: + +- `annotations` (Block List) Annotations to attach only to Slow Burn alerts. (see [below for nested schema](#nestedblock--alerting--slowburn--annotations)) +- `labels` (Block List) Labels to attach only to Slow Burn alerts. (see [below for nested schema](#nestedblock--alerting--slowburn--labels)) + + +### Nested Schema for `alerting.slowburn.annotations` + +Required: + +- `key` (String) +- `value` (String) + + + +### Nested Schema for `alerting.slowburn.labels` + +Required: + +- `key` (String) +- `value` (String) + + + + + +### Nested Schema for `labels` + +Required: + +- `key` (String) +- `value` (String) + + diff --git a/examples/data-sources/grafana_slo/data-source.tf b/examples/data-sources/grafana_slo/data-source.tf new file mode 100644 index 000000000..d69b5dc75 --- /dev/null +++ b/examples/data-sources/grafana_slo/data-source.tf @@ -0,0 +1 @@ +data "grafana_slo" "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..0dfe6229c --- /dev/null +++ b/examples/resources/grafana_slo/resource.tf @@ -0,0 +1,38 @@ +resource "grafana_slo" "test" { + name = "Terraform Testing" + description = "Terraform Description" + query { + query_type = "freeform" + freeform_query = "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))" + } + objectives { + value = 0.995 + window = "30d" + } + labels { + key = "custom" + value = "value" + } + alerting { + fastburn { + annotations { + key = "name" + value = "Critical - SLO Burn Rate Alert" + } + labels { + key = "type" + value = "slo" + } + } + slowburn { + annotations { + key = "name" + value = "Warning - SLO Burn Rate Alert" + } + labels { + 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..490581869 --- /dev/null +++ b/examples/resources/grafana_slo/resource_complex.tf @@ -0,0 +1,54 @@ +resource "grafana_slo" "complex" { + name = "Complex Resource - Terraform Testing" + description = "Complex Resource - Terraform Description" + query { + query_type = "freeform" + freeform_query = "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))" + } + objectives { + value = 0.995 + window = "30d" + } + labels { + key = "slokey" + value = "slokey" + } + alerting { + name = "alertingname" + + labels { + key = "alertingkey" + value = "alertingvalue" + } + + fastburn { + annotations { + key = "name" + value = "Critical - SLO Burn Rate Alert" + } + annotations { + key = "description" + value = "Error Budget is burning at a rate greater than 14.4x." + } + labels { + key = "type" + value = "slo" + } + } + + slowburn { + annotations { + key = "name" + value = "Warning - SLO Burn Rate Alert" + } + annotations { + key = "description" + value = "Error Budget is burning at a rate greater than 1x." + } + labels { + 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..01f8c8bdf --- /dev/null +++ b/examples/resources/grafana_slo/resource_update.tf @@ -0,0 +1,38 @@ +resource "grafana_slo" "update" { + name = "Updated - Terraform Testing" + description = "Updated - Terraform Description" + query { + query_type = "freeform" + freeform_query = "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))" + } + objectives { + value = 0.9995 + window = "7d" + } + labels { + key = "customkey" + value = "customvalue" + } + alerting { + fastburn { + annotations { + key = "name" + value = "Critical - SLO Burn Rate Alert" + } + labels { + key = "type" + value = "slo" + } + } + slowburn { + annotations { + key = "name" + value = "Warning - SLO Burn Rate Alert" + } + labels { + key = "type" + value = "slo" + } + } + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index 784b7a99c..99f13acb7 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/grafana/terraform-provider-grafana go 1.18 +replace github.com/grafana/grafana-api-golang-client => github.com/grafana/grafana-api-golang-client v0.20.2-0.20230501160934-caec3d595086 + require ( github.com/Masterminds/semver/v3 v3.2.1 github.com/grafana/amixr-api-go-client v0.0.7 diff --git a/go.sum b/go.sum index f1d527f14..ff0134474 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.20.2-0.20230501160934-caec3d595086 h1:lacP8lq3OGk4R5sQh90R7SG9KzSCG5RkS33bJ849w1w= +github.com/grafana/grafana-api-golang-client v0.20.2-0.20230501160934-caec3d595086/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.4 h1:amLwPpBvWnqoYHg4Dn2IBdkDy9szcRLr7yCJHMXNhG8= diff --git a/internal/provider/provider.go b/internal/provider/provider.go index fbca4d052..cece5933c 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. diff --git a/internal/resources/slo/data_source_slo.go b/internal/resources/slo/data_source_slo.go new file mode 100644 index 000000000..5955715b4 --- /dev/null +++ b/internal/resources/slo/data_source_slo.go @@ -0,0 +1,481 @@ +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, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "slos": &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Description: `Returns a list of all SLOs"`, + Elem: &schema.Resource{ + Schema: 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, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: `Name should be a short description of your indicator. Consider names like "API Availability"`, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: `Description is a free-text field that can provide more context to an SLO.`, + }, + "query": &schema.Schema{ + Type: schema.TypeList, + Required: true, + Description: `Query describes the indicator that will be measured against the objective.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "query_type": { + Type: schema.TypeString, + Required: true, + Description: `Query type must be one of freeform, ratio, percentile, or threshold Queries.`, + }, + "freeform_query": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "ratio_query": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "success_metric": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "metric": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "total_metric": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "metric": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + "percentile_query": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "histogram_metric": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "metric": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "percentile": { + Type: schema.TypeFloat, + Optional: true, + }, + }, + }, + }, + "threshold_query": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "threshold_metric": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "metric": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + }, + }, + }, + "threshold": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "value": &schema.Schema{ + Type: schema.TypeFloat, + Optional: true, + }, + "operator": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + "labels": &schema.Schema{ + Type: schema.TypeList, + Computed: 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: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "value": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "objectives": &schema.Schema{ + Type: schema.TypeList, + Computed: 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, + Computed: true, + 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, + Computed: true, + Description: `A Prometheus-parsable time duration string like 24h, 60m. This is the time window the objective is measured over.`, + }, + }, + }, + }, + "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, + Computed: 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{ + "labels": &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Description: `Labels will be attached to all alerts generated by any of these rules.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "value": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "annotations": &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Description: `Annotations will be attached to all alerts generated by any of these rules.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "value": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "fastburn": &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Description: "Alerting Rules generated for Fast Burn alerts", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "labels": &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Description: "Labels to attach only to Fast Burn alerts.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "value": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "annotations": &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Description: "Annotations to attach only to Fast Burn alerts.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "value": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + "slowburn": &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Description: "Alerting Rules generated for Slow Burn alerts", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "labels": &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Description: "Labels to attach only to Slow Burn alerts.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "value": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "annotations": &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Description: "Annotations to attach only to Slow Burn alerts.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "value": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +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 + ret["dashboard_uid"] = slo.DrillDownDashboardRef.UID + ret["query"] = unpackQuery(slo.Query) + + retLabels := unpackLabels(&slo.Labels) + ret["labels"] = retLabels + + retObjectives := unpackObjectives(slo.Objectives) + ret["objectives"] = retObjectives + + retAlerting := unpackAlerting(slo.Alerting) + ret["alerting"] = retAlerting + + return ret +} + +func unpackQuery(query gapi.Query) []map[string]interface{} { + retQueries := []map[string]interface{}{} + + if query.FreeformQuery.Query != "" { + retQuery := make(map[string]interface{}) + retQuery["query_type"] = "freeform" + retQuery["freeform_query"] = query.FreeformQuery.Query + retQueries = append(retQueries, retQuery) + } + + return retQueries +} + +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["labels"] = unpackLabels(alertData.Labels) + alertObject["annotations"] = 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.AlertMetadata) []map[string]interface{} { + retAlertMetaData := []map[string]interface{}{} + labelsAnnotsStruct := make(map[string]interface{}) + + if metaData.Annotations != nil { + retAnnotations := unpackLabels(metaData.Annotations) + labelsAnnotsStruct["annotations"] = retAnnotations + } + + if metaData.Labels != nil { + retLabels := unpackLabels(metaData.Labels) + labelsAnnotsStruct["labels"] = 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..c6a17065f --- /dev/null +++ b/internal/resources/slo/data_source_slo_test.go @@ -0,0 +1,23 @@ +package slo_test + +// import ( +// "regexp" +// "testing" + +// "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) + +// resource.ParallelTest(t, resource.TestCase{ +// ProviderFactories: testutils.ProviderFactories, +// Steps: []resource.TestStep{ +// { +// Config: testutils.TestAccExample(t, "data-sources/grafana_slo/data-source.tf"), +// ExpectError: regexp.MustCompile(`No SLOs Exist`), +// }, +// }, +// }) +// } diff --git a/internal/resources/slo/resource_slo.go b/internal/resources/slo/resource_slo.go new file mode 100644 index 000000000..750e6351d --- /dev/null +++ b/internal/resources/slo/resource_slo.go @@ -0,0 +1,596 @@ +package slo + +import ( + "context" + "errors" + "fmt" + + 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 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"`, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: `Description is a free-text field that can provide more context to an SLO.`, + }, + "query": &schema.Schema{ + Type: schema.TypeList, + Required: true, + Description: `Query describes the indicator that will be measured against the objective.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "query_type": { + Type: schema.TypeString, + Required: true, + Description: `Query type must be one of freeform, ratio, percentile, or threshold Queries.`, + }, + "freeform_query": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "ratio_query": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "success_metric": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "metric": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "total_metric": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "metric": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + "percentile_query": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "histogram_metric": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "metric": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "percentile": { + Type: schema.TypeFloat, + Optional: true, + }, + }, + }, + }, + "threshold_query": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "threshold_metric": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "metric": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + }, + }, + }, + "threshold": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "value": &schema.Schema{ + Type: schema.TypeFloat, + Optional: true, + }, + "operator": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + "labels": &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: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "value": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "objectives": &schema.Schema{ + Type: schema.TypeList, + MaxItems: 1, + 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, + 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.`, + }, + }, + }, + }, + "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{ + "labels": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Description: `Labels will be attached to all alerts generated by any of these rules.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "value": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "annotations": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Description: `Annotations will be attached to all alerts generated by any of these rules.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "value": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "fastburn": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Description: "Alerting Rules generated for Fast Burn alerts", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "labels": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Description: "Labels to attach only to Fast Burn alerts.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "value": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "annotations": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Description: "Annotations to attach only to Fast Burn alerts.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "value": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + }, + }, + }, + "slowburn": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Description: "Alerting Rules generated for Slow Burn alerts", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "labels": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Description: "Labels to attach only to Slow Burn alerts.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "value": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "annotations": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Description: "Annotations to attach only to Slow Burn alerts.", + Elem: &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 create SLO", + Detail: fmt.Sprintf("Error Message:%s", err.Error()), + }) + } + + 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", + Detail: fmt.Sprintf("API Error Message:%s", err.Error()), + }) + + return diags + } + + d.SetId(response.UUID) + resourceSloRead(ctx, d, m) + return diags +} + +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("labels") || 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: fmt.Sprintf("Error Message:%s", err.Error()), + }) + } + + 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 { + var diags diag.Diagnostics + + sloID := d.Id() + + client := m.(*common.Client).GrafanaAPI + client.DeleteSlo(sloID) + + d.SetId("") + + return diags +} + +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{}) + queryElements := query[0].(map[string]interface{}) + tfquery, err := packQuery(queryElements) + + if err != nil { + return gapi.Slo{}, err + } + + // Assumes that each SLO only has one Objective Value and one Objective Window + objectives := d.Get("objectives").([]interface{}) + objective := objectives[0].(map[string]interface{}) + tfobjective := packObjective(objective) + + labels := d.Get("labels").([]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(tfquery map[string]interface{}) (gapi.Query, error) { + var query gapi.Query + queryType := tfquery["query_type"].(string) + + switch queryType { + case "freeform": + freeformQuery := tfquery["freeform_query"].(string) + query = gapi.Query{ + FreeformQuery: packFreeformQuery(freeformQuery), + } + return query, nil + case "ratio": + return query, errors.New("ratio query not yet implemented") + case "percentile": + return query, errors.New("percentile query not yet implemented") + case "threshold": + return query, errors.New("threshold query not yet implemented") + default: + return query, errors.New("query must be of type freeform, ratio, percentile, or threshold") + } +} + +func packFreeformQuery(query string) gapi.FreeformQuery { + return gapi.FreeformQuery{ + Query: query, + } +} + +func packObjective(tfobjective map[string]interface{}) []gapi.Objective { + objective := gapi.Objective{ + Value: tfobjective["value"].(float64), + Window: tfobjective["window"].(string), + } + + objectiveSlice := []gapi.Objective{} + objectiveSlice = append(objectiveSlice, objective) + + return objectiveSlice +} + +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["annotations"].([]interface{}) + tfAnnots := packLabels(annots) + + labels := tfAlerting["labels"].([]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.AlertMetadata { + meta := metadata[0].(map[string]interface{}) + + labels := meta["labels"].([]interface{}) + tflabels := packLabels(labels) + + annots := meta["annotations"].([]interface{}) + tfannots := packLabels(annots) + + apiMetadata := gapi.AlertMetadata{ + Labels: &tflabels, + Annotations: &tfannots, + } + + return apiMetadata +} + +func setTerraformState(d *schema.ResourceData, slo gapi.Slo) { + d.Set("name", slo.Name) + d.Set("description", slo.Description) + d.Set("dashboard_uid", slo.DrillDownDashboardRef.UID) + d.Set("query", unpackQuery(slo.Query)) + + retLabels := unpackLabels(&slo.Labels) + d.Set("labels", 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..1a2f7db9d --- /dev/null +++ b/internal/resources/slo/resource_slo_test.go @@ -0,0 +1,87 @@ +package slo_test + +import ( + "fmt" + "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.freeform_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.freeform_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 + } +} diff --git a/slo_testing/slo-datasource-read.tf b/slo_testing/slo-datasource-read.tf new file mode 100644 index 000000000..65d6cbe1f --- /dev/null +++ b/slo_testing/slo-datasource-read.tf @@ -0,0 +1,18 @@ +terraform { + required_providers { + grafana = { + source = "registry.terraform.io/grafana/grafana" + } + } +} + +provider "grafana" { + url = "https://elainetest.grafana.net/" +} + +data "grafana_slo" "test1" { +} + +output "test1" { + value = data.grafana_slo.test1 +} \ No newline at end of file diff --git a/slo_testing/slo-resource-create.tf b/slo_testing/slo-resource-create.tf new file mode 100644 index 000000000..cb436f1e0 --- /dev/null +++ b/slo_testing/slo-resource-create.tf @@ -0,0 +1,63 @@ +terraform { + required_providers { + grafana = { + source = "registry.terraform.io/grafana/grafana" + } + } +} + +provider "grafana" { + url = "https://elainetest.grafana.net/" +} + +resource "grafana_slo" "test1" { + name = "Terraform - Name Test" + description = "Terraform - Description Test" + query { + query_type = "freeform" + freeform_query = "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))" + } + objectives { + value = 0.995 + window = "30d" + } + labels { + key = "custom" + value = "value" + } + alerting { + fastburn { + annotations { + key = "name" + value = "Critical - SLO Burn Rate Alert" + } + annotations { + key = "description" + value = "Error Budget Burning Very Quickly" + } + labels { + key = "type" + value = "slo" + } + } + + slowburn { + annotations { + key = "name" + value = "Warning - SLO Burn Rate Alert" + } + annotations { + key = "description" + value = "Error Budget Burning Quickly" + } + labels { + key = "type" + value = "slo" + } + } + } +} + +output "test1" { + value = grafana_slo.test1 +} diff --git a/slo_testing/slo-resource-import.tf b/slo_testing/slo-resource-import.tf new file mode 100644 index 000000000..67ba62d56 --- /dev/null +++ b/slo_testing/slo-resource-import.tf @@ -0,0 +1,28 @@ +terraform { + required_providers { + grafana = { + source = "registry.terraform.io/grafana/grafana" + } + } +} + +provider "grafana" { + url = "https://elainetest.grafana.net/" +} + +resource "grafana_slo" "sample" { + name = "Terraform - Import Test Name" + description = "Terraform - Import Test Description" + query { + query_type = "freeform" + freeform_query = "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))" + } + objectives { + objective_value = 0.995 + objective_window = "30d" + } +} + +output "sample_slo" { + value = grafana_slo.sample +} diff --git a/slo_testing/slo-testing-README.md b/slo_testing/slo-testing-README.md new file mode 100644 index 000000000..4d7a62097 --- /dev/null +++ b/slo_testing/slo-testing-README.md @@ -0,0 +1,82 @@ +# How to Test the SLO Terraform Provider - Hosted Grafana + +# Installation +Install Terraform here - https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli#install-terraform. + +## HG Account Set Up +For members of the SLO Team, you should be able to use the `sloapp.grafana-dev.net` for testing or your own HG Instance. +Within Administration, generate a new Service Account Token. + +Set the environment variable GRAFANA_AUTH to the value of your token `export GRAFANA_AUTH=` +Within the `.tf` files within `slo_testing/hg`, ensure that you set the `url` field to be the `url` of your HG Instance. + +## Creating the TF Binary +1. Within the grafana-terraform-provider root directory, run `go build`. This creates a binary of the terraform-provider-grafana. +2. Within the grafana-terraform-provider root directory, create a file called `.terraformrc` with the following contents. Update the path to the path of your local `grafana-terraform-provider`. This ensures that it will use the local binary version of the terraform-provider-grafana. +``` +provider_installation { + dev_overrides { + "grafana/grafana" = "/path/to/your/grafana/terraform-provider" # this path is the directory where the binary is built + } + # For all other providers, install them directly from their origin provider + # registries as normal. If you omit this, Terraform will _only_ use + # the dev_overrides block, and so no other providers will be available. + direct {} + } +``` + +### Types of Resources +Datasource - datasources are resources that are external to Terraform (i.e. not managed by Terraform state). When interacting with a Datasource, they can be used to READ information, and datasources can also be imported (i.e. converted) into Resources, which allows Terraform state to control them. + +Resources - these are resources that can be managed by Terraform state. This means that you CREATE, READ, UPDATE, DELETE them. + +## Testing Datasource - READ +Objective - we want to send a GET Request to the SLO Endpoint that returns a list of all SLOs, and we want to be able to READ that information and output it to the Terraform CLI. + +1. Delete any `.terraform.lock.hcl` and `terraform.tfstate` and `terraform.tfstate.backup` files. Within the terraform-provider-grafana root directory, run `go build`. Set the GRAFANA_AUTH environment variable to your HG Grafana API Key, if not already done. +2. Change to the `slo_testing` directory `cd slo_testing`. +3. Within your SLO UI, create a SLO if one does not already exist. +4. Within the `slo-datasource-read.tf` file, ensure the url is set to the url of your HG Instance. +5. Comment out all the `.tf` files within the `slo_testing` folder, EXCEPT for the `slo-datasource-read.tf` file +6. Within the `slo_testing` directory, run the commands `terraform init` and `terraform apply`. +7. You should see a list of all SLOs within your Terraform CLI. + +## Testing Resource - CREATE +Objective - we want to be able to define a SLO Resource within Terraform state that should be created. Once the resource has successfully been created, we want to display the newly created SLO resource within the Terraform CLI. + +The `slo-resource-create.tf` file will create two SLOs. + +1. Change to the `slo_testing/hg` directory. +2. Comment out all the `.tf` files within the `slo_testing` folder, EXCEPT for the `slo-resource-create.tf` file +3. Within the `slo_testing` directory, run the command `terraform apply`. +4. Within your terminal, you should see the output of the a newly created SLO from within Terraform, and the same newly created SLO within the SLO UI. + +## Testing the UPDATE Method +Objective - we want to be able to update a SLO Resource created within Terraform. Once the resource has successfully been modified, we want to display the newly created SLO resource within the Terraform CLI. + +1. Do NOT delete any `.terraform.lock.hcl` and `terraform.tfstate` and `terraform.tfstate.backup` files. Ensure that this step is executed after testing the CREATE method. +2. Change to the `slo_testing` directory. +3. Comment out all the `.tf` files within the `slo_testing` folder, EXCEPT for the `slo-resource-create.tf` file +4. Modify any of the fields within the `slo-resource-create.tf` file - for example, you can change the `name` field to read `"Updated Terraform - Name Test"`. +5. Within the `slo_testing` directory, run the command `terraform apply`. This should update the resource specified in the terraform state file. +6. Check within the UI that the update was successful. + +## Testing the DELETE/DESTROY Method +Objective - we want to be able to delete a SLO Resource that was created with Terraform. + +1. Do NOT delete any `.terraform.lock.hcl` and `terraform.tfstate` and `terraform.tfstate.backup` files. Ensure that this step is executed after testing the UPDATE method. +2. Change to the `slo_testing` directory. +3. To delete all Terraformed SLO resources, execute the command `terraform destroy`, and type `yes` in the terminal to confirm the delete +4. Any SLO Resources created with Terraform should be deleted. + +### Testing the IMPORT Method +1. Change to the `slo_testing` directory. +2. Comment out all the `.tf` files within the `slo_testing` folder, EXCEPT for the `slo-resource-import.tf` file +3. Create a SLO using the UI or Postman. Take note of the SLO's UUID +4. Within the Terraform CLI directly, execute the command: `terraform import grafana_slo.sample slo_UUID` +5. Now execute the command: `terraform state show grafana_slo.sample` - you should see the data from the imported Resource. +6. To verify that this resource is now under Terraform control, execute the command `terraform destroy`. This should destroy the resource from within the Terraform CLI. + +### TBD ### +1. Once the GAPI Branch has been approved, remove the `replace` within `go.mod` +2. Remove `slo_testing` folder 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" }