diff --git a/docs/resources/oncall_integration.md b/docs/resources/oncall_integration.md
index c2dac8019..88d73f3ad 100644
--- a/docs/resources/oncall_integration.md
+++ b/docs/resources/oncall_integration.md
@@ -8,8 +8,8 @@ description: |-
# grafana_oncall_integration (Resource)
-* [Official documentation](https://grafana.com/docs/oncall/latest/configure/integrations/)
-* [HTTP API](https://grafana.com/docs/oncall/latest/oncall-api-reference/)
+- [Official documentation](https://grafana.com/docs/oncall/latest/configure/integrations/)
+- [HTTP API](https://grafana.com/docs/oncall/latest/oncall-api-reference/)
## Example Usage
@@ -59,7 +59,7 @@ resource "grafana_oncall_integration" "integration_with_templates" {
}
}
-# You can add static labels and dynamic labels to an integration with 'labels' and 'dynamic_labels
+# You can add static labels and dynamic labels to an integration with 'labels' and 'dynamic_labels'
# using the 'grafana_oncall_label' datasource
data "grafana_oncall_label" "test-label" {
provider = grafana.oncall
@@ -78,12 +78,25 @@ resource "grafana_oncall_integration" "test-acc-integration" {
name = "my integration"
type = "webhook"
default_route {}
- labels = [data.grafana_oncall_label.test-label]
- dynamic_labels = [data.grafana_oncall_label.test-dynamic-label]
+
+ labels = [
+ {
+ key = data.grafana_oncall_label.test-label.key
+ value = data.grafana_oncall_label.test-label.value
+ }
+ ]
+
+ dynamic_labels = [
+ {
+ key = data.grafana_oncall_label.test-dynamic-label.key
+ value = data.grafana_oncall_label.test-dynamic-label.value
+ }
+ ]
}
```
+
## Schema
### Required
@@ -105,6 +118,7 @@ resource "grafana_oncall_integration" "test-acc-integration" {
- `link` (String) The link for using in an integrated tool.
+
### Nested Schema for `default_route`
Optional:
@@ -119,6 +133,7 @@ Read-Only:
- `id` (String)
+
### Nested Schema for `default_route.msteams`
Optional:
@@ -126,8 +141,8 @@ Optional:
- `enabled` (Boolean) Enable notification in MS teams. Defaults to `true`.
- `id` (String) MS teams channel id. Alerts will be directed to this channel in Microsoft teams.
-
+
### Nested Schema for `default_route.slack`
Optional:
@@ -135,8 +150,8 @@ Optional:
- `channel_id` (String) Slack channel id. Alerts will be directed to this channel in Slack.
- `enabled` (Boolean) Enable notification in Slack. Defaults to `true`.
-
+
### Nested Schema for `default_route.telegram`
Optional:
@@ -144,9 +159,8 @@ Optional:
- `enabled` (Boolean) Enable notification in Telegram. Defaults to `true`.
- `id` (String) Telegram channel id. Alerts will be directed to this channel in Telegram.
-
-
+
### Nested Schema for `templates`
Optional:
@@ -165,6 +179,7 @@ Optional:
- `web` (Block List, Max: 1) Templates for Web. (see [below for nested schema](#nestedblock--templates--web))
+
### Nested Schema for `templates.email`
Optional:
@@ -172,8 +187,8 @@ Optional:
- `message` (String) Template for Alert message.
- `title` (String) Template for Alert title.
-
+
### Nested Schema for `templates.microsoft_teams`
Optional:
@@ -182,8 +197,8 @@ Optional:
- `message` (String) Template for Alert message.
- `title` (String) Template for Alert title.
-
+
### Nested Schema for `templates.mobile_app`
Optional:
@@ -191,16 +206,16 @@ Optional:
- `message` (String) Template for Alert message.
- `title` (String) Template for Alert title.
-
+
### Nested Schema for `templates.phone_call`
Optional:
- `title` (String) Template for Alert title.
-
+
### Nested Schema for `templates.slack`
Optional:
@@ -209,16 +224,16 @@ Optional:
- `message` (String) Template for Alert message.
- `title` (String) Template for Alert title.
-
+
### Nested Schema for `templates.sms`
Optional:
- `title` (String) Template for Alert title.
-
+
### Nested Schema for `templates.telegram`
Optional:
@@ -227,8 +242,8 @@ Optional:
- `message` (String) Template for Alert message.
- `title` (String) Template for Alert title.
-
+
### Nested Schema for `templates.web`
Optional:
diff --git a/internal/resources/oncall/resource_integration.go b/internal/resources/oncall/resource_integration.go
index a5f3f95be..e535a90e8 100644
--- a/internal/resources/oncall/resource_integration.go
+++ b/internal/resources/oncall/resource_integration.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
+ "sort"
"strings"
onCallAPI "github.com/grafana/amixr-api-go-client"
@@ -223,9 +224,6 @@ func resourceIntegration() *common.Resource {
return false
}
for k, v := range oldTemplateMap {
- // Convert everything to string to be able to compare across types.
- // We're only interested in the actual value here,
- // and Terraform will implicitly convert a string to a number, and vice versa.
if fmt.Sprintf("%v", newTemplateMap[k]) != fmt.Sprintf("%v", v) {
return false
}
@@ -234,26 +232,50 @@ func resourceIntegration() *common.Resource {
},
},
"labels": {
- Type: schema.TypeList,
- Elem: &schema.Schema{
- Type: schema.TypeMap,
- Elem: &schema.Schema{
- Type: schema.TypeString,
+ Type: schema.TypeSet,
+ Optional: true,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "key": {
+ Type: schema.TypeString,
+ Required: true,
+ },
+ "value": {
+ Type: schema.TypeString,
+ Required: true,
+ },
},
},
- Optional: true,
- 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).",
+ Set: schema.HashResource(&schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "key": {Type: schema.TypeString},
+ "value": {Type: schema.TypeString},
+ },
+ }),
+ Description: "A set of static labels. Each item must include a \"key\" and \"value\" (using the `grafana_oncall_label` datasource).",
},
"dynamic_labels": {
- Type: schema.TypeList,
- Elem: &schema.Schema{
- Type: schema.TypeMap,
- Elem: &schema.Schema{
- Type: schema.TypeString,
+ Type: schema.TypeSet,
+ Optional: true,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "key": {
+ Type: schema.TypeString,
+ Required: true,
+ },
+ "value": {
+ Type: schema.TypeString,
+ Required: true,
+ },
},
},
- Optional: true,
- 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).",
+ Set: schema.HashResource(&schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "key": {Type: schema.TypeString},
+ "value": {Type: schema.TypeString},
+ },
+ }),
+ Description: "A set of dynamic labels. Each item must include a \"key\" and \"value\" (using the `grafana_oncall_label` datasource).",
},
},
}
@@ -321,10 +343,35 @@ func resourceIntegrationCreate(ctx context.Context, d *schema.ResourceData, clie
teamIDData := d.Get("team_id").(string)
nameData := d.Get("name").(string)
typeData := d.Get("type").(string)
- templatesData := d.Get("templates").([]any)
+
+ var templatesData []any
+ switch v := d.Get("templates").(type) {
+ case []any:
+ templatesData = v
+ default:
+ templatesData = []any{}
+ }
defaultRouteData := d.Get("default_route").([]any)
- labelsData := d.Get("labels").([]any)
- dynamicLabelsData := d.Get("dynamic_labels").([]any)
+
+ var labelsData []any
+ switch v := d.Get("labels").(type) {
+ case *schema.Set:
+ labelsData = v.List()
+ case []any:
+ labelsData = v
+ default:
+ labelsData = []any{}
+ }
+
+ var dynamicLabelsData []any
+ switch v := d.Get("dynamic_labels").(type) {
+ case *schema.Set:
+ dynamicLabelsData = v.List()
+ case []any:
+ dynamicLabelsData = v
+ default:
+ dynamicLabelsData = []any{}
+ }
createOptions := &onCallAPI.CreateIntegrationOptions{
TeamId: teamIDData,
@@ -349,15 +396,40 @@ func resourceIntegrationCreate(ctx context.Context, d *schema.ResourceData, clie
func resourceIntegrationUpdate(ctx context.Context, d *schema.ResourceData, client *onCallAPI.Client) diag.Diagnostics {
nameData := d.Get("name").(string)
teamIDData := d.Get("team_id").(string)
- templateData := d.Get("templates").([]any)
+
+ var templatesData []any
+ switch v := d.Get("templates").(type) {
+ case []any:
+ templatesData = v
+ default:
+ templatesData = []any{}
+ }
defaultRouteData := d.Get("default_route").([]any)
- labelsData := d.Get("labels").([]any)
- dynamicLabelsData := d.Get("dynamic_labels").([]any)
+
+ var labelsData []any
+ switch v := d.Get("labels").(type) {
+ case *schema.Set:
+ labelsData = v.List()
+ case []any:
+ labelsData = v
+ default:
+ labelsData = []any{}
+ }
+
+ var dynamicLabelsData []any
+ switch v := d.Get("dynamic_labels").(type) {
+ case *schema.Set:
+ dynamicLabelsData = v.List()
+ case []any:
+ dynamicLabelsData = v
+ default:
+ dynamicLabelsData = []any{}
+ }
updateOptions := &onCallAPI.UpdateIntegrationOptions{
Name: nameData,
TeamId: teamIDData,
- Templates: expandTemplates(templateData),
+ Templates: expandTemplates(templatesData),
DefaultRoute: expandDefaultRoute(defaultRouteData),
Labels: expandLabels(labelsData),
DynamicLabels: expandLabels(dynamicLabelsData),
@@ -491,6 +563,9 @@ func expandRouteMSTeams(in []any) *onCallAPI.MSTeamsRoute {
}
func flattenTemplates(in *onCallAPI.Templates) []map[string]any {
+ if in == nil {
+ return []map[string]any{}
+ }
templates := make([]map[string]any, 0, 1)
out := make(map[string]any)
add := false
@@ -777,17 +852,17 @@ func flattenDefaultRoute(in *onCallAPI.DefaultRoute, d *schema.ResourceData) []m
out := make(map[string]any)
out["id"] = in.ID
out["escalation_chain_id"] = in.EscalationChainId
- // Set messengers data only if related fields are present
+
_, slackOk := d.GetOk("default_route.0.slack")
- if slackOk {
+ if slackOk && in.SlackRoute != nil {
out["slack"] = flattenRouteSlack(in.SlackRoute)
}
_, telegramOk := d.GetOk("default_route.0.telegram")
- if telegramOk {
+ if telegramOk && in.TelegramRoute != nil {
out["telegram"] = flattenRouteTelegram(in.TelegramRoute)
}
_, msteamsOk := d.GetOk("default_route.0.msteams")
- if msteamsOk {
+ if msteamsOk && in.MSTeamsRoute != nil {
out["msteams"] = flattenRouteMSTeams(in.MSTeamsRoute)
}
@@ -800,8 +875,9 @@ func expandDefaultRoute(input []any) *onCallAPI.DefaultRoute {
for _, r := range input {
inputMap := r.(map[string]any)
- id := inputMap["id"].(string)
- defaultRoute.ID = id
+ if v, ok := inputMap["id"].(string); ok && v != "" {
+ defaultRoute.ID = v
+ }
if inputMap["escalation_chain_id"] != "" {
escalationChainID := inputMap["escalation_chain_id"].(string)
defaultRoute.EscalationChainId = &escalationChainID
@@ -845,15 +921,21 @@ func expandLabels(input []any) []*onCallAPI.Label {
}
func flattenLabels(labels []*onCallAPI.Label) []map[string]string {
- flattenedLabels := make([]map[string]string, 0, 1)
+ flattenedLabels := make([]map[string]string, 0, len(labels))
for _, l := range labels {
flattenedLabels = append(flattenedLabels, map[string]string{
- "id": l.Key.Name,
"key": l.Key.Name,
"value": l.Value.Name,
})
}
+ sort.Slice(flattenedLabels, func(i, j int) bool {
+ if flattenedLabels[i]["key"] == flattenedLabels[j]["key"] {
+ return flattenedLabels[i]["value"] < flattenedLabels[j]["value"]
+ }
+ return flattenedLabels[i]["key"] < flattenedLabels[j]["key"]
+ })
+
return flattenedLabels
}
diff --git a/internal/resources/oncall/resource_integration_test.go b/internal/resources/oncall/resource_integration_test.go
index 224b802a6..bc17f025b 100644
--- a/internal/resources/oncall/resource_integration_test.go
+++ b/internal/resources/oncall/resource_integration_test.go
@@ -33,7 +33,7 @@ func TestAccOnCallIntegration_basic(t *testing.T) {
),
},
{
- Config: testAccOnCallIntegrationConfig(rName, rType, `templates {}`),
+ Config: testAccOnCallIntegrationConfig(rName, rType, `templates = [{}]`),
Check: resource.ComposeTestCheckFunc(
testAccCheckOnCallIntegrationResourceExists("grafana_oncall_integration.test-acc-integration"),
resource.TestCheckResourceAttr("grafana_oncall_integration.test-acc-integration", "name", rName),
@@ -43,9 +43,9 @@ func TestAccOnCallIntegration_basic(t *testing.T) {
),
},
{
- Config: testAccOnCallIntegrationConfig(rName, rType, `templates {
+ Config: testAccOnCallIntegrationConfig(rName, rType, `templates = [{
grouping_key = "test"
- }`),
+ }]`),
Check: resource.ComposeTestCheckFunc(
testAccCheckOnCallIntegrationResourceExists("grafana_oncall_integration.test-acc-integration"),
resource.TestCheckResourceAttr("grafana_oncall_integration.test-acc-integration", "name", rName),
@@ -86,8 +86,10 @@ func TestAccOnCallIntegration_basic(t *testing.T) {
resource.TestCheckResourceAttr("grafana_oncall_integration.test-acc-integration", "type", rType),
resource.TestCheckResourceAttrSet("grafana_oncall_integration.test-acc-integration", "link"),
resource.TestCheckResourceAttr("grafana_oncall_integration.test-acc-integration", "labels.#", "1"),
- resource.TestCheckResourceAttr("grafana_oncall_integration.test-acc-integration", "labels.0.key", "TestKey"),
- resource.TestCheckResourceAttr("grafana_oncall_integration.test-acc-integration", "labels.0.value", "TestValue"),
+ resource.TestCheckTypeSetElemNestedAttrs("grafana_oncall_integration.test-acc-integration", "labels.*", map[string]string{
+ "key": "TestKey",
+ "value": "TestValue",
+ }),
),
},
},
@@ -136,7 +138,12 @@ data "grafana_oncall_label" "test-acc-integration-label" {
}
`
- return datasource + testAccOnCallIntegrationConfig(rName, rType, `labels = [data.grafana_oncall_label.test-acc-integration-label]`)
+ return datasource + testAccOnCallIntegrationConfig(rName, rType, `
+labels = [{
+ key = data.grafana_oncall_label.test-acc-integration-label.key
+ value = data.grafana_oncall_label.test-acc-integration-label.value
+}]
+`)
}
func testAccCheckOnCallIntegrationResourceExists(name string) resource.TestCheckFunc {