internal/services/azapi_resource_action_resource.go (339 lines of code) (raw):

package services import ( "context" "fmt" "slices" "time" "github.com/Azure/terraform-provider-azapi/internal/clients" "github.com/Azure/terraform-provider-azapi/internal/docstrings" "github.com/Azure/terraform-provider-azapi/internal/locks" "github.com/Azure/terraform-provider-azapi/internal/retry" "github.com/Azure/terraform-provider-azapi/internal/services/defaults" "github.com/Azure/terraform-provider-azapi/internal/services/dynamic" "github.com/Azure/terraform-provider-azapi/internal/services/migration" "github.com/Azure/terraform-provider-azapi/internal/services/myplanmodifier" "github.com/Azure/terraform-provider-azapi/internal/services/myvalidator" "github.com/Azure/terraform-provider-azapi/internal/services/parse" "github.com/Azure/terraform-provider-azapi/internal/skip" "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "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/schema/validator" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" ) type ActionResourceModel struct { ID types.String `tfsdk:"id"` Type types.String `tfsdk:"type"` ResourceId types.String `tfsdk:"resource_id"` Action types.String `tfsdk:"action"` Method types.String `tfsdk:"method"` Body types.Dynamic `tfsdk:"body"` When types.String `tfsdk:"when"` Locks types.List `tfsdk:"locks"` ResponseExportValues types.Dynamic `tfsdk:"response_export_values"` SensitiveResponseExportValues types.Dynamic `tfsdk:"sensitive_response_export_values"` Output types.Dynamic `tfsdk:"output"` SensitiveOutput types.Dynamic `tfsdk:"sensitive_output"` Timeouts timeouts.Value `tfsdk:"timeouts" skip_on:"update"` Retry retry.RetryValue `tfsdk:"retry" skip_on:"update"` Headers types.Map `tfsdk:"headers"` QueryParameters types.Map `tfsdk:"query_parameters"` } type ActionResource struct { ProviderData *clients.Client } var _ resource.Resource = &ActionResource{} var _ resource.ResourceWithConfigure = &ActionResource{} var _ resource.ResourceWithModifyPlan = &ActionResource{} var _ resource.ResourceWithUpgradeState = &ActionResource{} func (r *ActionResource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { if v, ok := request.ProviderData.(*clients.Client); ok { r.ProviderData = v } } func (r *ActionResource) Metadata(ctx context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { response.TypeName = request.ProviderTypeName + "_resource_action" } func (r *ActionResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader { return map[int64]resource.StateUpgrader{ 0: migration.AzapiResourceActionMigrationV0ToV2(ctx), 1: migration.AzapiResourceActionMigrationV1ToV2(ctx), } } func (r *ActionResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { response.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, MarkdownDescription: docstrings.ID(), }, "type": schema.StringAttribute{ Required: true, Validators: []validator.String{ myvalidator.StringIsResourceType(), }, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, MarkdownDescription: docstrings.Type(), }, "resource_id": schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, Validators: []validator.String{ myvalidator.StringIsResourceID(), }, MarkdownDescription: "The ID of an existing Azure source.", }, "action": schema.StringAttribute{ Optional: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, MarkdownDescription: docstrings.ResourceAction(), }, "method": schema.StringAttribute{ Optional: true, Computed: true, Default: defaults.StringDefault("POST"), Validators: []validator.String{ stringvalidator.OneOf("POST", "PATCH", "PUT", "DELETE", "GET", "HEAD"), }, MarkdownDescription: "Specifies the HTTP method of the azure resource action. Allowed values are `POST`, `PATCH`, `PUT` and `DELETE`. Defaults to `POST`.", }, // The body attribute is a dynamic attribute that only allows users to specify the resource body as an HCL object "body": schema.DynamicAttribute{ Optional: true, PlanModifiers: []planmodifier.Dynamic{ myplanmodifier.DynamicUseStateWhen(dynamic.SemanticallyEqual), }, MarkdownDescription: docstrings.Body(), Validators: []validator.Dynamic{ myvalidator.DynamicIsNotStringValidator(), }, }, "when": schema.StringAttribute{ Optional: true, Computed: true, Default: defaults.StringDefault("apply"), Validators: []validator.String{ stringvalidator.OneOf("apply", "destroy"), }, MarkdownDescription: "When to perform the action, value must be one of: `apply`, `destroy`. Default is `apply`.", }, "locks": schema.ListAttribute{ ElementType: types.StringType, Optional: true, Validators: []validator.List{ listvalidator.ValueStringsAre(myvalidator.StringIsNotEmpty()), }, MarkdownDescription: docstrings.Locks(), }, "response_export_values": schema.DynamicAttribute{ Optional: true, PlanModifiers: []planmodifier.Dynamic{ myplanmodifier.DynamicUseStateWhen(dynamic.SemanticallyEqual), }, MarkdownDescription: docstrings.ResponseExportValues(), }, "sensitive_response_export_values": schema.DynamicAttribute{ Optional: true, PlanModifiers: []planmodifier.Dynamic{ myplanmodifier.DynamicUseStateWhen(dynamic.SemanticallyEqual), }, MarkdownDescription: docstrings.ResponseExportValues(), }, "output": schema.DynamicAttribute{ Computed: true, MarkdownDescription: docstrings.Output("azapi_resource_action"), }, "sensitive_output": schema.DynamicAttribute{ Computed: true, Sensitive: true, MarkdownDescription: docstrings.SensitiveOutput("azapi_resource_action"), }, "retry": retry.RetrySchema(ctx), "headers": schema.MapAttribute{ ElementType: types.StringType, Optional: true, MarkdownDescription: "A map of headers to include in the request", }, "query_parameters": schema.MapAttribute{ ElementType: types.ListType{ ElemType: types.StringType, }, Optional: true, MarkdownDescription: "A map of query parameters to include in the request", }, }, Blocks: map[string]schema.Block{ "timeouts": timeouts.Block(ctx, timeouts.Opts{ Create: true, Update: true, Read: true, Delete: true, }), }, Version: 2, } } func (r *ActionResource) ModifyPlan(ctx context.Context, request resource.ModifyPlanRequest, response *resource.ModifyPlanResponse) { var config, plan, state *ActionResourceModel response.Diagnostics.Append(request.Config.Get(ctx, &config)...) response.Diagnostics.Append(request.Plan.Get(ctx, &plan)...) response.Diagnostics.Append(request.State.Get(ctx, &state)...) if response.Diagnostics.HasError() { return } // destroy doesn't need to modify plan if config == nil { return } if state == nil || !dynamic.SemanticallyEqual(config.Body, state.Body) { plan.Output = basetypes.NewDynamicUnknown() plan.SensitiveOutput = basetypes.NewDynamicUnknown() } else { plan.Output = state.Output if !plan.ResponseExportValues.Equal(state.ResponseExportValues) { plan.Output = basetypes.NewDynamicUnknown() } plan.SensitiveOutput = state.SensitiveOutput if !plan.SensitiveResponseExportValues.Equal(state.SensitiveResponseExportValues) { plan.SensitiveOutput = basetypes.NewDynamicUnknown() } } response.Diagnostics.Append(response.Plan.Set(ctx, plan)...) } func (r *ActionResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { var model ActionResourceModel if response.Diagnostics.Append(request.Plan.Get(ctx, &model)...); response.Diagnostics.HasError() { return } timeout, diags := model.Timeouts.Create(ctx, 30*time.Minute) if response.Diagnostics.Append(diags...); response.Diagnostics.HasError() { return } ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() if model.When.ValueString() == "apply" { r.Action(ctx, model, &response.State, &response.Diagnostics) } else { id, err := parse.ResourceIDWithResourceType(model.ResourceId.ValueString(), model.Type.ValueString()) if err != nil { response.Diagnostics.AddError("Invalid configuration", err.Error()) return } resourceId := id.ID() if actionName := model.Action.ValueString(); actionName != "" { resourceId = fmt.Sprintf("%s/%s", id.ID(), actionName) } model.ID = basetypes.NewStringValue(resourceId) model.Output = basetypes.NewDynamicNull() model.SensitiveOutput = basetypes.NewDynamicNull() response.Diagnostics.Append(response.State.Set(ctx, model)...) } } func (r *ActionResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { var state, plan ActionResourceModel if response.Diagnostics.Append(request.Plan.Get(ctx, &plan)...); response.Diagnostics.HasError() { return } if response.Diagnostics.Append(request.State.Get(ctx, &state)...); response.Diagnostics.HasError() { return } timeout, diags := plan.Timeouts.Update(ctx, 30*time.Minute) if response.Diagnostics.Append(diags...); response.Diagnostics.HasError() { return } ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() // See if we can skip the external API call (changes are to state only) if skip.CanSkipExternalRequest(state, plan, "update") { tflog.Debug(ctx, "azapi_resource.CreateUpdate skipping external request as no unskippable changes were detected") response.Diagnostics.Append(response.State.Set(ctx, plan)...) return } tflog.Debug(ctx, "azapi_resource.CreateUpdate proceeding with external request as no skippable changes were detected") if plan.When.ValueString() == "apply" { r.Action(ctx, plan, &response.State, &response.Diagnostics) } } func (r *ActionResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { var model ActionResourceModel if response.Diagnostics.Append(request.State.Get(ctx, &model)...); response.Diagnostics.HasError() { return } if model.When.ValueString() == "destroy" { r.Action(ctx, model, &response.State, &response.Diagnostics) } } func (r *ActionResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { var state ActionResourceModel if response.Diagnostics.Append(request.State.Get(ctx, &state)...); response.Diagnostics.HasError() { return } if state.When.IsNull() { state.When = basetypes.NewStringValue("apply") } response.Diagnostics.Append(response.State.Set(ctx, state)...) } func (r *ActionResource) Action(ctx context.Context, model ActionResourceModel, state *tfsdk.State, diagnostics *diag.Diagnostics) { actionTimeout, diags := model.Timeouts.Create(ctx, 30*time.Minute) diagnostics.Append(diags...) if diagnostics.HasError() { return } ctx, cancel := context.WithTimeout(ctx, actionTimeout) defer cancel() id, err := parse.ResourceIDWithResourceType(model.ResourceId.ValueString(), model.Type.ValueString()) if err != nil { diagnostics.AddError("Invalid configuration", err.Error()) return } ctx = tflog.SetField(ctx, "resource_id", id.ID()) var requestBody interface{} if err := unmarshalBody(model.Body, &requestBody); err != nil { diagnostics.AddError("Invalid body", fmt.Sprintf(`The argument "body" is invalid: %s`, err.Error())) return } lockIds := AsStringList(model.Locks) slices.Sort(lockIds) for _, lockId := range lockIds { locks.ByID(lockId) defer locks.UnlockByID(lockId) } // Ensure the context deadline has been set before calling ConfigureClientWithCustomRetry(). client := r.ProviderData.ResourceClient.ConfigureClientWithCustomRetry(ctx, model.Retry, false) responseBody, err := client.Action(ctx, id.AzureResourceId, model.Action.ValueString(), id.ApiVersion, model.Method.ValueString(), requestBody, clients.NewRequestOptions(AsMapOfString(model.Headers), AsMapOfLists(model.QueryParameters))) if err != nil { diagnostics.AddError("Failed to perform action", fmt.Errorf("performing action %s of %q: %+v", model.Action.ValueString(), id, err).Error()) return } resourceId := id.ID() if actionName := model.Action.ValueString(); actionName != "" { resourceId = fmt.Sprintf("%s/%s", id.ID(), actionName) } model.ID = basetypes.NewStringValue(resourceId) output, err := buildOutputFromBody(responseBody, model.ResponseExportValues, nil) if err != nil { diagnostics.AddError("Failed to build output", err.Error()) return } model.Output = output sensitiveOutput, err := buildOutputFromBody(responseBody, model.SensitiveResponseExportValues, nil) if err != nil { diagnostics.AddError("Failed to build sensitive output", err.Error()) return } model.SensitiveOutput = sensitiveOutput diagnostics.Append(state.Set(ctx, model)...) }