Skip to content

Commit 7cd039c

Browse files
Alerting Rule Groups: Support editing from UI (#1214)
* Alerting Rule Groups: Support editing from UI Closes #697 Requires grafana/grafana-openapi-client-go#45 There has been informal support through setting the `X-Disable-Provenance` header manually but it doesn't work that well. Notably, it currently fails on updates (see issue) This adds a field on the rule group to allow editing from UI Note: The provenance is actually a rule attribute (not group). Because we manage the rule group as a whole and the setting has to set through a header, I chose to add the setting as a group-scoped attribute because it's easier to manage Otherwise, we'd have to do POSTs, PUTs and DELETEs for each rule within the group Also, I added tests to make sure that the provenance is correctly synced from Terraform to the alerting service and that it can be read back * Remove commented out code * Update ref * Rename `allow_editing_from_ui` -> `disable_provenance` * Update client ref
1 parent 3db0477 commit 7cd039c

File tree

5 files changed

+132
-65
lines changed

5 files changed

+132
-65
lines changed

docs/resources/rule_group.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ EOT
120120

121121
### Optional
122122

123+
- `disable_provenance` (Boolean) Allow modifying the rule group from other sources than Terraform or the Grafana API. Defaults to `false`.
123124
- `org_id` (String) The Organization ID. If not set, the Org ID defined in the provider block will be used.
124125

125126
### Read-Only

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ require (
1010
github.com/go-openapi/strfmt v0.21.9
1111
github.com/grafana/amixr-api-go-client v0.0.11
1212
github.com/grafana/grafana-api-golang-client v0.27.0
13-
github.com/grafana/grafana-openapi-client-go v0.0.0-20231208125730-b6492d2ae05f
13+
github.com/grafana/grafana-openapi-client-go v0.0.0-20231215124113-30c79ed880b9
1414
github.com/grafana/machine-learning-go-client v0.5.0
1515
github.com/grafana/synthetic-monitoring-agent v0.19.1
1616
github.com/grafana/synthetic-monitoring-api-go-client v0.7.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ github.com/grafana/amixr-api-go-client v0.0.11 h1:jlE+5t0tRuCtjbpM81j70Dr2J4eCyS
116116
github.com/grafana/amixr-api-go-client v0.0.11/go.mod h1:N6x26XUrM5zGtK5zL5vNJnAn2JFMxLFPPLTw/6pDkFE=
117117
github.com/grafana/grafana-api-golang-client v0.27.0 h1:zIwMXcbCB4n588i3O2N6HfNcQogCNTd/vPkEXTr7zX8=
118118
github.com/grafana/grafana-api-golang-client v0.27.0/go.mod h1:uNLZEmgKtTjHBtCQMwNn3qsx2mpMb8zU+7T4Xv3NR9Y=
119-
github.com/grafana/grafana-openapi-client-go v0.0.0-20231208125730-b6492d2ae05f h1:BYEmWlwwy+f8kI1nB4orGxvFQV1P2zXU+M/Xy8y/D/0=
120-
github.com/grafana/grafana-openapi-client-go v0.0.0-20231208125730-b6492d2ae05f/go.mod h1:LwkzHzVOQG/fFIZmPvLxU2SrtLyxx+YAkx6ykw5sTfQ=
119+
github.com/grafana/grafana-openapi-client-go v0.0.0-20231215124113-30c79ed880b9 h1:yRvxeTz934brAilk3YaRK0f43Q079rBMU6t81ereQkI=
120+
github.com/grafana/grafana-openapi-client-go v0.0.0-20231215124113-30c79ed880b9/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ=
121121
github.com/grafana/machine-learning-go-client v0.5.0 h1:Q1K+MPSy8vfMm2jsk3WQ7O77cGr2fM5hxwtPSoPc5NU=
122122
github.com/grafana/machine-learning-go-client v0.5.0/go.mod h1:QFfZz8NkqVF8++skjkKQXJEZfpCYd8S0yTWJUpsLLTA=
123123
github.com/grafana/synthetic-monitoring-agent v0.19.1 h1:ImH6JG8ZJ1h+KP7lJV6nkYyImAXtEthMaoLRpP4Hd0M=

internal/resources/grafana/resource_alerting_rule_group.go

Lines changed: 60 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ This resource requires Grafana 9.1.0 or later.
5757
Required: true,
5858
Description: "The interval, in seconds, at which all rules in the group are evaluated. If a group contains many rules, the rules are evaluated sequentially.",
5959
},
60+
"disable_provenance": {
61+
Type: schema.TypeBool,
62+
Optional: true,
63+
Default: false,
64+
Description: "Allow modifying the rule group from other sources than Terraform or the Grafana API.",
65+
},
6066
"rule": {
6167
Type: schema.TypeList,
6268
Required: true,
@@ -198,9 +204,30 @@ func readAlertRuleGroup(ctx context.Context, data *schema.ResourceData, meta int
198204
return err
199205
}
200206

201-
if err := packRuleGroup(resp.Payload, data); err != nil {
202-
return diag.FromErr(err)
207+
g := resp.Payload
208+
data.Set("name", g.Title)
209+
data.Set("folder_uid", g.FolderUID)
210+
data.Set("interval_seconds", g.Interval)
211+
disableProvenance := true
212+
rules := make([]interface{}, 0, len(g.Rules))
213+
for _, r := range g.Rules {
214+
ruleResp, err := client.Provisioning.GetAlertRule(r.UID) // We need to get the rule through a separate API call to get the provenance.
215+
if err != nil {
216+
return diag.FromErr(err)
217+
}
218+
r := ruleResp.Payload
219+
data.Set("org_id", strconv.FormatInt(*r.OrgID, 10))
220+
packed, err := packAlertRule(r)
221+
if err != nil {
222+
return diag.FromErr(err)
223+
}
224+
if r.Provenance != "" {
225+
disableProvenance = false
226+
}
227+
rules = append(rules, packed)
203228
}
229+
data.Set("disable_provenance", disableProvenance)
230+
data.Set("rule", rules)
204231
data.SetId(MakeOrgResourceID(orgID, packGroupID(key)))
205232

206233
return nil
@@ -209,16 +236,40 @@ func readAlertRuleGroup(ctx context.Context, data *schema.ResourceData, meta int
209236
func putAlertRuleGroup(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
210237
client, orgID := OAPIClientFromNewOrgResource(meta, data)
211238

212-
group, err := unpackRuleGroup(data)
213-
if err != nil {
214-
return diag.FromErr(err)
239+
group := data.Get("name").(string)
240+
folder := data.Get("folder_uid").(string)
241+
interval := data.Get("interval_seconds").(int)
242+
243+
packedRules := data.Get("rule").([]interface{})
244+
rules := make([]*models.ProvisionedAlertRule, 0, len(packedRules))
245+
for i := range packedRules {
246+
rule, err := unpackAlertRule(packedRules[i], group, folder, orgID)
247+
if err != nil {
248+
return diag.FromErr(err)
249+
}
250+
rules = append(rules, rule)
251+
}
252+
253+
putParams := provisioning.NewPutAlertRuleGroupParams().
254+
WithFolderUID(folder).
255+
WithGroup(group).WithBody(&models.AlertRuleGroup{
256+
Title: group,
257+
FolderUID: folder,
258+
Rules: rules,
259+
Interval: int64(interval),
260+
})
261+
262+
if data.Get("disable_provenance").(bool) {
263+
disableProvenance := "disabled" // This can be any non-empty string.
264+
putParams.SetXDisableProvenance(&disableProvenance)
215265
}
216266

217-
if _, err = client.Provisioning.PutAlertRuleGroup(group); err != nil {
267+
resp, err := client.Provisioning.PutAlertRuleGroup(putParams)
268+
if err != nil {
218269
return diag.FromErr(err)
219270
}
220271

221-
key := packGroupID(AlertRuleGroupKey{group.FolderUID, group.Group})
272+
key := packGroupID(AlertRuleGroupKey{resp.Payload.FolderUID, resp.Payload.Title})
222273
data.SetId(MakeOrgResourceID(orgID, key))
223274
return readAlertRuleGroup(ctx, data, meta)
224275
}
@@ -235,7 +286,8 @@ func deleteAlertRuleGroup(ctx context.Context, data *schema.ResourceData, meta i
235286
group := resp.Payload
236287

237288
for _, r := range group.Rules {
238-
_, err := client.Provisioning.DeleteAlertRule(r.UID)
289+
params := provisioning.NewDeleteAlertRuleParams().WithUID(r.UID)
290+
_, err := client.Provisioning.DeleteAlertRule(params)
239291
if diag, shouldReturn := common.CheckReadError("rule group", data, err); shouldReturn {
240292
return diag
241293
}
@@ -257,55 +309,6 @@ func diffSuppressJSON(k, oldValue, newValue string, data *schema.ResourceData) b
257309
return reflect.DeepEqual(o, n)
258310
}
259311

260-
func packRuleGroup(g *models.AlertRuleGroup, data *schema.ResourceData) error {
261-
data.Set("name", g.Title)
262-
data.Set("folder_uid", g.FolderUID)
263-
data.Set("interval_seconds", g.Interval)
264-
rules := make([]interface{}, 0, len(g.Rules))
265-
for _, r := range g.Rules {
266-
data.Set("org_id", strconv.FormatInt(*r.OrgID, 10))
267-
packed, err := packAlertRule(r)
268-
if err != nil {
269-
return err
270-
}
271-
rules = append(rules, packed)
272-
}
273-
data.Set("rule", rules)
274-
return nil
275-
}
276-
277-
func unpackRuleGroup(data *schema.ResourceData) (*provisioning.PutAlertRuleGroupParams, error) {
278-
group := data.Get("name").(string)
279-
folder := data.Get("folder_uid").(string)
280-
interval := data.Get("interval_seconds").(int)
281-
packedRules := data.Get("rule").([]interface{})
282-
283-
// org_id is a string to properly support referencing between resources. However, the API expects an int64.
284-
orgID, err := strconv.ParseInt(data.Get("org_id").(string), 10, 64)
285-
if err != nil {
286-
return nil, err
287-
}
288-
289-
rules := make([]*models.ProvisionedAlertRule, 0, len(packedRules))
290-
for i := range packedRules {
291-
rule, err := unpackAlertRule(packedRules[i], group, folder, orgID)
292-
if err != nil {
293-
return nil, err
294-
}
295-
rules = append(rules, rule)
296-
}
297-
298-
return provisioning.NewPutAlertRuleGroupParams().
299-
WithFolderUID(folder).
300-
WithGroup(group).WithBody(&models.AlertRuleGroup{
301-
302-
Title: group,
303-
FolderUID: folder,
304-
Rules: rules,
305-
Interval: int64(interval),
306-
}), nil
307-
}
308-
309312
func packAlertRule(r *models.ProvisionedAlertRule) (interface{}, error) {
310313
data, err := packRuleData(r.Data)
311314
if err != nil {

internal/resources/grafana/resource_alerting_rule_group_test.go

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ func TestAccAlertRule_inOrg(t *testing.T) {
226226
Steps: []resource.TestStep{
227227
// Test creation.
228228
{
229-
Config: testAccAlertRuleGroupInOrgConfig(name, 240),
229+
Config: testAccAlertRuleGroupInOrgConfig(name, 240, false),
230230
Check: resource.ComposeTestCheckFunc(
231231
alertingRuleGroupCheckExists.exists("grafana_rule_group.test", &group),
232232
orgCheckExists.exists("grafana_organization.test", &org),
@@ -239,7 +239,7 @@ func TestAccAlertRule_inOrg(t *testing.T) {
239239
},
240240
// Test update content.
241241
{
242-
Config: testAccAlertRuleGroupInOrgConfig(name, 360),
242+
Config: testAccAlertRuleGroupInOrgConfig(name, 360, false),
243243
Check: resource.ComposeTestCheckFunc(
244244
alertingRuleGroupCheckExists.exists("grafana_rule_group.test", &group),
245245
orgCheckExists.exists("grafana_organization.test", &org),
@@ -258,7 +258,7 @@ func TestAccAlertRule_inOrg(t *testing.T) {
258258
},
259259
// Test delete resource, but not org.
260260
{
261-
Config: testutils.WithoutResource(t, testAccAlertRuleGroupInOrgConfig(name, 360), "grafana_rule_group.test"),
261+
Config: testutils.WithoutResource(t, testAccAlertRuleGroupInOrgConfig(name, 360, false), "grafana_rule_group.test"),
262262
Check: resource.ComposeTestCheckFunc(
263263
orgCheckExists.exists("grafana_organization.test", &org),
264264
alertingRuleGroupCheckExists.destroyed(&group, &org),
@@ -268,7 +268,69 @@ func TestAccAlertRule_inOrg(t *testing.T) {
268268
})
269269
}
270270

271-
func testAccAlertRuleGroupInOrgConfig(name string, interval int) string {
271+
func TestAccAlertRule_disableProvenance(t *testing.T) {
272+
testutils.CheckOSSTestsEnabled(t, ">=9.1.0")
273+
274+
var group models.AlertRuleGroup
275+
var org models.OrgDetailsDTO
276+
name := acctest.RandString(10)
277+
278+
resource.ParallelTest(t, resource.TestCase{
279+
ProviderFactories: testutils.ProviderFactories,
280+
CheckDestroy: resource.ComposeTestCheckFunc(
281+
orgCheckExists.destroyed(&org, nil),
282+
alertingRuleGroupCheckExists.destroyed(&group, &org),
283+
),
284+
Steps: []resource.TestStep{
285+
{
286+
Config: testAccAlertRuleGroupInOrgConfig(name, 240, false),
287+
Check: resource.ComposeTestCheckFunc(
288+
alertingRuleGroupCheckExists.exists("grafana_rule_group.test", &group),
289+
orgCheckExists.exists("grafana_organization.test", &org),
290+
checkResourceIsInOrg("grafana_rule_group.test", "grafana_organization.test"),
291+
resource.TestCheckResourceAttr("grafana_rule_group.test", "name", name),
292+
resource.TestCheckResourceAttr("grafana_rule_group.test", "disable_provenance", "false"),
293+
),
294+
},
295+
// Test import.
296+
{
297+
ResourceName: "grafana_rule_group.test",
298+
ImportState: true,
299+
ImportStateVerify: true,
300+
},
301+
// Enable editing from UI.
302+
{
303+
Config: testAccAlertRuleGroupInOrgConfig(name, 240, true),
304+
Check: resource.ComposeTestCheckFunc(
305+
alertingRuleGroupCheckExists.exists("grafana_rule_group.test", &group),
306+
orgCheckExists.exists("grafana_organization.test", &org),
307+
checkResourceIsInOrg("grafana_rule_group.test", "grafana_organization.test"),
308+
resource.TestCheckResourceAttr("grafana_rule_group.test", "name", name),
309+
resource.TestCheckResourceAttr("grafana_rule_group.test", "disable_provenance", "true"),
310+
),
311+
},
312+
// Test import.
313+
{
314+
ResourceName: "grafana_rule_group.test",
315+
ImportState: true,
316+
ImportStateVerify: true,
317+
},
318+
// Disable editing from UI.
319+
{
320+
Config: testAccAlertRuleGroupInOrgConfig(name, 240, false),
321+
Check: resource.ComposeTestCheckFunc(
322+
alertingRuleGroupCheckExists.exists("grafana_rule_group.test", &group),
323+
orgCheckExists.exists("grafana_organization.test", &org),
324+
checkResourceIsInOrg("grafana_rule_group.test", "grafana_organization.test"),
325+
resource.TestCheckResourceAttr("grafana_rule_group.test", "name", name),
326+
resource.TestCheckResourceAttr("grafana_rule_group.test", "disable_provenance", "false"),
327+
),
328+
},
329+
},
330+
})
331+
}
332+
333+
func testAccAlertRuleGroupInOrgConfig(name string, interval int, disableProvenance bool) string {
272334
return fmt.Sprintf(`
273335
resource "grafana_organization" "test" {
274336
name = "%[1]s"
@@ -284,6 +346,7 @@ resource "grafana_rule_group" "test" {
284346
name = "%[1]s"
285347
folder_uid = grafana_folder.test.uid
286348
interval_seconds = %[2]d
349+
disable_provenance = %[3]t
287350
rule {
288351
name = "My Alert Rule 1"
289352
for = "2m"
@@ -308,5 +371,5 @@ resource "grafana_rule_group" "test" {
308371
}
309372
}
310373
}
311-
`, name, interval)
374+
`, name, interval, disableProvenance)
312375
}

0 commit comments

Comments
 (0)