From 65c6106ea94a8839bfcdd7fe91c93ef1964f59b2 Mon Sep 17 00:00:00 2001 From: Marcel Jacek <72880145+marceljk@users.noreply.github.com> Date: Fri, 12 Sep 2025 09:58:50 +0200 Subject: [PATCH] fix(observability): remove invalid fields `match` and `match_regex` from main route in alert_config (#915) * fix: remove invalid `match` and `match_regex` from main route in alert_config - deprecated `match` and `match_regex` in child routes - add new `matchers` field --- docs/data-sources/observability_instance.md | 7 +- docs/resources/observability_instance.md | 7 +- .../observability/instance/datasource.go | 10 - .../observability/instance/resource.go | 179 ++++++++++-------- .../observability/instance/resource_test.go | 43 +++-- .../observability/observability_acc_test.go | 8 + .../observability/testdata/resource-max.tf | 4 + stackit/internal/testutil/testutil.go | 5 +- stackit/internal/testutil/testutil_test.go | 5 + 9 files changed, 157 insertions(+), 111 deletions(-) diff --git a/docs/data-sources/observability_instance.md b/docs/data-sources/observability_instance.md index 1e69cc72..85c36c3f 100644 --- a/docs/data-sources/observability_instance.md +++ b/docs/data-sources/observability_instance.md @@ -133,8 +133,6 @@ Read-Only: - `group_by` (List of String) The labels by which incoming alerts are grouped together. For example, multiple alerts coming in for cluster=A and alertname=LatencyHigh would be batched into a single group. To aggregate by all possible labels use the special value '...' as the sole label name, for example: group_by: ['...']. This effectively disables aggregation entirely, passing through all alerts as-is. This is unlikely to be what you want, unless you have a very low alert volume or your upstream notification system performs its own grouping. - `group_interval` (String) How long to wait before sending a notification about new alerts that are added to a group of alerts for which an initial notification has already been sent. (Usually ~5m or more.) - `group_wait` (String) How long to initially wait to send a notification for a group of alerts. Allows to wait for an inhibiting alert to arrive or collect more initial alerts for the same group. (Usually ~0s to few minutes.) . -- `match` (Map of String) A set of equality matchers an alert has to fulfill to match the node. -- `match_regex` (Map of String) A set of regex-matchers an alert has to fulfill to match the node. - `receiver` (String) The name of the receiver to route the alerts to. - `repeat_interval` (String) How long to wait before sending a notification again if it has already been sent successfully for an alert. (Usually ~3h or more). - `routes` (Attributes List) List of child routes. (see [below for nested schema](#nestedatt--alert_config--route--routes)) @@ -147,7 +145,8 @@ Read-Only: - `group_by` (List of String) The labels by which incoming alerts are grouped together. For example, multiple alerts coming in for cluster=A and alertname=LatencyHigh would be batched into a single group. To aggregate by all possible labels use the special value '...' as the sole label name, for example: group_by: ['...']. This effectively disables aggregation entirely, passing through all alerts as-is. This is unlikely to be what you want, unless you have a very low alert volume or your upstream notification system performs its own grouping. - `group_interval` (String) How long to wait before sending a notification about new alerts that are added to a group of alerts for which an initial notification has already been sent. (Usually ~5m or more.) - `group_wait` (String) How long to initially wait to send a notification for a group of alerts. Allows to wait for an inhibiting alert to arrive or collect more initial alerts for the same group. (Usually ~0s to few minutes.) -- `match` (Map of String) A set of equality matchers an alert has to fulfill to match the node. -- `match_regex` (Map of String) A set of regex-matchers an alert has to fulfill to match the node. +- `match` (Map of String, Deprecated) A set of equality matchers an alert has to fulfill to match the node. This field is deprecated and will be removed after 10th March 2026, use `matchers` in the `routes` instead +- `match_regex` (Map of String, Deprecated) A set of regex-matchers an alert has to fulfill to match the node. This field is deprecated and will be removed after 10th March 2026, use `matchers` in the `routes` instead +- `matchers` (List of String) A list of matchers that an alert has to fulfill to match the node. A matcher is a string with a syntax inspired by PromQL and OpenMetrics. - `receiver` (String) The name of the receiver to route the alerts to. - `repeat_interval` (String) How long to wait before sending a notification again if it has already been sent successfully for an alert. (Usually ~3h or more). diff --git a/docs/resources/observability_instance.md b/docs/resources/observability_instance.md index 83e68b13..ef178fe1 100644 --- a/docs/resources/observability_instance.md +++ b/docs/resources/observability_instance.md @@ -142,8 +142,6 @@ Optional: - `group_by` (List of String) The labels by which incoming alerts are grouped together. For example, multiple alerts coming in for cluster=A and alertname=LatencyHigh would be batched into a single group. To aggregate by all possible labels use the special value '...' as the sole label name, for example: group_by: ['...']. This effectively disables aggregation entirely, passing through all alerts as-is. This is unlikely to be what you want, unless you have a very low alert volume or your upstream notification system performs its own grouping. - `group_interval` (String) How long to wait before sending a notification about new alerts that are added to a group of alerts for which an initial notification has already been sent. (Usually ~5m or more.) - `group_wait` (String) How long to initially wait to send a notification for a group of alerts. Allows to wait for an inhibiting alert to arrive or collect more initial alerts for the same group. (Usually ~0s to few minutes.) -- `match` (Map of String) A set of equality matchers an alert has to fulfill to match the node. -- `match_regex` (Map of String) A set of regex-matchers an alert has to fulfill to match the node. - `repeat_interval` (String) How long to wait before sending a notification again if it has already been sent successfully for an alert. (Usually ~3h or more). - `routes` (Attributes List) List of child routes. (see [below for nested schema](#nestedatt--alert_config--route--routes)) @@ -159,8 +157,9 @@ Optional: - `group_by` (List of String) The labels by which incoming alerts are grouped together. For example, multiple alerts coming in for cluster=A and alertname=LatencyHigh would be batched into a single group. To aggregate by all possible labels use the special value '...' as the sole label name, for example: group_by: ['...']. This effectively disables aggregation entirely, passing through all alerts as-is. This is unlikely to be what you want, unless you have a very low alert volume or your upstream notification system performs its own grouping. - `group_interval` (String) How long to wait before sending a notification about new alerts that are added to a group of alerts for which an initial notification has already been sent. (Usually ~5m or more.) - `group_wait` (String) How long to initially wait to send a notification for a group of alerts. Allows to wait for an inhibiting alert to arrive or collect more initial alerts for the same group. (Usually ~0s to few minutes.) -- `match` (Map of String) A set of equality matchers an alert has to fulfill to match the node. -- `match_regex` (Map of String) A set of regex-matchers an alert has to fulfill to match the node. +- `match` (Map of String, Deprecated) A set of equality matchers an alert has to fulfill to match the node. This field is deprecated and will be removed after 10th March 2026, use `matchers` in the `routes` instead +- `match_regex` (Map of String, Deprecated) A set of regex-matchers an alert has to fulfill to match the node. This field is deprecated and will be removed after 10th March 2026, use `matchers` in the `routes` instead +- `matchers` (List of String) A list of matchers that an alert has to fulfill to match the node. A matcher is a string with a syntax inspired by PromQL and OpenMetrics. - `repeat_interval` (String) How long to wait before sending a notification again if it has already been sent successfully for an alert. (Usually ~3h or more). diff --git a/stackit/internal/services/observability/instance/datasource.go b/stackit/internal/services/observability/instance/datasource.go index 4fe8a7ac..e099730e 100644 --- a/stackit/internal/services/observability/instance/datasource.go +++ b/stackit/internal/services/observability/instance/datasource.go @@ -296,16 +296,6 @@ func (d *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques Description: "How long to initially wait to send a notification for a group of alerts. Allows to wait for an inhibiting alert to arrive or collect more initial alerts for the same group. (Usually ~0s to few minutes.) .", Computed: true, }, - "match": schema.MapAttribute{ - Description: "A set of equality matchers an alert has to fulfill to match the node.", - Computed: true, - ElementType: types.StringType, - }, - "match_regex": schema.MapAttribute{ - Description: "A set of regex-matchers an alert has to fulfill to match the node.", - Computed: true, - ElementType: types.StringType, - }, "receiver": schema.StringAttribute{ Description: "The name of the receiver to route the alerts to.", Computed: true, diff --git a/stackit/internal/services/observability/instance/resource.go b/stackit/internal/services/observability/instance/resource.go index a9cc1bcb..990f9372 100644 --- a/stackit/internal/services/observability/instance/resource.go +++ b/stackit/internal/services/observability/instance/resource.go @@ -40,7 +40,7 @@ import ( ) // Currently, due to incorrect types in the API, the maximum recursion level for child routes is set to 1. -// Once this is fixed, the value should be set to 10. +// Once this is fixed, the value should be set to 10 and toRoutePayload needs to be adjusted, to support it. const childRouteMaxRecursionLevel = 1 // Ensure the implementation satisfies the expected interfaces. @@ -118,12 +118,26 @@ var globalConfigurationTypes = map[string]attr.Type{ } // Struct corresponding to Model.AlertConfig.route -type routeModel struct { +type mainRouteModel struct { GroupBy types.List `tfsdk:"group_by"` GroupInterval types.String `tfsdk:"group_interval"` GroupWait types.String `tfsdk:"group_wait"` - Match types.Map `tfsdk:"match"` + Receiver types.String `tfsdk:"receiver"` + RepeatInterval types.String `tfsdk:"repeat_interval"` + Routes types.List `tfsdk:"routes"` +} + +// Struct corresponding to Model.AlertConfig.route +// This is used to map the routes between the mainRouteModel and the last level of recursion of the routes field +type routeModelMiddle struct { + GroupBy types.List `tfsdk:"group_by"` + GroupInterval types.String `tfsdk:"group_interval"` + GroupWait types.String `tfsdk:"group_wait"` + // Deprecated: Match is deprecated and will be removed after 10th March 2026. Use Matchers instead + Match types.Map `tfsdk:"match"` + // Deprecated: MatchRegex is deprecated and will be removed after 10th March 2026. Use Matchers instead MatchRegex types.Map `tfsdk:"match_regex"` + Matchers types.List `tfsdk:"matchers"` Receiver types.String `tfsdk:"receiver"` RepeatInterval types.String `tfsdk:"repeat_interval"` Routes types.List `tfsdk:"routes"` @@ -132,11 +146,14 @@ type routeModel struct { // Struct corresponding to Model.AlertConfig.route but without the recursive routes field // This is used to map the last level of recursion of the routes field type routeModelNoRoutes struct { - GroupBy types.List `tfsdk:"group_by"` - GroupInterval types.String `tfsdk:"group_interval"` - GroupWait types.String `tfsdk:"group_wait"` - Match types.Map `tfsdk:"match"` + GroupBy types.List `tfsdk:"group_by"` + GroupInterval types.String `tfsdk:"group_interval"` + GroupWait types.String `tfsdk:"group_wait"` + // Deprecated: Match is deprecated and will be removed after 10th March 2026. Use Matchers instead + Match types.Map `tfsdk:"match"` + // Deprecated: MatchRegex is deprecated and will be removed after 10th March 2026. Use Matchers instead MatchRegex types.Map `tfsdk:"match_regex"` + Matchers types.List `tfsdk:"matchers"` Receiver types.String `tfsdk:"receiver"` RepeatInterval types.String `tfsdk:"repeat_interval"` } @@ -145,8 +162,6 @@ var routeTypes = map[string]attr.Type{ "group_by": types.ListType{ElemType: types.StringType}, "group_interval": types.StringType, "group_wait": types.StringType, - "match": types.MapType{ElemType: types.StringType}, - "match_regex": types.MapType{ElemType: types.StringType}, "receiver": types.StringType, "repeat_interval": types.StringType, "routes": types.ListType{ElemType: getRouteListType()}, @@ -218,8 +233,9 @@ var routeDescriptions = map[string]string{ "group_by": "The labels by which incoming alerts are grouped together. For example, multiple alerts coming in for cluster=A and alertname=LatencyHigh would be batched into a single group. To aggregate by all possible labels use the special value '...' as the sole label name, for example: group_by: ['...']. This effectively disables aggregation entirely, passing through all alerts as-is. This is unlikely to be what you want, unless you have a very low alert volume or your upstream notification system performs its own grouping.", "group_interval": "How long to wait before sending a notification about new alerts that are added to a group of alerts for which an initial notification has already been sent. (Usually ~5m or more.)", "group_wait": "How long to initially wait to send a notification for a group of alerts. Allows to wait for an inhibiting alert to arrive or collect more initial alerts for the same group. (Usually ~0s to few minutes.)", - "match": "A set of equality matchers an alert has to fulfill to match the node.", - "match_regex": "A set of regex-matchers an alert has to fulfill to match the node.", + "match": "A set of equality matchers an alert has to fulfill to match the node. This field is deprecated and will be removed after 10th March 2026, use `matchers` in the `routes` instead", + "match_regex": "A set of regex-matchers an alert has to fulfill to match the node. This field is deprecated and will be removed after 10th March 2026, use `matchers` in the `routes` instead", + "matchers": "A list of matchers that an alert has to fulfill to match the node. A matcher is a string with a syntax inspired by PromQL and OpenMetrics.", "receiver": "The name of the receiver to route the alerts to.", "repeat_interval": "How long to wait before sending a notification again if it has already been sent successfully for an alert. (Usually ~3h or more).", "routes": "List of child routes.", @@ -241,6 +257,7 @@ func getRouteListTypeAux(level, limit int) types.ObjectType { "group_wait": types.StringType, "match": types.MapType{ElemType: types.StringType}, "match_regex": types.MapType{ElemType: types.StringType}, + "matchers": types.ListType{ElemType: types.StringType}, "receiver": types.StringType, "repeat_interval": types.StringType, } @@ -290,13 +307,21 @@ func getRouteNestedObjectAux(isDatasource bool, level, limit int) schema.ListNes }, }, "match": schema.MapAttribute{ - Description: routeDescriptions["match"], - Optional: !isDatasource, - Computed: isDatasource, - ElementType: types.StringType, + Description: routeDescriptions["match"], + DeprecationMessage: "Use `matchers` in the `routes` instead.", + Optional: !isDatasource, + Computed: isDatasource, + ElementType: types.StringType, }, "match_regex": schema.MapAttribute{ - Description: routeDescriptions["match_regex"], + Description: routeDescriptions["match_regex"], + DeprecationMessage: "Use `matchers` in the `routes` instead.", + Optional: !isDatasource, + Computed: isDatasource, + ElementType: types.StringType, + }, + "matchers": schema.ListAttribute{ + Description: routeDescriptions["matchers"], Optional: !isDatasource, Computed: isDatasource, ElementType: types.StringType, @@ -701,16 +726,6 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r stringplanmodifier.UseStateForUnknown(), }, }, - "match": schema.MapAttribute{ - Description: routeDescriptions["match"], - Optional: true, - ElementType: types.StringType, - }, - "match_regex": schema.MapAttribute{ - Description: routeDescriptions["match_regex"], - Optional: true, - ElementType: types.StringType, - }, "receiver": schema.StringAttribute{ Description: routeDescriptions["receiver"], Required: true, @@ -1521,8 +1536,6 @@ func getMockAlertConfig(ctx context.Context) (alertConfigModel, error) { "group_wait": types.StringValue("30s"), "group_interval": types.StringValue("5m"), "repeat_interval": types.StringValue("4h"), - "match": types.MapNull(types.StringType), - "match_regex": types.MapNull(types.StringType), "routes": types.ListNull(getRouteListType()), }) if diags.HasError() { @@ -1735,16 +1748,6 @@ func mapRouteToAttributes(ctx context.Context, route *observability.Route) (attr return types.ObjectNull(routeTypes), fmt.Errorf("mapping group by: %w", core.DiagsToError(diags)) } - matchModel, diags := types.MapValueFrom(ctx, types.StringType, route.Match) - if diags.HasError() { - return types.ObjectNull(routeTypes), fmt.Errorf("mapping match: %w", core.DiagsToError(diags)) - } - - matchRegexModel, diags := types.MapValueFrom(ctx, types.StringType, route.MatchRe) - if diags.HasError() { - return types.ObjectNull(routeTypes), fmt.Errorf("mapping match regex: %w", core.DiagsToError(diags)) - } - childRoutes, err := mapChildRoutesToAttributes(ctx, route.Routes) if err != nil { return types.ObjectNull(routeTypes), fmt.Errorf("mapping child routes: %w", err) @@ -1754,8 +1757,6 @@ func mapRouteToAttributes(ctx context.Context, route *observability.Route) (attr "group_by": groupByModel, "group_interval": types.StringPointerValue(route.GroupInterval), "group_wait": types.StringPointerValue(route.GroupWait), - "match": matchModel, - "match_regex": matchRegexModel, "receiver": types.StringPointerValue(route.Receiver), "repeat_interval": types.StringPointerValue(route.RepeatInterval), "routes": childRoutes, @@ -1796,12 +1797,18 @@ func mapChildRoutesToAttributes(ctx context.Context, routes *[]observability.Rou return nullList, fmt.Errorf("mapping match regex: %w", core.DiagsToError(diags)) } + matchersModel, diags := types.ListValueFrom(ctx, types.StringType, route.Matchers) + if diags.HasError() { + return nullList, fmt.Errorf("mapping matchers: %w", core.DiagsToError(diags)) + } + routeMap := map[string]attr.Value{ "group_by": groupByModel, "group_interval": types.StringPointerValue(route.GroupInterval), "group_wait": types.StringPointerValue(route.GroupWait), "match": matchModel, "match_regex": matchRegexModel, + "matchers": matchersModel, "receiver": types.StringPointerValue(route.Receiver), "repeat_interval": types.StringPointerValue(route.RepeatInterval), } @@ -1921,7 +1928,7 @@ func toUpdateAlertConfigPayload(ctx context.Context, model *alertConfigModel) (* return nil, fmt.Errorf("mapping receivers: %w", err) } - routeTF := routeModel{} + routeTF := mainRouteModel{} diags := model.Route.As(ctx, &routeTF, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, fmt.Errorf("mapping route: %w", core.DiagsToError(diags)) @@ -2023,14 +2030,12 @@ func toReceiverPayload(ctx context.Context, model *alertConfigModel) (*[]observa return &receivers, nil } -func toRoutePayload(ctx context.Context, routeTF *routeModel) (*observability.UpdateAlertConfigsPayloadRoute, error) { +func toRoutePayload(ctx context.Context, routeTF *mainRouteModel) (*observability.UpdateAlertConfigsPayloadRoute, error) { if routeTF == nil { return nil, fmt.Errorf("nil route model") } var groupByPayload *[]string - var matchPayload *map[string]interface{} - var matchRegexPayload *map[string]interface{} var childRoutesPayload *[]observability.UpdateAlertConfigsPayloadRouteRoutesInner if !routeTF.GroupBy.IsNull() && !routeTF.GroupBy.IsUnknown() { @@ -2041,24 +2046,8 @@ func toRoutePayload(ctx context.Context, routeTF *routeModel) (*observability.Up } } - if !routeTF.Match.IsNull() && !routeTF.Match.IsUnknown() { - matchMap, err := conversion.ToStringInterfaceMap(ctx, routeTF.Match) - if err != nil { - return nil, fmt.Errorf("mapping match: %w", err) - } - matchPayload = &matchMap - } - - if !routeTF.MatchRegex.IsNull() && !routeTF.MatchRegex.IsUnknown() { - matchRegexMap, err := conversion.ToStringInterfaceMap(ctx, routeTF.MatchRegex) - if err != nil { - return nil, fmt.Errorf("mapping match regex: %w", err) - } - matchRegexPayload = &matchRegexMap - } - if !routeTF.Routes.IsNull() && !routeTF.Routes.IsUnknown() { - childRoutes := []routeModel{} + childRoutes := []routeModelMiddle{} diags := routeTF.Routes.ElementsAs(ctx, &childRoutes, false) if diags.HasError() { // If there is an error, we will try to map the child routes as if they are the last child routes @@ -2070,12 +2059,13 @@ func toRoutePayload(ctx context.Context, routeTF *routeModel) (*observability.Up return nil, fmt.Errorf("mapping child routes: %w", core.DiagsToError(diags)) } for i := range lastChildRoutes { - childRoute := routeModel{ + childRoute := routeModelMiddle{ GroupBy: lastChildRoutes[i].GroupBy, GroupInterval: lastChildRoutes[i].GroupInterval, GroupWait: lastChildRoutes[i].GroupWait, Match: lastChildRoutes[i].Match, MatchRegex: lastChildRoutes[i].MatchRegex, + Matchers: lastChildRoutes[i].Matchers, Receiver: lastChildRoutes[i].Receiver, RepeatInterval: lastChildRoutes[i].RepeatInterval, Routes: types.ListNull(getRouteListType()), @@ -2087,11 +2077,11 @@ func toRoutePayload(ctx context.Context, routeTF *routeModel) (*observability.Up childRoutesList := []observability.UpdateAlertConfigsPayloadRouteRoutesInner{} for i := range childRoutes { childRoute := childRoutes[i] - childRoutePayload, err := toRoutePayload(ctx, &childRoute) + childRoutePayload, err := toChildRoutePayload(ctx, &childRoute) if err != nil { return nil, fmt.Errorf("mapping child route: %w", err) } - childRoutesList = append(childRoutesList, *toChildRoutePayload(childRoutePayload)) + childRoutesList = append(childRoutesList, *childRoutePayload) } childRoutesPayload = &childRoutesList @@ -2101,28 +2091,63 @@ func toRoutePayload(ctx context.Context, routeTF *routeModel) (*observability.Up GroupBy: groupByPayload, GroupInterval: conversion.StringValueToPointer(routeTF.GroupInterval), GroupWait: conversion.StringValueToPointer(routeTF.GroupWait), - Match: matchPayload, - MatchRe: matchRegexPayload, Receiver: conversion.StringValueToPointer(routeTF.Receiver), RepeatInterval: conversion.StringValueToPointer(routeTF.RepeatInterval), Routes: childRoutesPayload, }, nil } -func toChildRoutePayload(in *observability.UpdateAlertConfigsPayloadRoute) *observability.UpdateAlertConfigsPayloadRouteRoutesInner { - if in == nil { - return nil +func toChildRoutePayload(ctx context.Context, routeTF *routeModelMiddle) (*observability.UpdateAlertConfigsPayloadRouteRoutesInner, error) { + if routeTF == nil { + return nil, fmt.Errorf("nil route model") } + + var groupByPayload, matchersPayload *[]string + var matchPayload, matchRegexPayload *map[string]interface{} + + if !utils.IsUndefined(routeTF.GroupBy) { + groupByPayload = &[]string{} + diags := routeTF.GroupBy.ElementsAs(ctx, groupByPayload, false) + if diags.HasError() { + return nil, fmt.Errorf("mapping group by: %w", core.DiagsToError(diags)) + } + } + + if !utils.IsUndefined(routeTF.Match) { + matchMap, err := conversion.ToStringInterfaceMap(ctx, routeTF.Match) + if err != nil { + return nil, fmt.Errorf("mapping match: %w", err) + } + matchPayload = &matchMap + } + + if !utils.IsUndefined(routeTF.MatchRegex) { + matchRegexMap, err := conversion.ToStringInterfaceMap(ctx, routeTF.MatchRegex) + if err != nil { + return nil, fmt.Errorf("mapping match regex: %w", err) + } + matchRegexPayload = &matchRegexMap + } + + if !utils.IsUndefined(routeTF.Matchers) { + matchersList, err := conversion.StringListToPointer(routeTF.Matchers) + if err != nil { + return nil, fmt.Errorf("mapping match regex: %w", err) + } + matchersPayload = matchersList + } + return &observability.UpdateAlertConfigsPayloadRouteRoutesInner{ - GroupBy: in.GroupBy, - GroupInterval: in.GroupInterval, - GroupWait: in.GroupWait, - Match: in.Match, - MatchRe: in.MatchRe, - Receiver: in.Receiver, - RepeatInterval: in.RepeatInterval, + GroupBy: groupByPayload, + GroupInterval: conversion.StringValueToPointer(routeTF.GroupInterval), + GroupWait: conversion.StringValueToPointer(routeTF.GroupWait), + Match: matchPayload, + MatchRe: matchRegexPayload, + Matchers: matchersPayload, + Receiver: conversion.StringValueToPointer(routeTF.Receiver), + RepeatInterval: conversion.StringValueToPointer(routeTF.RepeatInterval), // Routes not currently supported - } + }, nil } func toGlobalConfigPayload(ctx context.Context, model *alertConfigModel) (*observability.UpdateAlertConfigsPayloadGlobal, error) { diff --git a/stackit/internal/services/observability/instance/resource_test.go b/stackit/internal/services/observability/instance/resource_test.go index c83d5876..963af516 100644 --- a/stackit/internal/services/observability/instance/resource_test.go +++ b/stackit/internal/services/observability/instance/resource_test.go @@ -68,8 +68,6 @@ func fixtureRouteModel() basetypes.ObjectValue { }), "group_interval": types.StringValue("1m"), "group_wait": types.StringValue("1m"), - "match": types.MapValueMust(types.StringType, map[string]attr.Value{"key": types.StringValue("value")}), - "match_regex": types.MapValueMust(types.StringType, map[string]attr.Value{"key": types.StringValue("value")}), "receiver": types.StringValue("name"), "repeat_interval": types.StringValue("1m"), // "routes": types.ListNull(getRouteListType()), @@ -79,10 +77,14 @@ func fixtureRouteModel() basetypes.ObjectValue { types.StringValue("label1"), types.StringValue("label2"), }), - "group_interval": types.StringValue("1m"), - "group_wait": types.StringValue("1m"), - "match": types.MapValueMust(types.StringType, map[string]attr.Value{"key": types.StringValue("value")}), - "match_regex": types.MapValueMust(types.StringType, map[string]attr.Value{"key": types.StringValue("value")}), + "group_interval": types.StringValue("1m"), + "group_wait": types.StringValue("1m"), + "match": types.MapValueMust(types.StringType, map[string]attr.Value{"key": types.StringValue("value")}), + "match_regex": types.MapValueMust(types.StringType, map[string]attr.Value{"key": types.StringValue("value")}), + "matchers": types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("matcher1"), + types.StringValue("matcher2"), + }), "receiver": types.StringValue("name"), "repeat_interval": types.StringValue("1m"), }), @@ -95,8 +97,6 @@ func fixtureNullRouteModel() basetypes.ObjectValue { "group_by": types.ListNull(types.StringType), "group_interval": types.StringNull(), "group_wait": types.StringNull(), - "match": types.MapNull(types.StringType), - "match_regex": types.MapNull(types.StringType), "receiver": types.StringNull(), "repeat_interval": types.StringNull(), "routes": types.ListNull(getRouteListType()), @@ -171,8 +171,6 @@ func fixtureRoutePayload() *observability.UpdateAlertConfigsPayloadRoute { GroupBy: utils.Ptr([]string{"label1", "label2"}), GroupInterval: utils.Ptr("1m"), GroupWait: utils.Ptr("1m"), - Match: &map[string]interface{}{"key": "value"}, - MatchRe: &map[string]interface{}{"key": "value"}, Receiver: utils.Ptr("name"), RepeatInterval: utils.Ptr("1m"), Routes: &[]observability.UpdateAlertConfigsPayloadRouteRoutesInner{ @@ -182,6 +180,7 @@ func fixtureRoutePayload() *observability.UpdateAlertConfigsPayloadRoute { GroupWait: utils.Ptr("1m"), Match: &map[string]interface{}{"key": "value"}, MatchRe: &map[string]interface{}{"key": "value"}, + Matchers: &[]string{"matcher1", "matcher2"}, Receiver: utils.Ptr("name"), RepeatInterval: utils.Ptr("1m"), }, @@ -246,6 +245,7 @@ func fixtureRouteResponse() *observability.Route { GroupWait: utils.Ptr("1m"), Match: &map[string]string{"key": "value"}, MatchRe: &map[string]string{"key": "value"}, + Matchers: &[]string{"matcher1", "matcher2"}, Receiver: utils.Ptr("name"), RepeatInterval: utils.Ptr("1m"), Routes: &[]observability.RouteSerializer{ @@ -255,6 +255,7 @@ func fixtureRouteResponse() *observability.Route { GroupWait: utils.Ptr("1m"), Match: &map[string]string{"key": "value"}, MatchRe: &map[string]string{"key": "value"}, + Matchers: &[]string{"matcher1", "matcher2"}, Receiver: utils.Ptr("name"), RepeatInterval: utils.Ptr("1m"), }, @@ -300,13 +301,21 @@ func fixtureRouteAttributeSchema(route *schema.ListNestedAttribute, isDatasource }, }, "match": schema.MapAttribute{ - Description: routeDescriptions["match"], - Optional: !isDatasource, - Computed: isDatasource, - ElementType: types.StringType, + Description: routeDescriptions["match"], + DeprecationMessage: "Use `matchers` in the `routes` instead.", + Optional: !isDatasource, + Computed: isDatasource, + ElementType: types.StringType, }, "match_regex": schema.MapAttribute{ - Description: routeDescriptions["match_regex"], + Description: routeDescriptions["match_regex"], + DeprecationMessage: "Use `matchers` in the `routes` instead.", + Optional: !isDatasource, + Computed: isDatasource, + ElementType: types.StringType, + }, + "matchers": schema.ListAttribute{ + Description: routeDescriptions["matchers"], Optional: !isDatasource, Computed: isDatasource, ElementType: types.StringType, @@ -1484,6 +1493,7 @@ func TestGetRouteListTypeAux(t *testing.T) { "group_wait": types.StringType, "match": types.MapType{ElemType: types.StringType}, "match_regex": types.MapType{ElemType: types.StringType}, + "matchers": types.ListType{ElemType: types.StringType}, "receiver": types.StringType, "repeat_interval": types.StringType, }, @@ -1500,6 +1510,7 @@ func TestGetRouteListTypeAux(t *testing.T) { "group_wait": types.StringType, "match": types.MapType{ElemType: types.StringType}, "match_regex": types.MapType{ElemType: types.StringType}, + "matchers": types.ListType{ElemType: types.StringType}, "receiver": types.StringType, "repeat_interval": types.StringType, "routes": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{ @@ -1508,6 +1519,7 @@ func TestGetRouteListTypeAux(t *testing.T) { "group_wait": types.StringType, "match": types.MapType{ElemType: types.StringType}, "match_regex": types.MapType{ElemType: types.StringType}, + "matchers": types.ListType{ElemType: types.StringType}, "receiver": types.StringType, "repeat_interval": types.StringType, }}}, @@ -1525,6 +1537,7 @@ func TestGetRouteListTypeAux(t *testing.T) { "group_wait": types.StringType, "match": types.MapType{ElemType: types.StringType}, "match_regex": types.MapType{ElemType: types.StringType}, + "matchers": types.ListType{ElemType: types.StringType}, "receiver": types.StringType, "repeat_interval": types.StringType, }, diff --git a/stackit/internal/services/observability/observability_acc_test.go b/stackit/internal/services/observability/observability_acc_test.go index ef27a80b..caa52b36 100644 --- a/stackit/internal/services/observability/observability_acc_test.go +++ b/stackit/internal/services/observability/observability_acc_test.go @@ -100,6 +100,7 @@ var testConfigVarsMax = config.Variables{ "smtp_smart_host": config.StringVariable("smtp.gmail.com:587"), "match": config.StringVariable("alert1"), "match_regex": config.StringVariable("alert1"), + "matchers": config.StringVariable("instance =~ \".*\""), // logalertgroup "logalertgroup_for_time": config.StringVariable("60s"), "logalertgroup_label": config.StringVariable("label1"), @@ -134,6 +135,7 @@ func configVarsMaxUpdated() config.Variables { tempConfig["webhook_configs_url"] = config.StringVariable("https://chat.googleapis.com/api") tempConfig["ms_teams"] = config.StringVariable("false") tempConfig["google_chat"] = config.StringVariable("true") + tempConfig["matchers"] = config.StringVariable("instance =~ \"my.*\"") return tempConfig } @@ -529,6 +531,8 @@ func TestAccResourceMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.repeat_interval", testutil.ConvertConfigVariable(testConfigVarsMax["repeat_interval"])), resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.match.match1", testutil.ConvertConfigVariable(testConfigVarsMax["match"])), resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.match_regex.match_regex1", testutil.ConvertConfigVariable(testConfigVarsMax["match_regex"])), + resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.matchers.0", testutil.ConvertConfigVariable(testConfigVarsMax["matchers"])), + resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.matchers.#", "1"), resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.opsgenie_api_key", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_key"])), resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.opsgenie_api_url", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_url"])), @@ -692,6 +696,8 @@ func TestAccResourceMax(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.routes.0.repeat_interval", testutil.ConvertConfigVariable(testConfigVarsMax["repeat_interval"])), resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.routes.0.match.match1", testutil.ConvertConfigVariable(testConfigVarsMax["match"])), resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.routes.0.match_regex.match_regex1", testutil.ConvertConfigVariable(testConfigVarsMax["match_regex"])), + resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.routes.0.matchers.0", testutil.ConvertConfigVariable(testConfigVarsMax["matchers"])), + resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.routes.0.matchers.#", "1"), resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.global.opsgenie_api_key", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_key"])), resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.global.opsgenie_api_url", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_url"])), @@ -916,6 +922,8 @@ func TestAccResourceMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.repeat_interval", testutil.ConvertConfigVariable(testConfigVarsMax["repeat_interval"])), resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.match.match1", testutil.ConvertConfigVariable(testConfigVarsMax["match"])), resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.match_regex.match_regex1", testutil.ConvertConfigVariable(testConfigVarsMax["match_regex"])), + resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.matchers.0", testutil.ConvertConfigVariable(configVarsMaxUpdated()["matchers"])), + resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.matchers.#", "1"), resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.opsgenie_api_key", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_key"])), resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.opsgenie_api_url", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_url"])), diff --git a/stackit/internal/services/observability/testdata/resource-max.tf b/stackit/internal/services/observability/testdata/resource-max.tf index 59f91d71..91cb513b 100644 --- a/stackit/internal/services/observability/testdata/resource-max.tf +++ b/stackit/internal/services/observability/testdata/resource-max.tf @@ -42,6 +42,7 @@ variable "smtp_from" {} variable "smtp_smart_host" {} variable "match" {} variable "match_regex" {} +variable "matchers" {} variable "logalertgroup_name" {} variable "logalertgroup_alert" {} @@ -151,6 +152,9 @@ resource "stackit_observability_instance" "instance" { match_regex = { match_regex1 = var.match_regex } + matchers = [ + var.matchers + ] } ] }, diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index 45660fa7..25576f36 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -556,7 +556,10 @@ func ConvertConfigVariable(variable config.Variable) string { tmpByteArray, _ := variable.MarshalJSON() // In case the variable is a string, the quotes should be removed if tmpByteArray[0] == '"' && tmpByteArray[len(tmpByteArray)-1] == '"' { - return string(tmpByteArray[1 : len(tmpByteArray)-1]) + result := string(tmpByteArray[1 : len(tmpByteArray)-1]) + // Replace escaped quotes which where added MarshalJSON + rawString := strings.ReplaceAll(result, `\"`, `"`) + return rawString } return string(tmpByteArray) } diff --git a/stackit/internal/testutil/testutil_test.go b/stackit/internal/testutil/testutil_test.go index 23d77ce4..e92a718a 100644 --- a/stackit/internal/testutil/testutil_test.go +++ b/stackit/internal/testutil/testutil_test.go @@ -32,6 +32,11 @@ func TestConvertConfigVariable(t *testing.T) { variable: config.IntegerVariable(10), want: "10", }, + { + name: "quoted string", + variable: config.StringVariable(`instance =~ ".*"`), + want: `instance =~ ".*"`, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {