From 4b46119433f9fde58afb02fd9b5d47efd40c5cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20H=C3=B6nle?= Date: Fri, 12 Sep 2025 17:28:50 +0200 Subject: [PATCH] feat(observability): add send_resolved field (#935) relates to STACKITTPR-283 --- docs/data-sources/observability_instance.md | 3 + docs/resources/observability_instance.md | 3 + .../observability/instance/datasource.go | 12 +++ .../observability/instance/resource.go | 84 ++++++++++++------- .../observability/instance/resource_test.go | 51 ++++++----- .../observability/observability_acc_test.go | 3 + .../observability/testdata/resource-max.tf | 6 ++ 7 files changed, 110 insertions(+), 52 deletions(-) diff --git a/docs/data-sources/observability_instance.md b/docs/data-sources/observability_instance.md index 85c36c3f..23bfdb5c 100644 --- a/docs/data-sources/observability_instance.md +++ b/docs/data-sources/observability_instance.md @@ -99,6 +99,7 @@ Read-Only: - `auth_password` (String, Sensitive) SMTP authentication password. - `auth_username` (String) SMTP authentication username. - `from` (String) The sender email address. Must be a valid email address +- `send_resolved` (Boolean) Whether to notify about resolved alerts. - `smart_host` (String) The SMTP host through which emails are sent. - `to` (String) The email address to send notifications to. Must be a valid email address @@ -111,6 +112,7 @@ Read-Only: - `api_key` (String) The API key for OpsGenie. - `api_url` (String) The host to send OpsGenie API requests to. Must be a valid URL - `priority` (String) Priority of the alert. Possible values are: `P1`, `P2`, `P3`, `P4`, `P5`. +- `send_resolved` (Boolean) Whether to notify about resolved alerts. - `tags` (String) Comma separated list of tags attached to the notifications. @@ -121,6 +123,7 @@ Read-Only: - `google_chat` (Boolean) Google Chat webhooks require special handling, set this to true if the webhook is for Google Chat. - `ms_teams` (Boolean) Microsoft Teams webhooks require special handling, set this to true if the webhook is for Microsoft Teams. +- `send_resolved` (Boolean) Whether to notify about resolved alerts. - `url` (String, Sensitive) The endpoint to send HTTP POST requests to. Must be a valid URL diff --git a/docs/resources/observability_instance.md b/docs/resources/observability_instance.md index ef178fe1..c60c2167 100644 --- a/docs/resources/observability_instance.md +++ b/docs/resources/observability_instance.md @@ -104,6 +104,7 @@ Optional: - `auth_password` (String, Sensitive) SMTP authentication password. - `auth_username` (String) SMTP authentication username. - `from` (String) The sender email address. Must be a valid email address +- `send_resolved` (Boolean) Whether to notify about resolved alerts. - `smart_host` (String) The SMTP host through which emails are sent. - `to` (String) The email address to send notifications to. Must be a valid email address @@ -116,6 +117,7 @@ Optional: - `api_key` (String) The API key for OpsGenie. - `api_url` (String) The host to send OpsGenie API requests to. Must be a valid URL - `priority` (String) Priority of the alert. Possible values are: `P1`, `P2`, `P3`, `P4`, `P5`. +- `send_resolved` (Boolean) Whether to notify about resolved alerts. - `tags` (String) Comma separated list of tags attached to the notifications. @@ -126,6 +128,7 @@ Optional: - `google_chat` (Boolean) Google Chat webhooks require special handling, set this to true if the webhook is for Google Chat. - `ms_teams` (Boolean) Microsoft Teams webhooks require special handling, set this to true if the webhook is for Microsoft Teams. +- `send_resolved` (Boolean) Whether to notify about resolved alerts. - `url` (String, Sensitive) The endpoint to send HTTP POST requests to. Must be a valid URL diff --git a/stackit/internal/services/observability/instance/datasource.go b/stackit/internal/services/observability/instance/datasource.go index e099730e..9f3a8634 100644 --- a/stackit/internal/services/observability/instance/datasource.go +++ b/stackit/internal/services/observability/instance/datasource.go @@ -220,6 +220,10 @@ func (d *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques Description: "The sender email address. Must be a valid email address", Computed: true, }, + "send_resolved": schema.BoolAttribute{ + Description: "Whether to notify about resolved alerts.", + Computed: true, + }, "smart_host": schema.StringAttribute{ Description: "The SMTP host through which emails are sent.", Computed: true, @@ -252,6 +256,10 @@ func (d *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques Description: "Priority of the alert. " + utils.FormatPossibleValues([]string{"P1", "P2", "P3", "P4", "P5"}...), Computed: true, }, + "send_resolved": schema.BoolAttribute{ + Description: "Whether to notify about resolved alerts.", + Computed: true, + }, }, }, }, @@ -273,6 +281,10 @@ func (d *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques Description: "Google Chat webhooks require special handling, set this to true if the webhook is for Google Chat.", Computed: true, }, + "send_resolved": schema.BoolAttribute{ + Description: "Whether to notify about resolved alerts.", + Computed: true, + }, }, }, }, diff --git a/stackit/internal/services/observability/instance/resource.go b/stackit/internal/services/observability/instance/resource.go index 990f9372..eccad5e4 100644 --- a/stackit/internal/services/observability/instance/resource.go +++ b/stackit/internal/services/observability/instance/resource.go @@ -188,6 +188,7 @@ type emailConfigsModel struct { AuthPassword types.String `tfsdk:"auth_password"` AuthUsername types.String `tfsdk:"auth_username"` From types.String `tfsdk:"from"` + SendResolved types.Bool `tfsdk:"send_resolved"` Smarthost types.String `tfsdk:"smart_host"` To types.String `tfsdk:"to"` } @@ -197,36 +198,41 @@ var emailConfigsTypes = map[string]attr.Type{ "auth_password": types.StringType, "auth_username": types.StringType, "from": types.StringType, + "send_resolved": types.BoolType, "smart_host": types.StringType, "to": types.StringType, } // Struct corresponding to Model.AlertConfig.receivers.opsGenieConfigs type opsgenieConfigsModel struct { - ApiKey types.String `tfsdk:"api_key"` - ApiUrl types.String `tfsdk:"api_url"` - Tags types.String `tfsdk:"tags"` - Priority types.String `tfsdk:"priority"` + ApiKey types.String `tfsdk:"api_key"` + ApiUrl types.String `tfsdk:"api_url"` + Tags types.String `tfsdk:"tags"` + Priority types.String `tfsdk:"priority"` + SendResolved types.Bool `tfsdk:"send_resolved"` } var opsgenieConfigsTypes = map[string]attr.Type{ - "api_key": types.StringType, - "api_url": types.StringType, - "tags": types.StringType, - "priority": types.StringType, + "api_key": types.StringType, + "api_url": types.StringType, + "tags": types.StringType, + "priority": types.StringType, + "send_resolved": types.BoolType, } // Struct corresponding to Model.AlertConfig.receivers.webHooksConfigs type webHooksConfigsModel struct { - Url types.String `tfsdk:"url"` - MsTeams types.Bool `tfsdk:"ms_teams"` - GoogleChat types.Bool `tfsdk:"google_chat"` + Url types.String `tfsdk:"url"` + MsTeams types.Bool `tfsdk:"ms_teams"` + GoogleChat types.Bool `tfsdk:"google_chat"` + SendResolved types.Bool `tfsdk:"send_resolved"` } var webHooksConfigsTypes = map[string]attr.Type{ - "url": types.StringType, - "ms_teams": types.BoolType, - "google_chat": types.BoolType, + "url": types.StringType, + "ms_teams": types.BoolType, + "google_chat": types.BoolType, + "send_resolved": types.BoolType, } var routeDescriptions = map[string]string{ @@ -629,6 +635,10 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r Description: "The sender email address. Must be a valid email address", Optional: true, }, + "send_resolved": schema.BoolAttribute{ + Description: "Whether to notify about resolved alerts.", + Optional: true, + }, "smart_host": schema.StringAttribute{ Description: "The SMTP host through which emails are sent.", Optional: true, @@ -664,6 +674,10 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r Description: "Priority of the alert. " + utils.FormatPossibleValues("P1", "P2", "P3", "P4", "P5"), Optional: true, }, + "send_resolved": schema.BoolAttribute{ + Description: "Whether to notify about resolved alerts.", + Optional: true, + }, }, }, }, @@ -695,6 +709,10 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r Computed: true, Default: booldefault.StaticBool(false), }, + "send_resolved": schema.BoolAttribute{ + Description: "Whether to notify about resolved alerts.", + Optional: true, + }, }, }, }, @@ -1490,6 +1508,7 @@ func getMockAlertConfig(ctx context.Context) (alertConfigModel, error) { mockEmailConfig, diags := types.ObjectValue(emailConfigsTypes, map[string]attr.Value{ "to": types.StringValue("123@gmail.com"), "smart_host": types.StringValue("smtp.gmail.com:587"), + "send_resolved": types.BoolValue(false), "from": types.StringValue("xxxx@gmail.com"), "auth_username": types.StringValue("xxxx@gmail.com"), "auth_password": types.StringValue("xxxxxxxxx"), @@ -1635,6 +1654,7 @@ func mapReceiversToAttributes(ctx context.Context, respReceivers *[]observabilit "auth_password": types.StringPointerValue(emailConfig.AuthPassword), "auth_username": types.StringPointerValue(emailConfig.AuthUsername), "from": types.StringPointerValue(emailConfig.From), + "send_resolved": types.BoolPointerValue(emailConfig.SendResolved), "smart_host": types.StringPointerValue(emailConfig.Smarthost), "to": types.StringPointerValue(emailConfig.To), } @@ -1650,10 +1670,11 @@ func mapReceiversToAttributes(ctx context.Context, respReceivers *[]observabilit if receiver.OpsgenieConfigs != nil { for _, opsgenieConfig := range *receiver.OpsgenieConfigs { opsGenieConfigMap := map[string]attr.Value{ - "api_key": types.StringPointerValue(opsgenieConfig.ApiKey), - "api_url": types.StringPointerValue(opsgenieConfig.ApiUrl), - "tags": types.StringPointerValue(opsgenieConfig.Tags), - "priority": types.StringPointerValue(opsgenieConfig.Priority), + "api_key": types.StringPointerValue(opsgenieConfig.ApiKey), + "api_url": types.StringPointerValue(opsgenieConfig.ApiUrl), + "tags": types.StringPointerValue(opsgenieConfig.Tags), + "priority": types.StringPointerValue(opsgenieConfig.Priority), + "send_resolved": types.BoolPointerValue(opsgenieConfig.SendResolved), } opsGenieConfigModel, diags := types.ObjectValue(opsgenieConfigsTypes, opsGenieConfigMap) if diags.HasError() { @@ -1666,13 +1687,11 @@ func mapReceiversToAttributes(ctx context.Context, respReceivers *[]observabilit webhooksConfigList := []attr.Value{} if receiver.WebHookConfigs != nil { for _, webhookConfig := range *receiver.WebHookConfigs { - msTeamsValue := types.BoolPointerValue(webhookConfig.MsTeams) - googleChatValue := types.BoolPointerValue(webhookConfig.GoogleChat) - webHookConfigsMap := map[string]attr.Value{ - "url": types.StringPointerValue(webhookConfig.Url), - "ms_teams": msTeamsValue, - "google_chat": googleChatValue, + "url": types.StringPointerValue(webhookConfig.Url), + "ms_teams": types.BoolPointerValue(webhookConfig.MsTeams), + "google_chat": types.BoolPointerValue(webhookConfig.GoogleChat), + "send_resolved": types.BoolPointerValue(webhookConfig.SendResolved), } webHookConfigsModel, diags := types.ObjectValue(webHooksConfigsTypes, webHookConfigsMap) if diags.HasError() { @@ -1981,6 +2000,7 @@ func toReceiverPayload(ctx context.Context, model *alertConfigModel) (*[]observa AuthPassword: conversion.StringValueToPointer(emailConfig.AuthPassword), AuthUsername: conversion.StringValueToPointer(emailConfig.AuthUsername), From: conversion.StringValueToPointer(emailConfig.From), + SendResolved: conversion.BoolValueToPointer(emailConfig.SendResolved), Smarthost: conversion.StringValueToPointer(emailConfig.Smarthost), To: conversion.StringValueToPointer(emailConfig.To), }) @@ -1998,10 +2018,11 @@ func toReceiverPayload(ctx context.Context, model *alertConfigModel) (*[]observa for i := range opsgenieConfigs { opsgenieConfig := opsgenieConfigs[i] payloadOpsGenieConfigs = append(payloadOpsGenieConfigs, observability.CreateAlertConfigReceiverPayloadOpsgenieConfigsInner{ - ApiKey: conversion.StringValueToPointer(opsgenieConfig.ApiKey), - ApiUrl: conversion.StringValueToPointer(opsgenieConfig.ApiUrl), - Tags: conversion.StringValueToPointer(opsgenieConfig.Tags), - Priority: conversion.StringValueToPointer(opsgenieConfig.Priority), + ApiKey: conversion.StringValueToPointer(opsgenieConfig.ApiKey), + ApiUrl: conversion.StringValueToPointer(opsgenieConfig.ApiUrl), + Tags: conversion.StringValueToPointer(opsgenieConfig.Tags), + Priority: conversion.StringValueToPointer(opsgenieConfig.Priority), + SendResolved: conversion.BoolValueToPointer(opsgenieConfig.SendResolved), }) } receiverPayload.OpsgenieConfigs = &payloadOpsGenieConfigs @@ -2017,9 +2038,10 @@ func toReceiverPayload(ctx context.Context, model *alertConfigModel) (*[]observa for i := range receiverWebHooksConfigs { webHooksConfig := receiverWebHooksConfigs[i] payloadWebHooksConfigs = append(payloadWebHooksConfigs, observability.CreateAlertConfigReceiverPayloadWebHookConfigsInner{ - Url: conversion.StringValueToPointer(webHooksConfig.Url), - MsTeams: conversion.BoolValueToPointer(webHooksConfig.MsTeams), - GoogleChat: conversion.BoolValueToPointer(webHooksConfig.GoogleChat), + Url: conversion.StringValueToPointer(webHooksConfig.Url), + MsTeams: conversion.BoolValueToPointer(webHooksConfig.MsTeams), + GoogleChat: conversion.BoolValueToPointer(webHooksConfig.GoogleChat), + SendResolved: conversion.BoolValueToPointer(webHooksConfig.SendResolved), }) } receiverPayload.WebHookConfigs = &payloadWebHooksConfigs diff --git a/stackit/internal/services/observability/instance/resource_test.go b/stackit/internal/services/observability/instance/resource_test.go index 963af516..e9229ccb 100644 --- a/stackit/internal/services/observability/instance/resource_test.go +++ b/stackit/internal/services/observability/instance/resource_test.go @@ -24,6 +24,7 @@ func fixtureEmailConfigsModel() basetypes.ListValue { "auth_password": types.StringValue("password"), "auth_username": types.StringValue("username"), "from": types.StringValue("notification@example.com"), + "send_resolved": types.BoolValue(true), "smart_host": types.StringValue("smtp.example.com"), "to": types.StringValue("me@example.com"), }), @@ -33,10 +34,11 @@ func fixtureEmailConfigsModel() basetypes.ListValue { func fixtureOpsGenieConfigsModel() basetypes.ListValue { return types.ListValueMust(types.ObjectType{AttrTypes: opsgenieConfigsTypes}, []attr.Value{ types.ObjectValueMust(opsgenieConfigsTypes, map[string]attr.Value{ - "api_key": types.StringValue("key"), - "tags": types.StringValue("tag"), - "api_url": types.StringValue("ops.example.com"), - "priority": types.StringValue("P3"), + "api_key": types.StringValue("key"), + "tags": types.StringValue("tag"), + "api_url": types.StringValue("ops.example.com"), + "priority": types.StringValue("P3"), + "send_resolved": types.BoolValue(true), }), }) } @@ -44,9 +46,10 @@ func fixtureOpsGenieConfigsModel() basetypes.ListValue { func fixtureWebHooksConfigsModel() basetypes.ListValue { return types.ListValueMust(types.ObjectType{AttrTypes: webHooksConfigsTypes}, []attr.Value{ types.ObjectValueMust(webHooksConfigsTypes, map[string]attr.Value{ - "url": types.StringValue("http://example.com"), - "ms_teams": types.BoolValue(true), - "google_chat": types.BoolValue(true), + "url": types.StringValue("http://example.com"), + "ms_teams": types.BoolValue(true), + "google_chat": types.BoolValue(true), + "send_resolved": types.BoolValue(true), }), }) } @@ -135,6 +138,7 @@ func fixtureEmailConfigsPayload() observability.CreateAlertConfigReceiverPayload AuthPassword: utils.Ptr("password"), AuthUsername: utils.Ptr("username"), From: utils.Ptr("notification@example.com"), + SendResolved: utils.Ptr(true), Smarthost: utils.Ptr("smtp.example.com"), To: utils.Ptr("me@example.com"), } @@ -142,18 +146,20 @@ func fixtureEmailConfigsPayload() observability.CreateAlertConfigReceiverPayload func fixtureOpsGenieConfigsPayload() observability.CreateAlertConfigReceiverPayloadOpsgenieConfigsInner { return observability.CreateAlertConfigReceiverPayloadOpsgenieConfigsInner{ - ApiKey: utils.Ptr("key"), - Tags: utils.Ptr("tag"), - ApiUrl: utils.Ptr("ops.example.com"), - Priority: utils.Ptr("P3"), + ApiKey: utils.Ptr("key"), + Tags: utils.Ptr("tag"), + ApiUrl: utils.Ptr("ops.example.com"), + Priority: utils.Ptr("P3"), + SendResolved: utils.Ptr(true), } } func fixtureWebHooksConfigsPayload() observability.CreateAlertConfigReceiverPayloadWebHookConfigsInner { return observability.CreateAlertConfigReceiverPayloadWebHookConfigsInner{ - Url: utils.Ptr("http://example.com"), - MsTeams: utils.Ptr(true), - GoogleChat: utils.Ptr(true), + Url: utils.Ptr("http://example.com"), + MsTeams: utils.Ptr(true), + GoogleChat: utils.Ptr(true), + SendResolved: utils.Ptr(true), } } @@ -216,6 +222,7 @@ func fixtureEmailConfigsResponse() observability.EmailConfig { AuthPassword: utils.Ptr("password"), AuthUsername: utils.Ptr("username"), From: utils.Ptr("notification@example.com"), + SendResolved: utils.Ptr(true), Smarthost: utils.Ptr("smtp.example.com"), To: utils.Ptr("me@example.com"), } @@ -223,18 +230,20 @@ func fixtureEmailConfigsResponse() observability.EmailConfig { func fixtureOpsGenieConfigsResponse() observability.OpsgenieConfig { return observability.OpsgenieConfig{ - ApiKey: utils.Ptr("key"), - Tags: utils.Ptr("tag"), - ApiUrl: utils.Ptr("ops.example.com"), - Priority: utils.Ptr("P3"), + ApiKey: utils.Ptr("key"), + Tags: utils.Ptr("tag"), + ApiUrl: utils.Ptr("ops.example.com"), + Priority: utils.Ptr("P3"), + SendResolved: utils.Ptr(true), } } func fixtureWebHooksConfigsResponse() observability.WebHook { return observability.WebHook{ - Url: utils.Ptr("http://example.com"), - MsTeams: utils.Ptr(true), - GoogleChat: utils.Ptr(true), + Url: utils.Ptr("http://example.com"), + MsTeams: utils.Ptr(true), + GoogleChat: utils.Ptr(true), + SendResolved: utils.Ptr(true), } } diff --git a/stackit/internal/services/observability/observability_acc_test.go b/stackit/internal/services/observability/observability_acc_test.go index caa52b36..7e395301 100644 --- a/stackit/internal/services/observability/observability_acc_test.go +++ b/stackit/internal/services/observability/observability_acc_test.go @@ -79,15 +79,18 @@ var testConfigVarsMax = config.Variables{ "auth_password": config.StringVariable("password"), "auth_username": config.StringVariable("username"), "email_from": config.StringVariable("aa@bb.ccc"), + "email_send_resolved": config.StringVariable("true"), "smart_host": config.StringVariable("smtp.gmail.com:587"), "email_to": config.StringVariable("bb@bb.ccc"), "opsgenie_api_key": config.StringVariable("example-api-key"), "opsgenie_api_tags": config.StringVariable("observability-alert"), "opsgenie_api_url": config.StringVariable("https://api.eu.opsgenie.com"), "opsgenie_priority": config.StringVariable("P3"), + "opsgenie_send_resolved": config.StringVariable("false"), "webhook_configs_url": config.StringVariable("https://example.com"), "ms_teams": config.StringVariable("true"), "google_chat": config.StringVariable("false"), + "webhook_configs_send_resolved": config.StringVariable("false"), "group_by": config.StringVariable("alertname"), "group_interval": config.StringVariable("10m"), "group_wait": config.StringVariable("1m"), diff --git a/stackit/internal/services/observability/testdata/resource-max.tf b/stackit/internal/services/observability/testdata/resource-max.tf index 91cb513b..9b82a43a 100644 --- a/stackit/internal/services/observability/testdata/resource-max.tf +++ b/stackit/internal/services/observability/testdata/resource-max.tf @@ -21,15 +21,18 @@ variable "auth_identity" {} variable "auth_password" {} variable "auth_username" {} variable "email_from" {} +variable "email_send_resolved" {} variable "smart_host" {} variable "email_to" {} variable "opsgenie_api_key" {} variable "opsgenie_api_tags" {} variable "opsgenie_api_url" {} variable "opsgenie_priority" {} +variable "opsgenie_send_resolved" {} variable "webhook_configs_url" {} variable "ms_teams" {} variable "google_chat" {} +variable "webhook_configs_send_resolved" {} variable "group_by" {} variable "group_interval" {} variable "group_wait" {} @@ -113,6 +116,7 @@ resource "stackit_observability_instance" "instance" { from = var.email_from smart_host = var.smart_host to = var.email_to + send_resolved = var.email_send_resolved } ] opsgenie_configs = [ @@ -121,6 +125,7 @@ resource "stackit_observability_instance" "instance" { tags = var.opsgenie_api_tags api_url = var.opsgenie_api_url priority = var.opsgenie_priority + send_resolved = var.opsgenie_send_resolved } ] webhooks_configs = [ @@ -128,6 +133,7 @@ resource "stackit_observability_instance" "instance" { url = var.webhook_configs_url ms_teams = var.ms_teams google_chat = var.google_chat + send_resolved = var.webhook_configs_send_resolved } ] },