Skip to content

Commit 8fd7bb0

Browse files
2396: Adding support for SLO TF to support Asserts SLO (#2397)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 8912e3d commit 8fd7bb0

File tree

3 files changed

+190
-4
lines changed

3 files changed

+190
-4
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
resource "grafana_slo" "asserts_example" {
2+
name = "Asserts SLO Example"
3+
description = "SLO managed by Asserts for entity-centric monitoring and RCA"
4+
query {
5+
freeform {
6+
query = "sum(rate(apiserver_request_total{code!=\"500\"}[$__rate_interval])) / sum(rate(apiserver_request_total[$__rate_interval]))"
7+
}
8+
type = "freeform"
9+
}
10+
objectives {
11+
value = 0.995
12+
window = "30d"
13+
}
14+
destination_datasource {
15+
uid = "grafanacloud-prom"
16+
}
17+
18+
# Asserts integration labels
19+
# The grafana_slo_provenance label triggers Asserts-specific behavior:
20+
# - Displays "asserts" badge instead of "provisioned"
21+
# - Shows "Open RCA workbench" button in the SLO UI
22+
# - Enables correlation with Asserts entity-centric monitoring
23+
label {
24+
key = "grafana_slo_provenance"
25+
value = "asserts"
26+
}
27+
label {
28+
key = "service_name"
29+
value = "my-service"
30+
}
31+
label {
32+
key = "team_name"
33+
value = "platform-team"
34+
}
35+
36+
# Search expression for Asserts RCA workbench
37+
# This enables the "Open RCA workbench" button to deep-link with pre-filtered context
38+
search_expression = "service=my-service"
39+
40+
alerting {
41+
fastburn {
42+
annotation {
43+
key = "name"
44+
value = "SLO Burn Rate Very High"
45+
}
46+
annotation {
47+
key = "description"
48+
value = "Error budget is burning too fast"
49+
}
50+
}
51+
52+
slowburn {
53+
annotation {
54+
key = "name"
55+
value = "SLO Burn Rate High"
56+
}
57+
annotation {
58+
key = "description"
59+
value = "Error budget is burning too fast"
60+
}
61+
}
62+
}
63+
}
64+

internal/resources/slo/resource_slo.go

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,51 @@ const (
2121
QueryTypeRatio string = "ratio"
2222
QueryTypeThreshold string = "threshold"
2323
QueryTypeGrafanaQueries string = "grafanaQueries"
24+
25+
// Asserts integration constants
26+
AssertsProvenanceLabel = "grafana_slo_provenance"
27+
AssertsProvenanceValue = "asserts"
28+
AssertsRequestHeader = "Grafana-Asserts-Request"
2429
)
2530

2631
var resourceSloID = common.NewResourceID(common.StringIDField("uuid"))
2732

33+
// hasAssertsProvenanceLabel checks if the SLO has the grafana_slo_provenance=asserts label
34+
func hasAssertsProvenanceLabel(labels []slo.SloV00Label) bool {
35+
for _, label := range labels {
36+
if label.Key == AssertsProvenanceLabel && label.Value == AssertsProvenanceValue {
37+
return true
38+
}
39+
}
40+
return false
41+
}
42+
43+
// createAssertsSLOClient creates a new SLO client with Asserts headers
44+
func createAssertsSLOClient(baseClient *slo.APIClient) *slo.APIClient {
45+
// Create a new configuration with the Asserts header
46+
config := slo.NewConfiguration()
47+
48+
// Copy the base client configuration
49+
config.Host = baseClient.GetConfig().Host
50+
config.Scheme = baseClient.GetConfig().Scheme
51+
config.HTTPClient = baseClient.GetConfig().HTTPClient
52+
53+
// Copy existing headers BUT exclude the Terraform provider header
54+
// The API checks Terraform header first, so we must remove it to allow Asserts provenance
55+
config.DefaultHeader = make(map[string]string)
56+
for k, v := range baseClient.GetConfig().DefaultHeader {
57+
// Skip the Terraform provider header
58+
if k == "Grafana-Terraform-Provider" {
59+
continue
60+
}
61+
config.DefaultHeader[k] = v
62+
}
63+
// Add the Asserts header which will now be checked by the API
64+
config.DefaultHeader[AssertsRequestHeader] = "true"
65+
66+
return slo.NewAPIClient(config)
67+
}
68+
2869
func resourceSlo() *common.Resource {
2970
schema := &schema.Resource{
3071
Description: `
@@ -329,7 +370,13 @@ func resourceSloCreate(ctx context.Context, d *schema.ResourceData, client *slo.
329370
return diags
330371
}
331372

332-
req := client.DefaultAPI.V1SloPost(ctx).SloV00Slo(sloModel)
373+
// Check if this SLO has Asserts provenance and create a custom client if needed
374+
apiClient := client
375+
if hasAssertsProvenanceLabel(sloModel.Labels) {
376+
apiClient = createAssertsSLOClient(client)
377+
}
378+
379+
req := apiClient.DefaultAPI.V1SloPost(ctx).SloV00Slo(sloModel)
333380
response, _, err := req.Execute()
334381

335382
if err != nil {
@@ -377,7 +424,13 @@ func resourceSloUpdate(ctx context.Context, d *schema.ResourceData, client *slo.
377424
return diags
378425
}
379426

380-
req := client.DefaultAPI.V1SloIdPut(ctx, sloID).SloV00Slo(sloV00Slo)
427+
// Check if this SLO has Asserts provenance and create a custom client if needed
428+
apiClient := client
429+
if hasAssertsProvenanceLabel(sloV00Slo.Labels) {
430+
apiClient = createAssertsSLOClient(client)
431+
}
432+
433+
req := apiClient.DefaultAPI.V1SloIdPut(ctx, sloID).SloV00Slo(sloV00Slo)
381434
if _, err := req.Execute(); err != nil {
382435
return apiError("Unable to Update SLO - API", err)
383436
}

internal/resources/slo/resource_slo_test.go

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,26 @@ func TestAccResourceSlo(t *testing.T) {
142142
ImportState: true,
143143
ImportStateVerify: true,
144144
},
145+
{
146+
// Tests Asserts Integration
147+
Config: testutils.TestAccExampleWithReplace(t, "resources/grafana_slo/resource_asserts.tf", map[string]string{
148+
"Asserts SLO Example": randomName + " - Asserts",
149+
}),
150+
Check: resource.ComposeTestCheckFunc(
151+
testAccSloCheckExistsWithProvenance("grafana_slo.asserts_example", &slo, "asserts"),
152+
resource.TestCheckResourceAttrSet("grafana_slo.asserts_example", "id"),
153+
resource.TestCheckResourceAttr("grafana_slo.asserts_example", "name", randomName+" - Asserts"),
154+
// Verify Asserts integration labels
155+
resource.TestCheckResourceAttr("grafana_slo.asserts_example", "label.0.key", "grafana_slo_provenance"),
156+
resource.TestCheckResourceAttr("grafana_slo.asserts_example", "label.0.value", "asserts"),
157+
resource.TestCheckResourceAttr("grafana_slo.asserts_example", "label.1.key", "service_name"),
158+
resource.TestCheckResourceAttr("grafana_slo.asserts_example", "label.2.key", "team_name"),
159+
// Verify search expression
160+
resource.TestCheckResourceAttr("grafana_slo.asserts_example", "search_expression", "service=my-service"),
161+
// Verify the SLO has the correct Asserts provenance
162+
testAccSloCheckAssertsProvenance("grafana_slo.asserts_example"),
163+
),
164+
},
145165
},
146166
})
147167
}
@@ -211,6 +231,10 @@ func TestAccSLO_recreate(t *testing.T) {
211231
}
212232

213233
func testAccSloCheckExists(rn string, slo *slo.SloV00Slo) resource.TestCheckFunc {
234+
return testAccSloCheckExistsWithProvenance(rn, slo, "terraform")
235+
}
236+
237+
func testAccSloCheckExistsWithProvenance(rn string, slo *slo.SloV00Slo, expectedProvenance string) resource.TestCheckFunc {
214238
return func(s *terraform.State) error {
215239
rs, ok := s.RootModule().Resources[rn]
216240
if !ok {
@@ -228,8 +252,8 @@ func testAccSloCheckExists(rn string, slo *slo.SloV00Slo) resource.TestCheckFunc
228252
return fmt.Errorf("error getting SLO: %s", err)
229253
}
230254

231-
if *gotSlo.ReadOnly.Provenance != "terraform" {
232-
return fmt.Errorf("provenance header missing - verify within the Grafana Terraform Provider that the 'Grafana-Terraform-Provider' request header is set to 'true'")
255+
if *gotSlo.ReadOnly.Provenance != expectedProvenance {
256+
return fmt.Errorf("expected provenance to be '%s', got '%s'", expectedProvenance, *gotSlo.ReadOnly.Provenance)
233257
}
234258

235259
*slo = *gotSlo
@@ -721,3 +745,48 @@ resource "grafana_slo" "custom_uuid_test" {
721745
}
722746
`, uuid)
723747
}
748+
749+
// testAccSloCheckAssertsProvenance verifies that the SLO has the correct Asserts provenance
750+
func testAccSloCheckAssertsProvenance(rn string) resource.TestCheckFunc {
751+
return func(s *terraform.State) error {
752+
rs, ok := s.RootModule().Resources[rn]
753+
if !ok {
754+
return fmt.Errorf("resource not found: %s", rn)
755+
}
756+
757+
if rs.Primary.ID == "" {
758+
return fmt.Errorf("resource id not set")
759+
}
760+
761+
client := testutils.Provider.Meta().(*common.Client).SLOClient
762+
req := client.DefaultAPI.V1SloIdGet(context.Background(), rs.Primary.ID)
763+
gotSlo, _, err := req.Execute()
764+
if err != nil {
765+
return fmt.Errorf("error getting SLO: %s", err)
766+
}
767+
768+
// Check that the SLO has the correct provenance
769+
if gotSlo.ReadOnly == nil || gotSlo.ReadOnly.Provenance == nil {
770+
return fmt.Errorf("SLO provenance is not set")
771+
}
772+
773+
if *gotSlo.ReadOnly.Provenance != "asserts" {
774+
return fmt.Errorf("expected SLO provenance to be 'asserts', got '%s'", *gotSlo.ReadOnly.Provenance)
775+
}
776+
777+
// Verify the SLO has the Asserts provenance label
778+
hasAssertsLabel := false
779+
for _, label := range gotSlo.Labels {
780+
if label.Key == "grafana_slo_provenance" && label.Value == "asserts" {
781+
hasAssertsLabel = true
782+
break
783+
}
784+
}
785+
786+
if !hasAssertsLabel {
787+
return fmt.Errorf("SLO does not have the grafana_slo_provenance=asserts label")
788+
}
789+
790+
return nil
791+
}
792+
}

0 commit comments

Comments
 (0)