diff --git a/docs/data-sources/observability_alertgroup.md b/docs/data-sources/observability_alertgroup.md index 9fa930a67..8432520c7 100644 --- a/docs/data-sources/observability_alertgroup.md +++ b/docs/data-sources/observability_alertgroup.md @@ -45,3 +45,4 @@ Read-Only: - `expression` (String) The PromQL expression to evaluate. Every evaluation cycle this is evaluated at the current time, and all resultant time series become pending/firing alerts. - `for` (String) Alerts are considered firing once they have been returned for this long. Alerts which have not yet fired for long enough are considered pending. Default is 0s - `labels` (Map of String) A map of key:value. Labels to add or overwrite for each alert +- `record` (String) The name of the metric. It's the identifier and must be unique in the group. diff --git a/docs/resources/observability_alertgroup.md b/docs/resources/observability_alertgroup.md index 0502ea640..771e9a026 100644 --- a/docs/resources/observability_alertgroup.md +++ b/docs/resources/observability_alertgroup.md @@ -32,16 +32,11 @@ resource "stackit_observability_alertgroup" "example" { } }, { - alert = "example-alert-name-2" expression = "kube_node_status_condition{condition=\"Ready\", status=\"false\"} > 0" - for = "1m" labels = { severity = "critical" }, - annotations = { - summary : "example summary" - description : "example description" - } + record = "example_record_name" }, ] } @@ -76,11 +71,12 @@ import { Required: -- `alert` (String) The name of the alert rule. Is the identifier and must be unique in the group. - `expression` (String) The PromQL expression to evaluate. Every evaluation cycle this is evaluated at the current time, and all resultant time series become pending/firing alerts. Optional: +- `alert` (String) The name of the alert rule. Is the identifier and must be unique in the group. - `annotations` (Map of String) A map of key:value. Annotations to add or overwrite for each alert - `for` (String) Alerts are considered firing once they have been returned for this long. Alerts which have not yet fired for long enough are considered pending. Default is 0s - `labels` (Map of String) A map of key:value. Labels to add or overwrite for each alert +- `record` (String) The name of the metric. It's the identifier and must be unique in the group. diff --git a/examples/resources/stackit_observability_alertgroup/resource.tf b/examples/resources/stackit_observability_alertgroup/resource.tf index b4ab9cf35..461618237 100644 --- a/examples/resources/stackit_observability_alertgroup/resource.tf +++ b/examples/resources/stackit_observability_alertgroup/resource.tf @@ -17,16 +17,11 @@ resource "stackit_observability_alertgroup" "example" { } }, { - alert = "example-alert-name-2" expression = "kube_node_status_condition{condition=\"Ready\", status=\"false\"} > 0" - for = "1m" labels = { severity = "critical" }, - annotations = { - summary : "example summary" - description : "example description" - } + record = "example_record_name" }, ] } diff --git a/stackit/internal/services/observability/alertgroup/datasource.go b/stackit/internal/services/observability/alertgroup/datasource.go index 998151e5d..11a575191 100644 --- a/stackit/internal/services/observability/alertgroup/datasource.go +++ b/stackit/internal/services/observability/alertgroup/datasource.go @@ -123,6 +123,10 @@ func (a *alertGroupDataSource) Schema(_ context.Context, _ datasource.SchemaRequ ElementType: types.StringType, Computed: true, }, + "record": schema.StringAttribute{ + Description: descriptions["record"], + Computed: true, + }, }, }, }, diff --git a/stackit/internal/services/observability/alertgroup/resource.go b/stackit/internal/services/observability/alertgroup/resource.go index e91d9f680..953c55492 100644 --- a/stackit/internal/services/observability/alertgroup/resource.go +++ b/stackit/internal/services/observability/alertgroup/resource.go @@ -53,6 +53,7 @@ type rule struct { Labels types.Map `tfsdk:"labels"` Expression types.String `tfsdk:"expression"` For types.String `tfsdk:"for"` + Record types.String `tfsdk:"record"` } var ruleTypes = map[string]attr.Type{ @@ -61,6 +62,7 @@ var ruleTypes = map[string]attr.Type{ "labels": basetypes.MapType{ElemType: types.StringType}, "expression": basetypes.StringType{}, "for": basetypes.StringType{}, + "record": basetypes.StringType{}, } // Descriptions for the resource and data source schemas are centralized here. @@ -75,6 +77,7 @@ var descriptions = map[string]string{ "for": "Alerts are considered firing once they have been returned for this long. Alerts which have not yet fired for long enough are considered pending. Default is 0s", "labels": "A map of key:value. Labels to add or overwrite for each alert", "annotations": "A map of key:value. Annotations to add or overwrite for each alert", + "record": "The name of the metric. It's the identifier and must be unique in the group.", } // NewAlertGroupResource is a helper function to simplify the provider implementation. @@ -107,6 +110,40 @@ func (a *alertGroupResource) Configure(ctx context.Context, req resource.Configu tflog.Info(ctx, "Observability alert group client configured") } +func (a *alertGroupResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var resourceModel Model + resp.Diagnostics.Append(req.Config.Get(ctx, &resourceModel)...) + if resp.Diagnostics.HasError() { + return + } + + rules := &[]rule{} + diags := resourceModel.Rules.ElementsAs(ctx, rules, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // check every rule if the requirements are met, if one fails the whole config fails + rs := *rules + for i := range rs { + // either `alert` or `record` is needed + if rs[i].Alert.IsNull() && rs[i].Record.IsNull() { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring alertgroup", "You need to provide either `alert` or `record` for a `rule`.") + } + + // both are set, only one is allowed + if !rs[i].Alert.IsNull() && !rs[i].Record.IsNull() { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring alertgroup", "Both `alert` and `record` were set for a`rule`. Only one is allowed.") + } + + // if record is set, `annotations` and `for` are not allowed + if (!rs[i].Record.IsNull() && !rs[i].Record.IsUnknown()) && ((!rs[i].Annotations.IsNull() && !rs[i].Annotations.IsUnknown()) || (!rs[i].For.IsNull() && !rs[i].For.IsUnknown())) { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring alertgroup", "Setting either `annotations` or `for` when using `record` is not allowed.") + } + } +} + // Schema defines the schema for the resource. func (a *alertGroupResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ @@ -174,7 +211,7 @@ func (a *alertGroupResource) Schema(_ context.Context, _ resource.SchemaRequest, Attributes: map[string]schema.Attribute{ "alert": schema.StringAttribute{ Description: descriptions["alert"], - Required: true, + Optional: true, Validators: []validator.String{ stringvalidator.RegexMatches( regexp.MustCompile(`^[a-zA-Z0-9-]+$`), @@ -222,6 +259,17 @@ func (a *alertGroupResource) Schema(_ context.Context, _ resource.SchemaRequest, mapvalidator.SizeAtMost(5), }, }, + "record": schema.StringAttribute{ + Description: descriptions["record"], + Optional: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile(`^[a-zA-Z0-9:_]+$`), + "must match expression", + ), + stringvalidator.LengthBetween(1, 300), + }, + }, }, }, }, @@ -468,6 +516,14 @@ func toRulesPayload(ctx context.Context, model *Model) ([]observability.UpdateAl oarr.Annotations = &annotations } + if !utils.IsUndefined(rule.Record) { + record := conversion.StringValueToPointer(rule.Record) + if record == nil { + return nil, fmt.Errorf("found nil record for rule[%d]", i) + } + oarr.Record = record + } + oarrs = append(oarrs, oarr) } @@ -539,6 +595,7 @@ func mapRules(_ context.Context, alertGroup *observability.AlertGroup, model *Mo "for": types.StringPointerValue(r.For), "labels": types.MapNull(types.StringType), "annotations": types.MapNull(types.StringType), + "record": types.StringPointerValue(r.Record), } if r.Labels != nil { diff --git a/stackit/internal/services/observability/alertgroup/resource_test.go b/stackit/internal/services/observability/alertgroup/resource_test.go index 746974212..a9abc4501 100644 --- a/stackit/internal/services/observability/alertgroup/resource_test.go +++ b/stackit/internal/services/observability/alertgroup/resource_test.go @@ -72,6 +72,7 @@ func TestToCreatePayload(t *testing.T) { "k": types.StringValue("v"), }, ), + "record": types.StringValue("record"), }, ), }, @@ -91,6 +92,7 @@ func TestToCreatePayload(t *testing.T) { Labels: &map[string]interface{}{ "k": "v", }, + Record: utils.Ptr("record"), }, }, }, @@ -153,6 +155,7 @@ func TestToRulesPayload(t *testing.T) { "annotations": types.MapValueMust(types.StringType, map[string]attr.Value{ "note": types.StringValue("important"), }), + "record": types.StringValue("record"), }), }), }, @@ -167,6 +170,7 @@ func TestToRulesPayload(t *testing.T) { Annotations: &map[string]interface{}{ "note": "important", }, + Record: utils.Ptr("record"), }, }, expectErr: false, @@ -181,6 +185,7 @@ func TestToRulesPayload(t *testing.T) { "for": types.StringValue("5s"), "labels": types.MapNull(types.StringType), "annotations": types.MapNull(types.StringType), + "record": types.StringValue("record1"), }), types.ObjectValueMust(ruleTypes, map[string]attr.Value{ "alert": types.StringValue("alert2"), @@ -192,14 +197,16 @@ func TestToRulesPayload(t *testing.T) { "annotations": types.MapValueMust(types.StringType, map[string]attr.Value{ "note": types.StringValue("important"), }), + "record": types.StringValue("record2"), }), }), }, expect: []observability.UpdateAlertgroupsRequestInnerRulesInner{ { - Alert: utils.Ptr("alert1"), - Expr: utils.Ptr("expr1"), - For: utils.Ptr("5s"), + Alert: utils.Ptr("alert1"), + Expr: utils.Ptr("expr1"), + For: utils.Ptr("5s"), + Record: utils.Ptr("record1"), }, { Alert: utils.Ptr("alert2"), @@ -211,6 +218,7 @@ func TestToRulesPayload(t *testing.T) { Annotations: &map[string]interface{}{ "note": "important", }, + Record: utils.Ptr("record2"), }, }, expectErr: false, diff --git a/stackit/internal/services/observability/observability_acc_test.go b/stackit/internal/services/observability/observability_acc_test.go index 4b8fac383..8971f5c78 100644 --- a/stackit/internal/services/observability/observability_acc_test.go +++ b/stackit/internal/services/observability/observability_acc_test.go @@ -39,6 +39,7 @@ var testConfigVarsMin = config.Variables{ "alertgroup_name": config.StringVariable(fmt.Sprintf("tf-acc-ag%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), "alert_rule_name": config.StringVariable("alert1"), "alert_rule_expression": config.StringVariable(alert_rule_expression), + "record_rule_name": config.StringVariable("record1"), "instance_name": config.StringVariable(fmt.Sprintf("tf-acc-i%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), "plan_name": config.StringVariable("Observability-Medium-EU01"), "logalertgroup_name": config.StringVariable(fmt.Sprintf("tf-acc-lag%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), @@ -55,6 +56,7 @@ var testConfigVarsMax = config.Variables{ "alertgroup_name": config.StringVariable(fmt.Sprintf("tf-acc-ag%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), "alert_rule_name": config.StringVariable("alert1"), "alert_rule_expression": config.StringVariable(alert_rule_expression), + "record_rule_name": config.StringVariable("record1"), "instance_name": config.StringVariable(fmt.Sprintf("tf-acc-i%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), "plan_name": config.StringVariable("Observability-Medium-EU01"), "logalertgroup_name": config.StringVariable(fmt.Sprintf("tf-acc-lag%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), @@ -215,6 +217,8 @@ func TestAccResourceMin(t *testing.T) { resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "name", testutil.ConvertConfigVariable(testConfigVarsMin["alertgroup_name"])), resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.alert", testutil.ConvertConfigVariable(testConfigVarsMin["alert_rule_name"])), resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.expression", alert_rule_expression), + resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.1.record", testutil.ConvertConfigVariable(testConfigVarsMin["record_rule_name"])), + resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.1.expression", alert_rule_expression), // logalertgroup resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), @@ -302,6 +306,8 @@ func TestAccResourceMin(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "name", testutil.ConvertConfigVariable(testConfigVarsMin["alertgroup_name"])), resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "rules.0.alert", testutil.ConvertConfigVariable(testConfigVarsMin["alert_rule_name"])), resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "rules.0.expression", alert_rule_expression), + resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "rules.1.record", testutil.ConvertConfigVariable(testConfigVarsMin["record_rule_name"])), + resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "rules.1.expression", alert_rule_expression), // logalertgroup resource.TestCheckResourceAttr("data.stackit_observability_logalertgroup.logalertgroup", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), @@ -460,6 +466,8 @@ func TestAccResourceMin(t *testing.T) { resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "name", testutil.ConvertConfigVariable(testConfigVarsMin["alertgroup_name"])), resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.alert", testutil.ConvertConfigVariable(configVarsMinUpdated()["alert_rule_name"])), resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.expression", alert_rule_expression), + resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.1.record", testutil.ConvertConfigVariable(configVarsMinUpdated()["record_rule_name"])), + resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.1.expression", alert_rule_expression), // logalertgroup resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), @@ -606,6 +614,9 @@ func TestAccResourceMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.expression", alert_rule_expression), resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.for", testutil.ConvertConfigVariable(testConfigVarsMax["alert_for_time"])), resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.labels.label1", testutil.ConvertConfigVariable(testConfigVarsMax["alert_label"])), + resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.1.record", testutil.ConvertConfigVariable(testConfigVarsMax["record_rule_name"])), + resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.1.expression", alert_rule_expression), + resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.1.labels.label1", testutil.ConvertConfigVariable(testConfigVarsMax["alert_label"])), resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.annotations.annotation1", testutil.ConvertConfigVariable(testConfigVarsMax["alert_annotation"])), resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "interval", testutil.ConvertConfigVariable(testConfigVarsMax["alert_interval"])), @@ -776,6 +787,9 @@ func TestAccResourceMax(t *testing.T) { resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "rules.0.labels.label1", testutil.ConvertConfigVariable(testConfigVarsMax["alert_label"])), resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "rules.0.annotations.annotation1", testutil.ConvertConfigVariable(testConfigVarsMax["alert_annotation"])), resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "interval", testutil.ConvertConfigVariable(testConfigVarsMax["alert_interval"])), + resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "rules.1.record", testutil.ConvertConfigVariable(testConfigVarsMax["record_rule_name"])), + resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "rules.1.expression", alert_rule_expression), + resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "rules.1.labels.label1", testutil.ConvertConfigVariable(testConfigVarsMax["alert_label"])), // logalertgroup resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), @@ -1006,6 +1020,9 @@ func TestAccResourceMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.expression", alert_rule_expression_updated), resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.for", testutil.ConvertConfigVariable(testConfigVarsMax["alert_for_time"])), resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.labels.label1", testutil.ConvertConfigVariable(testConfigVarsMax["alert_label"])), + resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.1.record", testutil.ConvertConfigVariable(testConfigVarsMax["record_rule_name"])), + resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.1.expression", alert_rule_expression_updated), + resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.1.labels.label1", testutil.ConvertConfigVariable(testConfigVarsMax["alert_label"])), resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.annotations.annotation1", testutil.ConvertConfigVariable(testConfigVarsMax["alert_annotation"])), resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "interval", testutil.ConvertConfigVariable(configVarsMaxUpdated()["alert_interval"])), diff --git a/stackit/internal/services/observability/testdata/resource-max.tf b/stackit/internal/services/observability/testdata/resource-max.tf index e943d2dbc..d7a4fd3d6 100644 --- a/stackit/internal/services/observability/testdata/resource-max.tf +++ b/stackit/internal/services/observability/testdata/resource-max.tf @@ -8,6 +8,7 @@ variable "alert_for_time" {} variable "alert_label" {} variable "alert_annotation" {} variable "alert_interval" {} +variable "record_rule_name" {} variable "instance_name" {} variable "plan_name" {} @@ -89,6 +90,13 @@ resource "stackit_observability_alertgroup" "alertgroup" { annotations = { annotation1 = var.alert_annotation } + }, + { + record = var.record_rule_name + expression = var.alert_rule_expression + labels = { + label1 = var.alert_label + } } ] interval = var.alert_interval diff --git a/stackit/internal/services/observability/testdata/resource-min.tf b/stackit/internal/services/observability/testdata/resource-min.tf index ecaa3b7ae..a90bc97a2 100644 --- a/stackit/internal/services/observability/testdata/resource-min.tf +++ b/stackit/internal/services/observability/testdata/resource-min.tf @@ -4,6 +4,7 @@ variable "project_id" {} variable "alertgroup_name" {} variable "alert_rule_name" {} variable "alert_rule_expression" {} +variable "record_rule_name" {} variable "instance_name" {} variable "plan_name" {} @@ -27,6 +28,10 @@ resource "stackit_observability_alertgroup" "alertgroup" { { alert = var.alert_rule_name expression = var.alert_rule_expression + }, + { + record = var.record_rule_name + expression = var.alert_rule_expression } ] }