diff --git a/docker-compose.yml b/docker-compose.yml index 21effba90..b49a2826b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,15 @@ services: - MYSQL_USER=${MYSQL_USER:-grafana} - MYSQL_PASSWORD=${MYSQL_PASSWORD:-grafana} healthcheck: - test: ["CMD", "mysqladmin", "ping", "-p$$MYSQL_ROOT_PASSWORD", "--protocol", "tcp"] + test: + [ + "CMD", + "mysqladmin", + "ping", + "-p$$MYSQL_ROOT_PASSWORD", + "--protocol", + "tcp", + ] interval: 10s retries: 10 start_period: 10s @@ -31,7 +39,7 @@ services: - GF_SERVER_ROOT_URL=${GRAFANA_URL} - GF_ENTERPRISE_LICENSE_TEXT=${GF_ENTERPRISE_LICENSE_TEXT:-} - GF_SERVER_SERVE_FROM_SUB_PATH=${GF_SERVER_SERVE_FROM_SUB_PATH:-} - - GF_FEATURE_TOGGLES_ENABLE=nestedFolders,ssoSettingsApi,ssoSettingsSAML,ssoSettingsLDAP,grafanaManagedRecordingRulesDatasources,enableSCIM,alertEnrichmentMultiStep,alertEnrichmentConditional + - GF_FEATURE_TOGGLES_ENABLE=nestedFolders,ssoSettingsApi,ssoSettingsSAML,ssoSettingsLDAP,grafanaManagedRecordingRulesDatasources,enableSCIM,alertEnrichmentMultiStep,alertEnrichmentConditional,alertingEnrichmentAssistantInvestigations healthcheck: test: wget --no-verbose --tries=1 --spider http://0.0.0.0:3000/api/health || exit 1 # Use wget because older versions of Grafana don't have curl interval: 10s diff --git a/examples/resources/grafana_apps_alertenrichment_alertenrichment_v1beta1/resource.tf b/examples/resources/grafana_apps_alertenrichment_alertenrichment_v1beta1/resource.tf index 4659d5191..1f44c5f46 100644 --- a/examples/resources/grafana_apps_alertenrichment_alertenrichment_v1beta1/resource.tf +++ b/examples/resources/grafana_apps_alertenrichment_alertenrichment_v1beta1/resource.tf @@ -106,5 +106,46 @@ resource "grafana_apps_alertenrichment_alertenrichment_v1beta1" "enrichment" { step { assistant_investigations {} } + + # Conditional step runs different actions based on alert severity + step { + conditional { + # Condition: Check if severity is critical + if { + label_matchers = [{ + type = "=" + name = "severity" + value = "critical" + }] + } + + # Actions for critical alerts + then { + step { + assign { + annotations = { + escalation_level = "immediate" + } + } + } + step { + external { + url = "https://irm.grafana.com/create-incident" + } + } + } + + # Actions for non-critical alerts + else { + step { + assign { + annotations = { + escalation_level = "standard" + } + } + } + } + } + } } } diff --git a/internal/resources/appplatform/alertenrichment_resource.go b/internal/resources/appplatform/alertenrichment_resource.go index 0a2773f7e..1697c8053 100644 --- a/internal/resources/appplatform/alertenrichment_resource.go +++ b/internal/resources/appplatform/alertenrichment_resource.go @@ -87,6 +87,22 @@ var ( "request": jsontypes.NormalizedType{}, }, } + + // dataSourceConditionType is the Terraform type for the data source condition block + dataSourceConditionType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "request": jsontypes.NormalizedType{}, + }, + } + + // conditionType is the Terraform type for the conditional 'if' block + conditionType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "label_matchers": types.ListType{ElemType: matcherType}, + "annotation_matchers": types.ListType{ElemType: matcherType}, + "data_source_condition": dataSourceConditionType, + }, + } ) // assignStepModel represents the Terraform model for an assign enrichment step @@ -137,6 +153,31 @@ type logsQueryModel struct { MaxLines types.Int64 `tfsdk:"max_lines"` } +// conditionalStepModel represents a conditional step with branches +type conditionalStepModel struct { + Timeout types.String `tfsdk:"timeout"` + If types.Object `tfsdk:"if"` + Then types.Object `tfsdk:"then"` + Else types.Object `tfsdk:"else"` +} + +// branchStepsModel wraps a steps list used inside conditional branches +type branchStepsModel struct { + Steps types.List `tfsdk:"step"` +} + +// conditionModel represents the condition used in a conditional step +type conditionModel struct { + LabelMatchers types.List `tfsdk:"label_matchers"` + AnnotationMatchers types.List `tfsdk:"annotation_matchers"` + DataSourceCondition types.Object `tfsdk:"data_source_condition"` +} + +// dataSourceconditionModel wraps a raw data source request used in conditions +type dataSourceconditionModel struct { + Request jsontypes.Normalized `tfsdk:"request"` +} + // rawQueryModel holds options for a raw data source query request type rawQueryModel struct { RefID types.String `tfsdk:"ref_id"` @@ -275,6 +316,15 @@ func (r *stepRegistry) BuildStepsList(ctx context.Context, steps []v1beta1.Step) name = stepName found = true break + } else if s.Type == v1beta1.StepTypeConditional && stepName == "conditional" { + var d diag.Diagnostics + obj, d = def.FromAPI(ctx, s) + if d.HasError() { + return types.ListNull(elemType), d + } + name = stepName + found = true + break } } @@ -304,7 +354,24 @@ func (r *stepRegistry) BuildStepsList(ctx context.Context, steps []v1beta1.Step) return types.ListValue(elemType, values) } -func initStepRegistry() *stepRegistry { +// Without returns a new stepRegistry with specified step types excluded +func (r *stepRegistry) Without(exclude ...string) *stepRegistry { + filtered := &stepRegistry{ + Definitions: make(map[string]*stepDefinition), + } + excludeSet := make(map[string]bool) + for _, e := range exclude { + excludeSet[e] = true + } + for k, v := range r.Definitions { + if !excludeSet[k] { + filtered.Definitions[k] = v + } + } + return filtered +} + +func initStepRegistryWithoutConditional() *stepRegistry { registry := &stepRegistry{ Definitions: make(map[string]*stepDefinition), } @@ -446,7 +513,95 @@ func initStepRegistry() *stepRegistry { return registry } -var registry = initStepRegistry() +func addConditionalStep(reg *stepRegistry) { + // Build schema for conditional branches + childBlocks := make(map[string]schema.Block) + for name, def := range reg.Definitions { + childBlocks[name] = def.Schema + } + + baseBranchBlock := schema.ListNestedBlock{ + Validators: []validator.List{stepExactlyOneBlockValidator{}}, + NestedObject: schema.NestedBlockObject{Blocks: childBlocks}, + } + + thenBranchSchema := schema.SingleNestedBlock{ + Description: "Steps when condition is true.", + Blocks: map[string]schema.Block{"step": baseBranchBlock}, + } + + elseBranchSchema := schema.SingleNestedBlock{ + Description: "Steps when condition is false.", + Blocks: map[string]schema.Block{"step": baseBranchBlock}, + } + + branchAttrTypes := map[string]attr.Type{ + "step": types.ListType{ElemType: types.ObjectType{AttrTypes: reg.BuildElementTypes()}}, + } + + condAttrTypes := map[string]attr.Type{ + "timeout": types.StringType, + "if": conditionType, + "then": types.ObjectType{AttrTypes: branchAttrTypes}, + "else": types.ObjectType{AttrTypes: branchAttrTypes}, + } + + // nested conditionals aren't supported, so we need the registry without them + // to build the nested steps blocks. + filtered := reg.Without("conditional") + + reg.Definitions["conditional"] = &stepDefinition{ + Schema: schema.SingleNestedBlock{ + Description: "Conditional step with if/then/else.", + Attributes: map[string]schema.Attribute{ + "timeout": schema.StringAttribute{Optional: true, Description: timeoutDescription}, + }, + Blocks: map[string]schema.Block{ + "if": schema.SingleNestedBlock{ + Description: "Condition to evaluate.", + Attributes: map[string]schema.Attribute{ + "label_matchers": schema.ListAttribute{Optional: true, ElementType: matcherType, Validators: []validator.List{matcherValidator{}}, Description: "Label matchers for the condition."}, + "annotation_matchers": schema.ListAttribute{Optional: true, ElementType: matcherType, Validators: []validator.List{matcherValidator{}}, Description: "Annotation matchers for the condition."}, + }, + Blocks: map[string]schema.Block{ + "data_source_condition": schema.SingleNestedBlock{ + Description: "Data source condition.", + Attributes: map[string]schema.Attribute{ + "request": schema.StringAttribute{Optional: true, CustomType: jsontypes.NormalizedType{}, Description: "Data source request payload."}, + }, + Validators: []validator.Object{requireAttrsWhenPresent("request")}, + }, + }, + Validators: []validator.Object{ + attributeCountExactly(1, "label_matchers", "annotation_matchers", "data_source_condition"), + }, + }, + "then": thenBranchSchema, + "else": elseBranchSchema, + }, + }, + AttrTypes: condAttrTypes, + ToAPI: func(ctx context.Context, obj types.Object) (v1beta1.Step, diag.Diagnostics) { + return encodeToAPI(ctx, obj, func(ctx context.Context, m conditionalStepModel) (v1beta1.Step, diag.Diagnostics) { + return conditionalStepToAPI(ctx, m, filtered) + }) + }, + FromAPI: func(ctx context.Context, step v1beta1.Step) (types.Object, diag.Diagnostics) { + model, d := conditionalStepFromAPI(ctx, step, filtered) + if d.HasError() { + return types.ObjectNull(condAttrTypes), d + } + obj, dd := types.ObjectValueFrom(ctx, condAttrTypes, model) + return obj, dd + }, + } +} + +var registry = func() *stepRegistry { + r := initStepRegistryWithoutConditional() + addConditionalStep(r) + return r +}() // stepsBlock defines the top-level `step` list block. func stepsBlock() map[string]schema.Block { @@ -773,6 +928,173 @@ func processRawQueryFromAPI(ctx context.Context, step v1beta1.Step, model *dataS return nil } +func conditionalStepToAPI(ctx context.Context, m conditionalStepModel, reg *stepRegistry) (v1beta1.Step, diag.Diagnostics) { + st := v1beta1.Step{Type: v1beta1.StepTypeConditional, Conditional: &v1beta1.Conditional{}} + if dd := setTimeout(&st, m.Timeout); dd.HasError() { + return v1beta1.Step{}, dd + } + cond, dd := parseCondition(ctx, m.If) + if dd.HasError() { + return v1beta1.Step{}, dd + } + st.Conditional.If = cond + + // Parse then branch + var thenBranch branchStepsModel + if d := m.Then.As(ctx, &thenBranch, objAsOpts); d.HasError() { + return v1beta1.Step{}, d + } + th, dd := reg.ParseStepsList(ctx, thenBranch.Steps) + if dd.HasError() { + return v1beta1.Step{}, dd + } + st.Conditional.Then = th + + // Parse else branch + var elseBranch branchStepsModel + if d := m.Else.As(ctx, &elseBranch, objAsOpts); d.HasError() { + return v1beta1.Step{}, d + } + el, dd := reg.ParseStepsList(ctx, elseBranch.Steps) + if dd.HasError() { + return v1beta1.Step{}, dd + } + st.Conditional.Else = el + + return st, nil +} + +func conditionalStepFromAPI(ctx context.Context, step v1beta1.Step, reg *stepRegistry) (conditionalStepModel, diag.Diagnostics) { + condModel, dd := convertConditionFromAPI(ctx, step.Conditional.If) + if dd.HasError() { + return conditionalStepModel{}, dd + } + ifObj, dd := types.ObjectValueFrom(ctx, conditionType.AttrTypes, condModel) + if dd.HasError() { + return conditionalStepModel{}, dd + } + // Build then branch + thList, dd := reg.BuildStepsList(ctx, step.Conditional.Then) + if dd.HasError() { + return conditionalStepModel{}, dd + } + thenObj, dd := types.ObjectValue( + map[string]attr.Type{"step": types.ListType{ElemType: types.ObjectType{AttrTypes: reg.BuildElementTypes()}}}, + map[string]attr.Value{"step": thList}, + ) + if dd.HasError() { + return conditionalStepModel{}, dd + } + + // Build else branch + elList, dd := reg.BuildStepsList(ctx, step.Conditional.Else) + if dd.HasError() { + return conditionalStepModel{}, dd + } + elseObj, dd := types.ObjectValue( + map[string]attr.Type{"step": types.ListType{ElemType: types.ObjectType{AttrTypes: reg.BuildElementTypes()}}}, + map[string]attr.Value{"step": elList}, + ) + if dd.HasError() { + return conditionalStepModel{}, dd + } + + return conditionalStepModel{ + Timeout: timeoutValueOrNull(step.Timeout.Duration), + If: ifObj, + Then: thenObj, + Else: elseObj, + }, nil +} + +func parseCondition(ctx context.Context, condObj types.Object) (v1beta1.Condition, diag.Diagnostics) { + var condModel conditionModel + if diag := condObj.As(ctx, &condModel, objAsOpts); diag.HasError() { + return v1beta1.Condition{}, diag + } + + result := v1beta1.Condition{} + + if !condModel.LabelMatchers.IsNull() && !condModel.LabelMatchers.IsUnknown() { + labelMatchers, diags := parseMatchers(ctx, condModel.LabelMatchers) + if diags.HasError() { + return v1beta1.Condition{}, diags + } + result.LabelMatchers = labelMatchers + } + + if !condModel.AnnotationMatchers.IsNull() && !condModel.AnnotationMatchers.IsUnknown() { + annotationMatchers, diags := parseMatchers(ctx, condModel.AnnotationMatchers) + if diags.HasError() { + return v1beta1.Condition{}, diags + } + result.AnnotationMatchers = annotationMatchers + } + + if !condModel.DataSourceCondition.IsNull() && !condModel.DataSourceCondition.IsUnknown() { + var dsCondModel dataSourceconditionModel + if diag := condModel.DataSourceCondition.As(ctx, &dsCondModel, objAsOpts); diag.HasError() { + return v1beta1.Condition{}, diag + } + + if !dsCondModel.Request.IsNull() && !dsCondModel.Request.IsUnknown() && dsCondModel.Request.ValueString() != "" { + var requestData commonapi.Unstructured + if d := dsCondModel.Request.Unmarshal(&requestData); d.HasError() { + return v1beta1.Condition{}, d + } + result.DataSourceQuery = &v1beta1.RawDataSourceQuery{Request: requestData} + } + } + + return result, nil +} + +func convertConditionFromAPI(ctx context.Context, condition v1beta1.Condition) (conditionModel, diag.Diagnostics) { + model := conditionModel{} + + if len(condition.LabelMatchers) > 0 { + labelMatchers, diags := convertMatchersToTf(ctx, condition.LabelMatchers) + if diags.HasError() { + return model, diags + } + model.LabelMatchers = labelMatchers + } else { + model.LabelMatchers = types.ListNull(matcherType) + } + + if len(condition.AnnotationMatchers) > 0 { + annotationMatchers, diags := convertMatchersToTf(ctx, condition.AnnotationMatchers) + if diags.HasError() { + return model, diags + } + model.AnnotationMatchers = annotationMatchers + } else { + model.AnnotationMatchers = types.ListNull(matcherType) + } + + if condition.DataSourceQuery != nil { + requestBytes, err := json.Marshal(&condition.DataSourceQuery.Request) + if err != nil { + return model, diag.Diagnostics{ + diag.NewErrorDiagnostic("invalid data source condition", fmt.Sprintf("failed to marshal request: %v", err)), + } + } + + dsconditionModel := dataSourceconditionModel{ + Request: jsontypes.NewNormalizedValue(string(requestBytes)), + } + dsConditionObj, diags := types.ObjectValueFrom(ctx, dataSourceConditionType.AttrTypes, dsconditionModel) + if diags.HasError() { + return model, diags + } + model.DataSourceCondition = dsConditionObj + } else { + model.DataSourceCondition = types.ObjectNull(dataSourceConditionType.AttrTypes) + } + + return model, nil +} + // AlertEnrichment creates a new Grafana Alert Enrichment resource func AlertEnrichment() NamedResource { return NewNamedResource[*v1beta1.AlertEnrichment, *v1beta1.AlertEnrichmentList]( diff --git a/internal/resources/appplatform/alertenrichment_resource_acc_test.go b/internal/resources/appplatform/alertenrichment_resource_acc_test.go index 3b42d2dc5..095b490c4 100644 --- a/internal/resources/appplatform/alertenrichment_resource_acc_test.go +++ b/internal/resources/appplatform/alertenrichment_resource_acc_test.go @@ -53,6 +53,7 @@ type alertEnrichmentConfigBuilder struct { explainStep *explainStepConfig siftStep *siftStepConfig dataSourceSteps []dataSourceStepConfig + conditionalStep *conditionalStepConfig } type matcherConfig struct { @@ -106,6 +107,10 @@ type rawQueryConfig struct { request string } +type conditionalStepConfig struct { + timeout string +} + func newAlertEnrichmentConfig(uid, title string) *alertEnrichmentConfigBuilder { return &alertEnrichmentConfigBuilder{ uid: uid, @@ -207,6 +212,13 @@ func (b *alertEnrichmentConfigBuilder) withDataSourceRawStep(timeout, refID, req return b } +func (b *alertEnrichmentConfigBuilder) withConditionalBasic(timeout string) *alertEnrichmentConfigBuilder { + b.conditionalStep = &conditionalStepConfig{ + timeout: timeout, + } + return b +} + func (b *alertEnrichmentConfigBuilder) build() string { config := b.buildHeader() config += b.buildArrayFields() @@ -297,6 +309,7 @@ func (b *alertEnrichmentConfigBuilder) buildSteps() string { config += b.buildExplainStep() config += b.buildSiftStep() config += b.buildDataSourceStep() + config += b.buildConditionalStep() return config } @@ -477,6 +490,54 @@ func (b *alertEnrichmentConfigBuilder) buildDataSourceStep() string { return config } +func (b *alertEnrichmentConfigBuilder) buildConditionalStep() string { + if b.conditionalStep == nil { + return "" + } + + timeout := b.conditionalStep.timeout + if timeout == "" { + timeout = "30s" + } + + config := fmt.Sprintf(` + step { + conditional { + timeout = "%s" + + if { + label_matchers = [{ + type = "=" + name = "severity" + value = "critical" + }] + } + + then { + step { + assign { + annotations = { + priority = "P1" + } + } + } + } + + else { + step { + assign { + annotations = { + priority = "P3" + } + } + } + } + } + }`, timeout) + + return config +} + func (b *alertEnrichmentConfigBuilder) buildFooter() string { return ` } @@ -523,6 +584,7 @@ func TestAccAlertEnrichment(t *testing.T) { withSiftStep("35s"). withDataSourceLogsStep("36s", "loki", "test-loki-uid", `{job=\"my-app\"} | json | level=\"error\"`, 5). withDataSourceRawStep("37s", "A", `"{\"datasource\":{\"type\":\"prometheus\",\"uid\":\"test-uid\"},\"expr\":\"up\",\"refId\":\"A\"}"`). + withConditionalBasic("38s"). build(), Check: terraformresource.ComposeTestCheckFunc( terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.title", "comprehensive-alert-enrichment"), @@ -532,7 +594,7 @@ func TestAccAlertEnrichment(t *testing.T) { terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.label_matchers.#", "2"), // Verify steps and ordering - terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.#", "9"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.#", "10"), // First assign step terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.assign.annotations.%", "4"), terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.assign.timeout", "30s"), @@ -561,6 +623,13 @@ func TestAccAlertEnrichment(t *testing.T) { terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.8.data_source.raw_query.ref_id", "A"), terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.8.data_source.raw_query.request", `{"datasource":{"type":"prometheus","uid":"test-uid"},"expr":"up","refId":"A"}`), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.9.conditional.timeout", "38s"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.9.conditional.if.label_matchers.#", "1"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.9.conditional.then.step.#", "1"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.9.conditional.then.step.0.assign.annotations.priority", "P1"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.9.conditional.else.step.#", "1"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.9.conditional.else.step.0.assign.annotations.priority", "P3"), + terraformresource.TestCheckResourceAttrSet(alertEnrichmentResourceName, "id"), ), }, @@ -1385,3 +1454,369 @@ resource "grafana_apps_alertenrichment_alertenrichment_v1beta1" "test" { } `, uid) } + +func TestAccAlertEnrichment_conditional(t *testing.T) { + testutils.CheckEnterpriseTestsEnabled(t, ">=12.2.0") + + uid := acctest.RandString(10) + + terraformresource.ParallelTest(t, terraformresource.TestCase{ + ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAlertEnrichmentResourceDestroy, + Steps: []terraformresource.TestStep{ + { + Config: testAccAlertEnrichmentConditionalBasic(uid), + Check: terraformresource.ComposeTestCheckFunc( + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "metadata.uid", uid), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.title", "Test Conditional Enrichment"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.#", "1"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.timeout", "45s"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.if.label_matchers.#", "1"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.if.label_matchers.0.type", "="), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.if.label_matchers.0.name", "severity"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.if.label_matchers.0.value", "critical"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.then.step.#", "1"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.then.step.0.assign.annotations.priority", "P1"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.else.step.#", "1"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.else.step.0.assign.annotations.priority", "P3"), + ), + }, + }, + }) +} + +func testAccAlertEnrichmentConditionalBasic(uid string) string { + return fmt.Sprintf(` +resource "grafana_apps_alertenrichment_alertenrichment_v1beta1" "test" { + metadata { + uid = "%s" + } + + spec { + title = "Test Conditional Enrichment" + + step { + conditional { + timeout = "45s" + + if { + label_matchers = [{ + type = "=" + name = "severity" + value = "critical" + }] + } + + then { + step { + assign { + annotations = { + priority = "P1" + } + } + } + } + + else { + step { + assign { + annotations = { + priority = "P3" + } + } + } + } + } + } + } +} +`, uid) +} + +func TestAccAlertEnrichment_conditionalWithDataSourceCondition(t *testing.T) { + testutils.CheckEnterpriseTestsEnabled(t, ">=12.2.0") + + uid := acctest.RandString(10) + + terraformresource.ParallelTest(t, terraformresource.TestCase{ + ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAlertEnrichmentResourceDestroy, + Steps: []terraformresource.TestStep{ + { + Config: testAccAlertEnrichmentConditionalWithDataSource(uid), + Check: terraformresource.ComposeTestCheckFunc( + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "metadata.uid", uid), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.title", "Test Conditional With Data Source"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.#", "1"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.if.data_source_condition.request", `{"datasource":{"type":"prometheus","uid":"test-uid"},"expr":"up == 0","refId":"A"}`), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.then.step.#", "1"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.then.step.0.assign.annotations.severity", "critical"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.else.step.#", "1"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.else.step.0.assign.annotations.severity", "warning"), + ), + }, + { + ResourceName: alertEnrichmentResourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "options.%", + "options.overwrite", + }, + ImportStateIdFunc: importStateIDFunc(alertEnrichmentResourceName), + }, + }, + }) +} + +func testAccAlertEnrichmentConditionalWithDataSource(uid string) string { + return fmt.Sprintf(` +resource "grafana_apps_alertenrichment_alertenrichment_v1beta1" "test" { + metadata { + uid = "%s" + } + + spec { + title = "Test Conditional With Data Source" + description = "Conditional with data source request" + + step { + conditional { + if { + data_source_condition { + request = jsonencode({ + datasource = { + type = "prometheus" + uid = "test-uid" + } + expr = "up == 0" + refId = "A" + }) + } + } + + then { + step { + assign { + annotations = { + severity = "critical" + } + } + } + } + + else { + step { + assign { + annotations = { + severity = "warning" + } + } + } + } + } + } + } +} +`, uid) +} + +func TestAccAlertEnrichment_conditionalWithAnnotationMatchers(t *testing.T) { + testutils.CheckEnterpriseTestsEnabled(t, ">=12.2.0") + + uid := acctest.RandString(10) + + terraformresource.ParallelTest(t, terraformresource.TestCase{ + ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAlertEnrichmentResourceDestroy, + Steps: []terraformresource.TestStep{ + { + Config: testAccAlertEnrichmentConditionalWithAnnotationMatchers(uid), + Check: terraformresource.ComposeTestCheckFunc( + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "metadata.uid", uid), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.title", "Test Conditional With Annotation Matchers"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.#", "1"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.if.annotation_matchers.#", "2"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.if.annotation_matchers.0.type", "="), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.if.annotation_matchers.0.name", "runbook_url"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.if.annotation_matchers.1.type", "!="), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.if.annotation_matchers.1.name", "suppress"), + ), + }, + { + ResourceName: alertEnrichmentResourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "options.%", + "options.overwrite", + }, + ImportStateIdFunc: importStateIDFunc(alertEnrichmentResourceName), + }, + }, + }) +} + +func testAccAlertEnrichmentConditionalWithAnnotationMatchers(uid string) string { + return fmt.Sprintf(` +resource "grafana_apps_alertenrichment_alertenrichment_v1beta1" "test" { + metadata { + uid = "%s" + } + + spec { + title = "Test Conditional With Annotation Matchers" + description = "Conditional with annotation matchers" + + step { + conditional { + if { + annotation_matchers = [ + { + type = "=" + name = "runbook_url" + value = "https://runbook.example.com" + }, + { + type = "!=" + name = "suppress" + value = "true" + } + ] + } + + then { + step { + assign { + annotations = { + escalation = "high" + } + } + } + } + + else { + step { + assign { + annotations = { + escalation = "low" + } + } + } + } + } + } + } +} +`, uid) +} + +func TestAccAlertEnrichment_conditionalWithMultipleEnricherTypes(t *testing.T) { + testutils.CheckEnterpriseTestsEnabled(t, ">=12.2.0") + + uid := acctest.RandString(10) + + terraformresource.ParallelTest(t, terraformresource.TestCase{ + ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAlertEnrichmentResourceDestroy, + Steps: []terraformresource.TestStep{ + { + Config: testAccAlertEnrichmentConditionalWithMultipleEnrichers(uid), + Check: terraformresource.ComposeTestCheckFunc( + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "metadata.uid", uid), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.title", "Test Conditional With Multiple Enrichers"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.#", "1"), + // then branch enrichers in order + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.then.step.#", "3"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.then.step.0.assign.annotations.escalation", "immediate"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.then.step.1.external.url", "https://pager.example.com/create-incident"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.then.step.2.explain.annotation", "ai_analysis"), + // else branch enrichers in order + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.else.step.#", "3"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.else.step.0.assign.annotations.escalation", "standard"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.else.step.1.sift.timeout", "30s"), + terraformresource.TestCheckResourceAttr(alertEnrichmentResourceName, "spec.step.0.conditional.else.step.2.asserts.timeout", "25s"), + ), + }, + { + ResourceName: alertEnrichmentResourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "options.%", + "options.overwrite", + }, + ImportStateIdFunc: importStateIDFunc(alertEnrichmentResourceName), + }, + }, + }) +} + +func testAccAlertEnrichmentConditionalWithMultipleEnrichers(uid string) string { + return fmt.Sprintf(` +resource "grafana_apps_alertenrichment_alertenrichment_v1beta1" "test" { + metadata { + uid = "%s" + } + + spec { + title = "Test Conditional With Multiple Enrichers" + description = "Conditional with multiple enricher types" + + step { + conditional { + timeout = "55s" + + if { + label_matchers = [{ + type = "=" + name = "severity" + value = "critical" + }] + } + + then { + step { + assign { + annotations = { + escalation = "immediate" + } + } + } + step { + external { + url = "https://pager.example.com/create-incident" + } + } + step { + explain { + annotation = "ai_analysis" + } + } + } + + else { + step { + assign { + annotations = { + escalation = "standard" + } + } + } + step { + sift { + timeout = "30s" + } + } + step { + asserts { + timeout = "25s" + } + } + } + } + } + } +} +`, uid) +}