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 {