Skip to content
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
331 changes: 190 additions & 141 deletions internal/resources/asserts/resource_disabled_alert_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,204 +4,253 @@ 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))
}

CreateContext: resourceDisabledAlertConfigCreate,
ReadContext: resourceDisabledAlertConfigRead,
UpdateContext: resourceDisabledAlertConfigUpdate,
DeleteContext: resourceDisabledAlertConfigDelete,
type disabledAlertConfigModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
MatchLabels types.Map `tfsdk:"match_labels"`
}

Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
type disabledAlertConfigResource struct {
client *assertsapi.APIClient
stackID int64
}

Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
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
Comment thread
rooneyshuman marked this conversation as resolved.
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) {
resp.TypeName = "grafana_asserts_suppressed_assertions_config"
}

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
}

// Only set matchLabels if not empty
if len(matchLabels) > 0 {
disabledAlertConfig.MatchLabels = matchLabels
if err := r.put(ctx, &data); err != nil {
resp.Diagnostics.AddError("Failed to update disabled alert configuration", err.Error())
return
}

// Call the generated client API
request := client.AlertConfigurationAPI.PutDisabledAlertConfig(ctx).
DisabledAlertConfigDto(disabledAlertConfig).
XScopeOrgID(fmt.Sprintf("%d", stackID))
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)...)
}

_, err := request.Execute()
if err != nil {
return diag.FromErr(fmt.Errorf("failed to create disabled alert configuration: %w", err))
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
}

d.SetId(name)
_, err := r.client.AlertConfigurationAPI.DeleteDisabledAlertConfig(ctx, data.Name.ValueString()).
XScopeOrgID(fmt.Sprintf("%d", r.stackID)).
Execute()
if err != nil {
resp.Diagnostics.AddError("Failed to delete disabled alert configuration", err.Error())
}
}

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(),
}
name := d.Id()

// Retry logic for read operation to handle eventual consistency
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() {
Comment thread
rooneyshuman marked this conversation as resolved.
Outdated
return fmt.Errorf("failed to read match_labels")
Comment thread
rooneyshuman marked this conversation as resolved.
Outdated
}
dto.MatchLabels = matchLabels
}

_, 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
}

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)
}
}

// 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 nil, diags
}

return resourceDisabledAlertConfigRead(ctx, d, meta)
}

func resourceDisabledAlertConfigDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, stackID, diags := validateAssertsClient(meta)
matchLabels, mapDiags := types.MapValueFrom(ctx, types.StringType, foundConfig.MatchLabels)
diags.Append(mapDiags...)
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, diags
}

return nil
return &disabledAlertConfigModel{
ID: types.StringValue(name),
Name: types.StringValue(name),
MatchLabels: matchLabels,
}, diags
}
Loading