Skip to content

Commit 723e379

Browse files
authored
feat: support oncall webhook presets (#2284)
1 parent ae6aa0c commit 723e379

File tree

5 files changed

+154
-45
lines changed

5 files changed

+154
-45
lines changed

docs/resources/oncall_outgoing_webhook.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ resource "grafana_oncall_outgoing_webhook" "test-acc-outgoing_webhook" {
3737
### Required
3838

3939
- `name` (String) The name of the outgoing webhook.
40-
- `url` (String) The webhook URL.
4140

4241
### Optional
4342

@@ -49,9 +48,11 @@ resource "grafana_oncall_outgoing_webhook" "test-acc-outgoing_webhook" {
4948
- `integration_filter` (List of String) Restricts the outgoing webhook to only trigger if the event came from a selected integration. If no integrations are selected the outgoing webhook will trigger for any integration.
5049
- `is_webhook_enabled` (Boolean) Controls whether the outgoing webhook will trigger or is ignored. Defaults to `true`.
5150
- `password` (String, Sensitive) The auth data of the webhook. Used for Basic authentication
51+
- `preset` (String) The preset of the outgoing webhook. Possible values are: `simple_webhook`, `advanced_webhook`, `grafana_sift`, `incident_webhook`. If no preset is set, the default preset is `advanced_webhook`.
5252
- `team_id` (String) The ID of the OnCall team (using the `grafana_oncall_team` datasource).
5353
- `trigger_template` (String) A template used to dynamically determine whether the webhook should execute based on the content of the payload.
54-
- `trigger_type` (String) The type of event that will cause this outgoing webhook to execute. The types of triggers are: `escalation`, `alert group created`, `acknowledge`, `resolve`, `silence`, `unsilence`, `unresolve`, `unacknowledge`. Defaults to `escalation`.
54+
- `trigger_type` (String) The type of event that will cause this outgoing webhook to execute. The events available will depend on the preset used. For alert group webhooks, the possible triggers are: `escalation`, `alert group created`, `status change`, `acknowledge`, `resolve`, `silence`, `unsilence`, `unresolve`, `unacknowledge`, `resolution note added`, `personal notification`; for incident webhooks: `incident declared`, `incident changed`, `incident resolved`. Defaults to `escalation`.
55+
- `url` (String) The webhook URL. Required when not using a preset that controls this field.
5556
- `user` (String) Username to use when making the outgoing webhook request.
5657

5758
### Read-Only

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ require (
88
github.com/fatih/color v1.18.0
99
github.com/go-openapi/runtime v0.28.0
1010
github.com/go-openapi/strfmt v0.23.0
11-
github.com/grafana/amixr-api-go-client v0.0.24 // main branch
11+
github.com/grafana/amixr-api-go-client v0.0.25
1212
github.com/grafana/authlib/claims v0.0.0-20250120084028-e3328c576437
1313
github.com/grafana/fleet-management-api v1.0.0
1414
github.com/grafana/grafana-app-sdk v0.35.2-0.20250408075831-c2a87bde0849

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
172172
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
173173
github.com/grafana/amixr-api-go-client v0.0.24 h1:Yvj8Ir02e3GTcetd+qHmajrLC690YJxK8lppEUkrsyA=
174174
github.com/grafana/amixr-api-go-client v0.0.24/go.mod h1:ihgLhTVimmjASuZ06y/mQxPcYH3toAIuUVGK6flHsMU=
175+
github.com/grafana/amixr-api-go-client v0.0.25 h1:tAQeJRuq9ihHotxq6/oEB6lIhuAdM+MUP0uPkNn1I3A=
176+
github.com/grafana/amixr-api-go-client v0.0.25/go.mod h1:ihgLhTVimmjASuZ06y/mQxPcYH3toAIuUVGK6flHsMU=
175177
github.com/grafana/authlib/claims v0.0.0-20250120084028-e3328c576437 h1:OlwbIVFcYgMjnQhpbZwRPVNrvZKTodvPMqwb8yEqVW0=
176178
github.com/grafana/authlib/claims v0.0.0-20250120084028-e3328c576437/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
177179
github.com/grafana/fleet-management-api v1.0.0 h1:twb0kBgeNRcvQi7iDXq7AFhlM2mWcN76kTXleJuPA38=

internal/resources/oncall/resource_outgoing_webhook.go

Lines changed: 119 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,42 @@ import (
1010
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1111
)
1212

13+
// When a preset is defined, certain fields are controlled by that preset and should not be
14+
// modified by users; these fields are automatically suppressed from diffs when a preset is active.
15+
// presetControlledFields maps preset names to lists of field names that are controlled by that preset
16+
var presetControlledFields = map[string][]string{
17+
"advanced_webhook": {},
18+
"grafana_sift": {"authorization_header", "data", "forward_whole_payload", "headers", "http_method", "password", "url", "user"},
19+
"incident_webhook": {"integration_filter"},
20+
"simple_webhook": {"authorization_header", "data", "forward_whole_payload", "headers", "http_method", "integration_filter", "password", "trigger_template", "trigger_type", "user"},
21+
}
22+
23+
// isFieldControlledByPreset checks if a field is controlled by the current preset
24+
func isFieldControlledByPreset(fieldName string, d *schema.ResourceData) bool {
25+
if preset, presetOk := d.GetOk("preset"); presetOk {
26+
if preset == "" {
27+
// If no preset is set, default to advanced_webhook
28+
preset = "advanced_webhook"
29+
}
30+
if controlledFields, exists := presetControlledFields[preset.(string)]; exists {
31+
for _, controlledField := range controlledFields {
32+
if controlledField == fieldName {
33+
return true
34+
}
35+
}
36+
}
37+
}
38+
return false
39+
}
40+
41+
// suppressDiffForPresetControlledField is a generic diff suppression function
42+
// that checks if a field is controlled by the current preset
43+
func suppressDiffForPresetControlledField(fieldName string) func(k, old, new string, d *schema.ResourceData) bool {
44+
return func(k, old, new string, d *schema.ResourceData) bool {
45+
return isFieldControlledByPreset(fieldName, d)
46+
}
47+
}
48+
1349
func resourceOutgoingWebhook() *common.Resource {
1450
schema := &schema.Resource{
1551
Description: `
@@ -29,70 +65,86 @@ func resourceOutgoingWebhook() *common.Resource {
2965
Required: true,
3066
Description: "The name of the outgoing webhook.",
3167
},
68+
"preset": {
69+
Type: schema.TypeString,
70+
Optional: true,
71+
Description: "The preset of the outgoing webhook. Possible values are: `simple_webhook`, `advanced_webhook`, `grafana_sift`, `incident_webhook`. If no preset is set, the default preset is `advanced_webhook`.",
72+
},
3273
"team_id": {
3374
Type: schema.TypeString,
3475
Optional: true,
3576
Description: "The ID of the OnCall team (using the `grafana_oncall_team` datasource).",
3677
},
3778
"url": {
38-
Type: schema.TypeString,
39-
Required: true,
40-
Description: "The webhook URL.",
79+
Type: schema.TypeString,
80+
Optional: true,
81+
Description: "The webhook URL. Required when not using a preset that controls this field.",
82+
DiffSuppressFunc: suppressDiffForPresetControlledField("url"),
4183
},
4284
"data": {
43-
Type: schema.TypeString,
44-
Optional: true,
45-
Description: "The data of the webhook.",
85+
Type: schema.TypeString,
86+
Optional: true,
87+
Description: "The data of the webhook.",
88+
DiffSuppressFunc: suppressDiffForPresetControlledField("data"),
4689
},
4790
"user": {
48-
Type: schema.TypeString,
49-
Optional: true,
50-
Description: "Username to use when making the outgoing webhook request.",
91+
Type: schema.TypeString,
92+
Optional: true,
93+
Description: "Username to use when making the outgoing webhook request.",
94+
DiffSuppressFunc: suppressDiffForPresetControlledField("user"),
5195
},
5296
"password": {
53-
Type: schema.TypeString,
54-
Optional: true,
55-
Description: "The auth data of the webhook. Used for Basic authentication",
56-
Sensitive: true,
97+
Type: schema.TypeString,
98+
Optional: true,
99+
Description: "The auth data of the webhook. Used for Basic authentication",
100+
Sensitive: true,
101+
DiffSuppressFunc: suppressDiffForPresetControlledField("password"),
57102
},
58103
"authorization_header": {
59-
Type: schema.TypeString,
60-
Optional: true,
61-
Description: "The auth data of the webhook. Used in Authorization header instead of user/password auth.",
62-
Sensitive: true,
104+
Type: schema.TypeString,
105+
Optional: true,
106+
Description: "The auth data of the webhook. Used in Authorization header instead of user/password auth.",
107+
Sensitive: true,
108+
DiffSuppressFunc: suppressDiffForPresetControlledField("authorization_header"),
63109
},
64110
"forward_whole_payload": {
65-
Type: schema.TypeBool,
66-
Optional: true,
67-
Description: "Toggle to send the entire webhook payload instead of using the values in the Data field.",
111+
Type: schema.TypeBool,
112+
Optional: true,
113+
Description: "Toggle to send the entire webhook payload instead of using the values in the Data field.",
114+
DiffSuppressFunc: suppressDiffForPresetControlledField("forward_whole_payload"),
68115
},
69116
"trigger_type": {
70-
Type: schema.TypeString,
71-
Optional: true,
72-
Description: "The type of event that will cause this outgoing webhook to execute. The types of triggers are: `escalation`, `alert group created`, `acknowledge`, `resolve`, `silence`, `unsilence`, `unresolve`, `unacknowledge`.",
73-
Default: "escalation",
117+
Type: schema.TypeString,
118+
Optional: true,
119+
Description: "The type of event that will cause this outgoing webhook to execute. The events available will depend on the preset used. For alert group webhooks, the possible triggers are: `escalation`, `alert group created`, `status change`, `acknowledge`, `resolve`, `silence`, `unsilence`, `unresolve`, `unacknowledge`, `resolution note added`, `personal notification`; for incident webhooks: `incident declared`, `incident changed`, `incident resolved`.",
120+
Default: "escalation",
121+
DiffSuppressFunc: suppressDiffForPresetControlledField("trigger_type"),
74122
},
75123
"http_method": {
76-
Type: schema.TypeString,
77-
Optional: true,
78-
Description: "The HTTP method used in the request made by the outgoing webhook.",
79-
Default: "POST",
124+
Type: schema.TypeString,
125+
Optional: true,
126+
Description: "The HTTP method used in the request made by the outgoing webhook.",
127+
Default: "POST",
128+
DiffSuppressFunc: suppressDiffForPresetControlledField("http_method"),
80129
},
81130
"trigger_template": {
82-
Type: schema.TypeString,
83-
Optional: true,
84-
Description: "A template used to dynamically determine whether the webhook should execute based on the content of the payload.",
131+
Type: schema.TypeString,
132+
Optional: true,
133+
Description: "A template used to dynamically determine whether the webhook should execute based on the content of the payload.",
134+
DiffSuppressFunc: suppressDiffForPresetControlledField("trigger_template"),
85135
},
86136
"headers": {
87-
Type: schema.TypeString,
88-
Optional: true,
89-
Description: "Headers to add to the outgoing webhook request.",
137+
Type: schema.TypeString,
138+
Optional: true,
139+
Description: "Headers to add to the outgoing webhook request.",
140+
DiffSuppressFunc: suppressDiffForPresetControlledField("headers"),
90141
},
91142
"integration_filter": {
92-
Type: schema.TypeList,
93-
Elem: &schema.Schema{Type: schema.TypeString},
94-
Optional: true,
95-
Description: "Restricts the outgoing webhook to only trigger if the event came from a selected integration. If no integrations are selected the outgoing webhook will trigger for any integration.",
143+
Type: schema.TypeList,
144+
Elem: &schema.Schema{Type: schema.TypeString},
145+
Optional: true,
146+
Description: "Restricts the outgoing webhook to only trigger if the event came from a selected integration. If no integrations are selected the outgoing webhook will trigger for any integration.",
147+
DiffSuppressFunc: suppressDiffForPresetControlledField("integration_filter"),
96148
},
97149
"is_webhook_enabled": {
98150
Type: schema.TypeBool,
@@ -127,18 +179,30 @@ func listWebhooks(client *onCallAPI.Client, listOptions onCallAPI.ListOptions) (
127179
func resourceOutgoingWebhookCreate(ctx context.Context, d *schema.ResourceData, client *onCallAPI.Client) diag.Diagnostics {
128180
name := d.Get("name").(string)
129181
teamID := d.Get("team_id").(string)
130-
url := d.Get("url").(string)
131182
forwardWholePayload := d.Get("forward_whole_payload").(bool)
132183
isWebhookEnabled := d.Get("is_webhook_enabled").(bool)
133184

134185
createOptions := &onCallAPI.CreateWebhookOptions{
135186
Name: name,
136187
Team: teamID,
137-
Url: url,
138188
ForwardAll: forwardWholePayload,
139189
IsWebhookEnabled: isWebhookEnabled,
140190
}
141191

192+
preset, presetOk := d.GetOk("preset")
193+
if presetOk {
194+
createOptions.Preset = preset.(string)
195+
}
196+
197+
// Handle URL validation and assignment
198+
if !isFieldControlledByPreset("url", d) {
199+
url, urlOk := d.GetOk("url")
200+
if !urlOk || url == "" {
201+
return diag.Errorf("url is required if it is not defined by the preset")
202+
}
203+
createOptions.Url = url.(string)
204+
}
205+
142206
data, dataOk := d.GetOk("data")
143207
if dataOk {
144208
dd := data.(string)
@@ -213,6 +277,7 @@ func resourceOutgoingWebhookRead(ctx context.Context, d *schema.ResourceData, cl
213277
}
214278

215279
d.Set("name", outgoingWebhook.Name)
280+
d.Set("preset", outgoingWebhook.Preset)
216281
d.Set("team_id", outgoingWebhook.Team)
217282
d.Set("url", outgoingWebhook.Url)
218283
d.Set("data", outgoingWebhook.Data)
@@ -231,18 +296,30 @@ func resourceOutgoingWebhookRead(ctx context.Context, d *schema.ResourceData, cl
231296
func resourceOutgoingWebhookUpdate(ctx context.Context, d *schema.ResourceData, client *onCallAPI.Client) diag.Diagnostics {
232297
name := d.Get("name").(string)
233298
teamID := d.Get("team_id").(string)
234-
url := d.Get("url").(string)
235299
forwardWholePayload := d.Get("forward_whole_payload").(bool)
236300
isWebhookEnabled := d.Get("is_webhook_enabled").(bool)
237301

238302
updateOptions := &onCallAPI.UpdateWebhookOptions{
239303
Name: name,
240304
Team: teamID,
241-
Url: url,
242305
ForwardAll: forwardWholePayload,
243306
IsWebhookEnabled: isWebhookEnabled,
244307
}
245308

309+
preset, presetOk := d.GetOk("preset")
310+
if presetOk {
311+
updateOptions.Preset = preset.(string)
312+
}
313+
314+
// Handle URL validation and assignment
315+
if !isFieldControlledByPreset("url", d) {
316+
url, urlOk := d.GetOk("url")
317+
if !urlOk || url == "" {
318+
return diag.Errorf("url is required if it is not defined by the preset")
319+
}
320+
updateOptions.Url = url.(string)
321+
}
322+
246323
data, dataOk := d.GetOk("data")
247324
if dataOk {
248325
dd := data.(string)

internal/resources/oncall/resource_outgoing_webhook_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,25 @@ func TestAccOnCallOutgoingWebhook_basic(t *testing.T) {
3131
})
3232
}
3333

34+
func TestAccOnCallOutgoingWebhook_preset(t *testing.T) {
35+
testutils.CheckCloudInstanceTestsEnabled(t)
36+
37+
webhookName := fmt.Sprintf("name-%s", acctest.RandString(8))
38+
39+
resource.ParallelTest(t, resource.TestCase{
40+
ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
41+
CheckDestroy: testAccCheckOnCallOutgoingWebhookResourceDestroy,
42+
Steps: []resource.TestStep{
43+
{
44+
Config: testAccOnCallOutgoingWebhookPresetConfig(webhookName),
45+
Check: resource.ComposeTestCheckFunc(
46+
testAccCheckOnCallOutgoingWebhookResourceExists("grafana_oncall_outgoing_webhook.test-acc-outgoing_webhook"),
47+
),
48+
},
49+
},
50+
})
51+
}
52+
3453
func testAccCheckOnCallOutgoingWebhookResourceDestroy(s *terraform.State) error {
3554
client := testutils.Provider.Meta().(*common.Client).OnCallClient
3655
for _, r := range s.RootModule().Resources {
@@ -65,6 +84,16 @@ resource "grafana_oncall_outgoing_webhook" "test-acc-outgoing_webhook" {
6584
`, webhookName)
6685
}
6786

87+
func testAccOnCallOutgoingWebhookPresetConfig(webhookName string) string {
88+
return fmt.Sprintf(`
89+
resource "grafana_oncall_outgoing_webhook" "test-acc-outgoing_webhook" {
90+
name = "%s"
91+
preset = "simple_webhook"
92+
url = "https://example.com"
93+
}
94+
`, webhookName)
95+
}
96+
6897
func testAccCheckOnCallOutgoingWebhookResourceExists(name string) resource.TestCheckFunc {
6998
return func(s *terraform.State) error {
7099
rs, ok := s.RootModule().Resources[name]

0 commit comments

Comments
 (0)