From 5fc3223184af8d40751a33e2bbccedc6e060dfae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?bel=C3=A9n?= Date: Tue, 7 Apr 2026 16:04:52 -0700 Subject: [PATCH 1/3] Migrate disabled alert config --- .../asserts/resource_disabled_alert_config.go | 330 ++++++++++-------- 1 file changed, 189 insertions(+), 141 deletions(-) diff --git a/internal/resources/asserts/resource_disabled_alert_config.go b/internal/resources/asserts/resource_disabled_alert_config.go index dc8cea296..ff1abd59d 100644 --- a/internal/resources/asserts/resource_disabled_alert_config.go +++ b/internal/resources/asserts/resource_disabled_alert_config.go @@ -4,204 +4,252 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - assertsapi "github.com/grafana/grafana-asserts-public-clients/go/gcom" "github.com/grafana/terraform-provider-grafana/v4/internal/common" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + sdkretry "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" +) + +var ( + _ resource.Resource = (*disabledAlertConfigResource)(nil) + _ resource.ResourceWithConfigure = (*disabledAlertConfigResource)(nil) + _ resource.ResourceWithImportState = (*disabledAlertConfigResource)(nil) ) func makeResourceDisabledAlertConfig() *common.Resource { - schema := &schema.Resource{ - Description: "Manages Knowledge Graph Disabled Alert Configurations through Grafana API.", + return common.NewResource( + common.CategoryAsserts, + "grafana_asserts_suppressed_assertions_config", + common.NewResourceID(common.StringIDField("name")), + &disabledAlertConfigResource{}, + ).WithLister(assertsListerFunction(listDisabledAlertConfigs)) +} + +type disabledAlertConfigModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + MatchLabels types.Map `tfsdk:"match_labels"` +} - CreateContext: resourceDisabledAlertConfigCreate, - ReadContext: resourceDisabledAlertConfigRead, - UpdateContext: resourceDisabledAlertConfigUpdate, - DeleteContext: resourceDisabledAlertConfigDelete, +type disabledAlertConfigResource struct { + client *assertsapi.APIClient + stackID int64 +} - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, +func (r *disabledAlertConfigResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*common.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *common.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + if client.AssertsAPIClient == nil { + resp.Diagnostics.AddError( + "Asserts API client is not configured", + "Please ensure that the Asserts API client is configured.", + ) + return + } + r.client = client.AssertsAPIClient + r.stackID = client.GrafanaStackID +} + +func (r *disabledAlertConfigResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "grafana_asserts_suppressed_assertions_config" +} - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, +func (r *disabledAlertConfigResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Manages Knowledge Graph Disabled Alert Configurations through Grafana API.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ Required: true, - ForceNew: true, // Force recreation if name changes Description: "The name of the disabled alert configuration.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - "match_labels": { - Type: schema.TypeMap, + "match_labels": schema.MapAttribute{ + ElementType: types.StringType, Optional: true, Description: "Labels to match for this disabled alert configuration.", - Elem: &schema.Schema{Type: schema.TypeString}, }, }, } +} - return common.NewLegacySDKResource( - common.CategoryAsserts, - "grafana_asserts_suppressed_assertions_config", - common.NewResourceID(common.StringIDField("name")), - schema, - ).WithLister(assertsListerFunction(listDisabledAlertConfigs)) +func (r *disabledAlertConfigResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data disabledAlertConfigModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + if err := r.put(ctx, &data); err != nil { + resp.Diagnostics.AddError("Failed to create disabled alert configuration", err.Error()) + return + } + + readData, diags := r.read(ctx, data.Name.ValueString()) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, readData)...) } -func resourceDisabledAlertConfigCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, stackID, diags := validateAssertsClient(meta) - if diags.HasError() { - return diags +func (r *disabledAlertConfigResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data disabledAlertConfigModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return } - name := d.Get("name").(string) - matchLabels := make(map[string]string) - if v, ok := d.GetOk("match_labels"); ok { - for k, val := range v.(map[string]interface{}) { - matchLabels[k] = val.(string) - } + readData, diags := r.read(ctx, data.Name.ValueString()) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if readData == nil { + resp.State.RemoveResource(ctx) + return } + resp.Diagnostics.Append(resp.State.Set(ctx, readData)...) +} - // Create DisabledAlertConfigDto using the generated client models - disabledAlertConfig := assertsapi.DisabledAlertConfigDto{ - Name: &name, - ManagedBy: getManagedByTerraform(), +func (r *disabledAlertConfigResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data disabledAlertConfigModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + if err := r.put(ctx, &data); err != nil { + resp.Diagnostics.AddError("Failed to update disabled alert configuration", err.Error()) + return } - // Only set matchLabels if not empty - if len(matchLabels) > 0 { - disabledAlertConfig.MatchLabels = matchLabels + readData, diags := r.read(ctx, data.Name.ValueString()) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } + resp.Diagnostics.Append(resp.State.Set(ctx, readData)...) +} - // Call the generated client API - request := client.AlertConfigurationAPI.PutDisabledAlertConfig(ctx). - DisabledAlertConfigDto(disabledAlertConfig). - XScopeOrgID(fmt.Sprintf("%d", stackID)) +func (r *disabledAlertConfigResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data disabledAlertConfigModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } - _, err := request.Execute() + _, err := r.client.AlertConfigurationAPI.DeleteDisabledAlertConfig(ctx, data.Name.ValueString()). + XScopeOrgID(fmt.Sprintf("%d", r.stackID)). + Execute() if err != nil { - return diag.FromErr(fmt.Errorf("failed to create disabled alert configuration: %w", err)) + resp.Diagnostics.AddError("Failed to delete disabled alert configuration", err.Error()) } +} - d.SetId(name) - - return resourceDisabledAlertConfigRead(ctx, d, meta) +func (r *disabledAlertConfigResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + readData, diags := r.read(ctx, req.ID) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + if readData == nil { + resp.Diagnostics.AddError("Resource not found", fmt.Sprintf("disabled alert configuration %q not found", req.ID)) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, readData)...) } -func resourceDisabledAlertConfigRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, stackID, diags := validateAssertsClient(meta) - if diags.HasError() { - return diags +// put creates or updates a disabled alert config via the Asserts API (upsert). +func (r *disabledAlertConfigResource) put(ctx context.Context, data *disabledAlertConfigModel) error { + name := data.Name.ValueString() + dto := assertsapi.DisabledAlertConfigDto{ + Name: &name, + ManagedBy: getManagedByTerraform(), + } + + if !data.MatchLabels.IsNull() && len(data.MatchLabels.Elements()) > 0 { + var matchLabels map[string]string + if diags := data.MatchLabels.ElementsAs(context.Background(), &matchLabels, false); diags.HasError() { + return fmt.Errorf("failed to read match_labels") + } + dto.MatchLabels = matchLabels } - name := d.Id() - // Retry logic for read operation to handle eventual consistency + _, err := r.client.AlertConfigurationAPI.PutDisabledAlertConfig(ctx). + DisabledAlertConfigDto(dto). + XScopeOrgID(fmt.Sprintf("%d", r.stackID)). + Execute() + return err +} + +// read fetches a disabled alert config by name with retry for eventual consistency. +func (r *disabledAlertConfigResource) read(ctx context.Context, name string) (*disabledAlertConfigModel, diag.Diagnostics) { + var diags diag.Diagnostics var foundConfig *assertsapi.DisabledAlertConfigDto - err := withRetryRead(ctx, func(retryCount, maxRetries int) *retry.RetryError { - // Get all disabled alert configs using the generated client API - request := client.AlertConfigurationAPI.GetAllDisabledAlertConfigs(ctx). - XScopeOrgID(fmt.Sprintf("%d", stackID)) - configs, _, err := request.Execute() + err := withRetryRead(ctx, func(retryCount, maxRetries int) *sdkretry.RetryError { + configs, _, err := r.client.AlertConfigurationAPI.GetAllDisabledAlertConfigs(ctx). + XScopeOrgID(fmt.Sprintf("%d", r.stackID)). + Execute() if err != nil { return createAPIError("get disabled alert configurations", retryCount, maxRetries, err) } - - // Find our specific config for _, config := range configs.DisabledAlertConfigs { if config.Name != nil && *config.Name == name { foundConfig = &config return nil } } - - // Check if we should give up or retry if retryCount >= maxRetries { return createNonRetryableError("disabled alert configuration", name, retryCount) } return createRetryableError("disabled alert configuration", name, retryCount, maxRetries) }) - if err != nil { - return diag.FromErr(err) + diags.AddError("Failed to read disabled alert configuration", err.Error()) + return nil, diags } - if foundConfig == nil { - d.SetId("") - return nil - } - - // Set the resource data - if foundConfig.Name != nil { - if err := d.Set("name", *foundConfig.Name); err != nil { - return diag.FromErr(err) - } - } - if foundConfig.MatchLabels != nil { - if err := d.Set("match_labels", foundConfig.MatchLabels); err != nil { - return diag.FromErr(err) - } - } - - return nil -} - -func resourceDisabledAlertConfigUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, stackID, diags := validateAssertsClient(meta) - if diags.HasError() { - return diags + return nil, diags } - name := d.Get("name").(string) - matchLabels := make(map[string]string) - - if v, ok := d.GetOk("match_labels"); ok { - for k, val := range v.(map[string]interface{}) { - matchLabels[k] = val.(string) + var matchLabels types.Map + if len(foundConfig.MatchLabels) > 0 { + var mapDiags diag.Diagnostics + matchLabels, mapDiags = types.MapValueFrom(ctx, types.StringType, foundConfig.MatchLabels) + diags.Append(mapDiags...) + if diags.HasError() { + return nil, diags } + } else { + matchLabels = types.MapNull(types.StringType) } - // Create DisabledAlertConfigDto using the generated client models - disabledAlertConfig := assertsapi.DisabledAlertConfigDto{ - Name: &name, - ManagedBy: getManagedByTerraform(), - } - - // Only set matchLabels if not empty - if len(matchLabels) > 0 { - disabledAlertConfig.MatchLabels = matchLabels - } - - // Update Disabled Alert Configuration using the generated client API - // Note: For disabled configs, update is effectively a re-create - request := client.AlertConfigurationAPI.PutDisabledAlertConfig(ctx). - DisabledAlertConfigDto(disabledAlertConfig). - XScopeOrgID(fmt.Sprintf("%d", stackID)) - - _, err := request.Execute() - if err != nil { - return diag.FromErr(fmt.Errorf("failed to update disabled alert configuration: %w", err)) - } - - return resourceDisabledAlertConfigRead(ctx, d, meta) -} - -func resourceDisabledAlertConfigDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, stackID, diags := validateAssertsClient(meta) - if diags.HasError() { - return diags - } - name := d.Id() - - // Delete Disabled Alert Configuration using the generated client API - request := client.AlertConfigurationAPI.DeleteDisabledAlertConfig(ctx, name). - XScopeOrgID(fmt.Sprintf("%d", stackID)) - - _, err := request.Execute() - if err != nil { - return diag.FromErr(fmt.Errorf("failed to delete disabled alert configuration: %w", err)) - } - - return nil + return &disabledAlertConfigModel{ + ID: types.StringValue(name), + Name: types.StringValue(name), + MatchLabels: matchLabels, + }, diags } From 7a14a0a39db79f5f0fb80cb38555daad2560c2b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?bel=C3=A9n?= Date: Tue, 7 Apr 2026 16:22:45 -0700 Subject: [PATCH 2/3] Code review suggestions --- .../asserts/resource_disabled_alert_config.go | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/internal/resources/asserts/resource_disabled_alert_config.go b/internal/resources/asserts/resource_disabled_alert_config.go index ff1abd59d..1e14a6117 100644 --- a/internal/resources/asserts/resource_disabled_alert_config.go +++ b/internal/resources/asserts/resource_disabled_alert_config.go @@ -62,6 +62,13 @@ func (r *disabledAlertConfigResource) Configure(_ context.Context, req resource. } r.client = client.AssertsAPIClient r.stackID = client.GrafanaStackID + if r.stackID == 0 { + resp.Diagnostics.AddError( + "Missing stack_id", + "stack_id must be set in provider configuration for Asserts resources.", + ) + return + } } func (r *disabledAlertConfigResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -235,16 +242,10 @@ func (r *disabledAlertConfigResource) read(ctx context.Context, name string) (*d return nil, diags } - var matchLabels types.Map - if len(foundConfig.MatchLabels) > 0 { - var mapDiags diag.Diagnostics - matchLabels, mapDiags = types.MapValueFrom(ctx, types.StringType, foundConfig.MatchLabels) - diags.Append(mapDiags...) - if diags.HasError() { - return nil, diags - } - } else { - matchLabels = types.MapNull(types.StringType) + matchLabels, mapDiags := types.MapValueFrom(ctx, types.StringType, foundConfig.MatchLabels) + diags.Append(mapDiags...) + if diags.HasError() { + return nil, diags } return &disabledAlertConfigModel{ From 19716c0ed9bbfba7ea99657213c8bcd18c27ccad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?bel=C3=A9n?= Date: Fri, 10 Apr 2026 11:09:21 -0700 Subject: [PATCH 3/3] Code review suggestions --- internal/resources/asserts/resource_disabled_alert_config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/resources/asserts/resource_disabled_alert_config.go b/internal/resources/asserts/resource_disabled_alert_config.go index 1e14a6117..9ad114b86 100644 --- a/internal/resources/asserts/resource_disabled_alert_config.go +++ b/internal/resources/asserts/resource_disabled_alert_config.go @@ -198,8 +198,8 @@ func (r *disabledAlertConfigResource) put(ctx context.Context, data *disabledAle if !data.MatchLabels.IsNull() && len(data.MatchLabels.Elements()) > 0 { var matchLabels map[string]string - if diags := data.MatchLabels.ElementsAs(context.Background(), &matchLabels, false); diags.HasError() { - return fmt.Errorf("failed to read match_labels") + if diags := data.MatchLabels.ElementsAs(ctx, &matchLabels, false); diags.HasError() { + return fmt.Errorf("failed to read match_labels: %s", diags) } dto.MatchLabels = matchLabels }