Skip to content

Commit 572abdb

Browse files
sidharth-chauhanstephan-rayner
authored andcommitted
fix(oncall): make labels/dynamic_labels order-insensitive (#2169)
1 parent e77ff3e commit 572abdb

File tree

3 files changed

+159
-50
lines changed

3 files changed

+159
-50
lines changed

docs/resources/oncall_integration.md

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ description: |-
88

99
# grafana_oncall_integration (Resource)
1010

11-
* [Official documentation](https://grafana.com/docs/oncall/latest/configure/integrations/)
12-
* [HTTP API](https://grafana.com/docs/oncall/latest/oncall-api-reference/)
11+
- [Official documentation](https://grafana.com/docs/oncall/latest/configure/integrations/)
12+
- [HTTP API](https://grafana.com/docs/oncall/latest/oncall-api-reference/)
1313

1414
## Example Usage
1515

@@ -59,7 +59,7 @@ resource "grafana_oncall_integration" "integration_with_templates" {
5959
}
6060
}
6161
62-
# You can add static labels and dynamic labels to an integration with 'labels' and 'dynamic_labels
62+
# You can add static labels and dynamic labels to an integration with 'labels' and 'dynamic_labels'
6363
# using the 'grafana_oncall_label' datasource
6464
data "grafana_oncall_label" "test-label" {
6565
provider = grafana.oncall
@@ -78,12 +78,25 @@ resource "grafana_oncall_integration" "test-acc-integration" {
7878
name = "my integration"
7979
type = "webhook"
8080
default_route {}
81-
labels = [data.grafana_oncall_label.test-label]
82-
dynamic_labels = [data.grafana_oncall_label.test-dynamic-label]
81+
82+
labels = [
83+
{
84+
key = data.grafana_oncall_label.test-label.key
85+
value = data.grafana_oncall_label.test-label.value
86+
}
87+
]
88+
89+
dynamic_labels = [
90+
{
91+
key = data.grafana_oncall_label.test-dynamic-label.key
92+
value = data.grafana_oncall_label.test-dynamic-label.value
93+
}
94+
]
8395
}
8496
```
8597

8698
<!-- schema generated by tfplugindocs -->
99+
87100
## Schema
88101

89102
### Required
@@ -105,6 +118,7 @@ resource "grafana_oncall_integration" "test-acc-integration" {
105118
- `link` (String) The link for using in an integrated tool.
106119

107120
<a id="nestedblock--default_route"></a>
121+
108122
### Nested Schema for `default_route`
109123

110124
Optional:
@@ -119,34 +133,34 @@ Read-Only:
119133
- `id` (String)
120134

121135
<a id="nestedblock--default_route--msteams"></a>
136+
122137
### Nested Schema for `default_route.msteams`
123138

124139
Optional:
125140

126141
- `enabled` (Boolean) Enable notification in MS teams. Defaults to `true`.
127142
- `id` (String) MS teams channel id. Alerts will be directed to this channel in Microsoft teams.
128143

129-
130144
<a id="nestedblock--default_route--slack"></a>
145+
131146
### Nested Schema for `default_route.slack`
132147

133148
Optional:
134149

135150
- `channel_id` (String) Slack channel id. Alerts will be directed to this channel in Slack.
136151
- `enabled` (Boolean) Enable notification in Slack. Defaults to `true`.
137152

138-
139153
<a id="nestedblock--default_route--telegram"></a>
154+
140155
### Nested Schema for `default_route.telegram`
141156

142157
Optional:
143158

144159
- `enabled` (Boolean) Enable notification in Telegram. Defaults to `true`.
145160
- `id` (String) Telegram channel id. Alerts will be directed to this channel in Telegram.
146161

147-
148-
149162
<a id="nestedblock--templates"></a>
163+
150164
### Nested Schema for `templates`
151165

152166
Optional:
@@ -165,15 +179,16 @@ Optional:
165179
- `web` (Block List, Max: 1) Templates for Web. (see [below for nested schema](#nestedblock--templates--web))
166180

167181
<a id="nestedblock--templates--email"></a>
182+
168183
### Nested Schema for `templates.email`
169184

170185
Optional:
171186

172187
- `message` (String) Template for Alert message.
173188
- `title` (String) Template for Alert title.
174189

175-
176190
<a id="nestedblock--templates--microsoft_teams"></a>
191+
177192
### Nested Schema for `templates.microsoft_teams`
178193

179194
Optional:
@@ -182,25 +197,25 @@ Optional:
182197
- `message` (String) Template for Alert message.
183198
- `title` (String) Template for Alert title.
184199

185-
186200
<a id="nestedblock--templates--mobile_app"></a>
201+
187202
### Nested Schema for `templates.mobile_app`
188203

189204
Optional:
190205

191206
- `message` (String) Template for Alert message.
192207
- `title` (String) Template for Alert title.
193208

194-
195209
<a id="nestedblock--templates--phone_call"></a>
210+
196211
### Nested Schema for `templates.phone_call`
197212

198213
Optional:
199214

200215
- `title` (String) Template for Alert title.
201216

202-
203217
<a id="nestedblock--templates--slack"></a>
218+
204219
### Nested Schema for `templates.slack`
205220

206221
Optional:
@@ -209,16 +224,16 @@ Optional:
209224
- `message` (String) Template for Alert message.
210225
- `title` (String) Template for Alert title.
211226

212-
213227
<a id="nestedblock--templates--sms"></a>
228+
214229
### Nested Schema for `templates.sms`
215230

216231
Optional:
217232

218233
- `title` (String) Template for Alert title.
219234

220-
221235
<a id="nestedblock--templates--telegram"></a>
236+
222237
### Nested Schema for `templates.telegram`
223238

224239
Optional:
@@ -227,8 +242,8 @@ Optional:
227242
- `message` (String) Template for Alert message.
228243
- `title` (String) Template for Alert title.
229244

230-
231245
<a id="nestedblock--templates--web"></a>
246+
232247
### Nested Schema for `templates.web`
233248

234249
Optional:

internal/resources/oncall/resource_integration.go

Lines changed: 114 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"net/http"
7+
"sort"
78
"strings"
89

910
onCallAPI "github.com/grafana/amixr-api-go-client"
@@ -234,26 +235,50 @@ func resourceIntegration() *common.Resource {
234235
},
235236
},
236237
"labels": {
237-
Type: schema.TypeList,
238-
Elem: &schema.Schema{
239-
Type: schema.TypeMap,
240-
Elem: &schema.Schema{
241-
Type: schema.TypeString,
238+
Type: schema.TypeSet,
239+
Optional: true,
240+
Elem: &schema.Resource{
241+
Schema: map[string]*schema.Schema{
242+
"key": {
243+
Type: schema.TypeString,
244+
Required: true,
245+
},
246+
"value": {
247+
Type: schema.TypeString,
248+
Required: true,
249+
},
242250
},
243251
},
244-
Optional: true,
245-
Description: "A list of string-to-string mappings for static labels. Each map must include one key named \"key\" and one key named \"value\" (using the `grafana_oncall_label` datasource).",
252+
Set: schema.HashResource(&schema.Resource{
253+
Schema: map[string]*schema.Schema{
254+
"key": {Type: schema.TypeString},
255+
"value": {Type: schema.TypeString},
256+
},
257+
}),
258+
Description: "A set of static labels. Each item must include a \"key\" and \"value\" (using the `grafana_oncall_label` datasource).",
246259
},
247260
"dynamic_labels": {
248-
Type: schema.TypeList,
249-
Elem: &schema.Schema{
250-
Type: schema.TypeMap,
251-
Elem: &schema.Schema{
252-
Type: schema.TypeString,
261+
Type: schema.TypeSet,
262+
Optional: true,
263+
Elem: &schema.Resource{
264+
Schema: map[string]*schema.Schema{
265+
"key": {
266+
Type: schema.TypeString,
267+
Required: true,
268+
},
269+
"value": {
270+
Type: schema.TypeString,
271+
Required: true,
272+
},
253273
},
254274
},
255-
Optional: true,
256-
Description: "A list of string-to-string mappings for dynamic labels. Each map must include one key named \"key\" and one key named \"value\" (using the `grafana_oncall_label` datasource).",
275+
Set: schema.HashResource(&schema.Resource{
276+
Schema: map[string]*schema.Schema{
277+
"key": {Type: schema.TypeString},
278+
"value": {Type: schema.TypeString},
279+
},
280+
}),
281+
Description: "A set of dynamic labels. Each item must include a \"key\" and \"value\" (using the `grafana_oncall_label` datasource).",
257282
},
258283
},
259284
}
@@ -321,10 +346,35 @@ func resourceIntegrationCreate(ctx context.Context, d *schema.ResourceData, clie
321346
teamIDData := d.Get("team_id").(string)
322347
nameData := d.Get("name").(string)
323348
typeData := d.Get("type").(string)
324-
templatesData := d.Get("templates").([]any)
349+
350+
var templatesData []any
351+
switch v := d.Get("templates").(type) {
352+
case []any:
353+
templatesData = v
354+
default:
355+
templatesData = []any{}
356+
}
325357
defaultRouteData := d.Get("default_route").([]any)
326-
labelsData := d.Get("labels").([]any)
327-
dynamicLabelsData := d.Get("dynamic_labels").([]any)
358+
359+
var labelsData []any
360+
switch v := d.Get("labels").(type) {
361+
case *schema.Set:
362+
labelsData = v.List()
363+
case []any:
364+
labelsData = v
365+
default:
366+
labelsData = []any{}
367+
}
368+
369+
var dynamicLabelsData []any
370+
switch v := d.Get("dynamic_labels").(type) {
371+
case *schema.Set:
372+
dynamicLabelsData = v.List()
373+
case []any:
374+
dynamicLabelsData = v
375+
default:
376+
dynamicLabelsData = []any{}
377+
}
328378

329379
createOptions := &onCallAPI.CreateIntegrationOptions{
330380
TeamId: teamIDData,
@@ -349,15 +399,40 @@ func resourceIntegrationCreate(ctx context.Context, d *schema.ResourceData, clie
349399
func resourceIntegrationUpdate(ctx context.Context, d *schema.ResourceData, client *onCallAPI.Client) diag.Diagnostics {
350400
nameData := d.Get("name").(string)
351401
teamIDData := d.Get("team_id").(string)
352-
templateData := d.Get("templates").([]any)
402+
403+
var templatesData []any
404+
switch v := d.Get("templates").(type) {
405+
case []any:
406+
templatesData = v
407+
default:
408+
templatesData = []any{}
409+
}
353410
defaultRouteData := d.Get("default_route").([]any)
354-
labelsData := d.Get("labels").([]any)
355-
dynamicLabelsData := d.Get("dynamic_labels").([]any)
411+
412+
var labelsData []any
413+
switch v := d.Get("labels").(type) {
414+
case *schema.Set:
415+
labelsData = v.List()
416+
case []any:
417+
labelsData = v
418+
default:
419+
labelsData = []any{}
420+
}
421+
422+
var dynamicLabelsData []any
423+
switch v := d.Get("dynamic_labels").(type) {
424+
case *schema.Set:
425+
dynamicLabelsData = v.List()
426+
case []any:
427+
dynamicLabelsData = v
428+
default:
429+
dynamicLabelsData = []any{}
430+
}
356431

357432
updateOptions := &onCallAPI.UpdateIntegrationOptions{
358433
Name: nameData,
359434
TeamId: teamIDData,
360-
Templates: expandTemplates(templateData),
435+
Templates: expandTemplates(templatesData),
361436
DefaultRoute: expandDefaultRoute(defaultRouteData),
362437
Labels: expandLabels(labelsData),
363438
DynamicLabels: expandLabels(dynamicLabelsData),
@@ -491,6 +566,9 @@ func expandRouteMSTeams(in []any) *onCallAPI.MSTeamsRoute {
491566
}
492567

493568
func flattenTemplates(in *onCallAPI.Templates) []map[string]any {
569+
if in == nil {
570+
return []map[string]any{}
571+
}
494572
templates := make([]map[string]any, 0, 1)
495573
out := make(map[string]any)
496574
add := false
@@ -779,15 +857,15 @@ func flattenDefaultRoute(in *onCallAPI.DefaultRoute, d *schema.ResourceData) []m
779857
out["escalation_chain_id"] = in.EscalationChainId
780858
// Set messengers data only if related fields are present
781859
_, slackOk := d.GetOk("default_route.0.slack")
782-
if slackOk {
860+
if slackOk && in.SlackRoute != nil {
783861
out["slack"] = flattenRouteSlack(in.SlackRoute)
784862
}
785863
_, telegramOk := d.GetOk("default_route.0.telegram")
786-
if telegramOk {
864+
if telegramOk && in.TelegramRoute != nil {
787865
out["telegram"] = flattenRouteTelegram(in.TelegramRoute)
788866
}
789867
_, msteamsOk := d.GetOk("default_route.0.msteams")
790-
if msteamsOk {
868+
if msteamsOk && in.MSTeamsRoute != nil {
791869
out["msteams"] = flattenRouteMSTeams(in.MSTeamsRoute)
792870
}
793871

@@ -800,8 +878,9 @@ func expandDefaultRoute(input []any) *onCallAPI.DefaultRoute {
800878

801879
for _, r := range input {
802880
inputMap := r.(map[string]any)
803-
id := inputMap["id"].(string)
804-
defaultRoute.ID = id
881+
if v, ok := inputMap["id"].(string); ok && v != "" {
882+
defaultRoute.ID = v
883+
}
805884
if inputMap["escalation_chain_id"] != "" {
806885
escalationChainID := inputMap["escalation_chain_id"].(string)
807886
defaultRoute.EscalationChainId = &escalationChainID
@@ -845,15 +924,22 @@ func expandLabels(input []any) []*onCallAPI.Label {
845924
}
846925

847926
func flattenLabels(labels []*onCallAPI.Label) []map[string]string {
848-
flattenedLabels := make([]map[string]string, 0, 1)
927+
flattenedLabels := make([]map[string]string, 0, len(labels))
849928

850929
for _, l := range labels {
851930
flattenedLabels = append(flattenedLabels, map[string]string{
852-
"id": l.Key.Name,
853931
"key": l.Key.Name,
854932
"value": l.Value.Name,
855933
})
856934
}
857935

936+
// deterministic ordering to avoid permadiffs when API returns labels in arbitrary order
937+
sort.Slice(flattenedLabels, func(i, j int) bool {
938+
if flattenedLabels[i]["key"] == flattenedLabels[j]["key"] {
939+
return flattenedLabels[i]["value"] < flattenedLabels[j]["value"]
940+
}
941+
return flattenedLabels[i]["key"] < flattenedLabels[j]["key"]
942+
})
943+
858944
return flattenedLabels
859945
}

0 commit comments

Comments
 (0)