0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-06-30 18:42:58 +00:00
terraform-provider-proxmox/fwprovider/ha/resource_haresource.go
Pavel Boldyrev 72f7cb81a8
feat(provider): reliable sequential and random vm_id generation (#1557)
Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
2024-10-03 20:18:37 -04:00

372 lines
10 KiB
Go

/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ha
import (
"context"
"fmt"
"regexp"
"strings"
"github.com/bpg/terraform-provider-proxmox/fwprovider/attribute"
"github.com/bpg/terraform-provider-proxmox/fwprovider/config"
haresources "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/ha/resources"
proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"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/stringdefault"
"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"
)
// haResourceResource contains the resource's internal data.
// NOTE: the naming is horrible, but this is the convention used by the framework.
// and the entity name in the API is "ha resource", so...
type haResourceResource struct {
// The HA resources API client
client *haresources.Client
}
// Ensure the resource implements the expected interfaces.
var (
_ resource.Resource = &haResourceResource{}
_ resource.ResourceWithConfigure = &haResourceResource{}
_ resource.ResourceWithImportState = &haResourceResource{}
)
// NewHAResourceResource returns a new resource for managing High Availability resources.
func NewHAResourceResource() resource.Resource {
return &haResourceResource{}
}
// Metadata defines the name of the resource.
func (r *haResourceResource) Metadata(
_ context.Context,
req resource.MetadataRequest,
resp *resource.MetadataResponse,
) {
resp.TypeName = req.ProviderTypeName + "_haresource"
}
// Schema defines the schema for the resource.
func (r *haResourceResource) Schema(
_ context.Context,
_ resource.SchemaRequest,
resp *resource.SchemaResponse,
) {
resp.Schema = schema.Schema{
Description: "Manages Proxmox HA resources.",
Attributes: map[string]schema.Attribute{
"id": attribute.ID(),
"resource_id": schema.StringAttribute{
Description: "The Proxmox HA resource identifier",
Required: true,
Validators: []validator.String{
resourceIDValidator(),
},
},
"state": schema.StringAttribute{
Description: "The desired state of the resource.",
Optional: true,
Computed: true,
Default: stringdefault.StaticString("started"),
Validators: []validator.String{
resourceStateValidator(),
},
},
"type": schema.StringAttribute{
MarkdownDescription: "The type of HA resources to create. If unset, it will be deduced from the `resource_id`.",
Computed: true,
Optional: true,
Validators: []validator.String{
resourceTypeValidator(),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"comment": schema.StringAttribute{
Description: "The comment associated with this resource.",
Optional: true,
Validators: []validator.String{
stringvalidator.UTF8LengthAtLeast(1),
stringvalidator.RegexMatches(regexp.MustCompile(`^\S|^$`), "must not start with whitespace"),
stringvalidator.RegexMatches(regexp.MustCompile(`\S$|^$`), "must not end with whitespace"),
},
},
"group": schema.StringAttribute{
Description: "The identifier of the High Availability group this resource is a member of.",
Optional: true,
Validators: []validator.String{
stringvalidator.RegexMatches(
regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9\-_.]*[a-zA-Z0-9]$`),
"must start with a letter, end with a letter or number, be composed of "+
"letters, numbers, '-', '_' and '.', and must be at least 2 characters long",
),
},
},
"max_relocate": schema.Int64Attribute{
Description: "The maximal number of relocation attempts.",
Optional: true,
Validators: []validator.Int64{
int64validator.Between(0, 10),
},
},
"max_restart": schema.Int64Attribute{
Description: "The maximal number of restart attempts.",
Optional: true,
Validators: []validator.Int64{
int64validator.Between(0, 10),
},
},
},
}
}
// Configure adds the provider-configured client to the resource.
func (r *haResourceResource) Configure(
_ context.Context,
req resource.ConfigureRequest,
resp *resource.ConfigureResponse,
) {
if req.ProviderData == nil {
return
}
cfg, ok := req.ProviderData.(config.Resource)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected config.Resource, got: %T", req.ProviderData),
)
return
}
r.client = cfg.Client.Cluster().HA().Resources()
}
// Create creates a new HA resource.
func (r *haResourceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data ResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resID, err := proxmoxtypes.ParseHAResourceID(data.ResourceID.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Unexpected error parsing Proxmox HA resource identifier",
fmt.Sprintf("Couldn't parse the Terraform resource ID into a valid HA resource identifier: %s", err),
)
return
}
createRequest := data.ToCreateRequest(resID)
err = r.client.Create(ctx, createRequest)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Could not create HA resource '%v'.", resID),
err.Error(),
)
return
}
data.ID = types.StringValue(resID.String())
r.readBack(ctx, &data, &resp.Diagnostics, &resp.State)
}
// Update updates an existing HA resource.
func (r *haResourceResource) Update(
ctx context.Context,
req resource.UpdateRequest,
resp *resource.UpdateResponse,
) {
var data, state ResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
resID, err := proxmoxtypes.ParseHAResourceID(state.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Unexpected error parsing Proxmox HA resource identifier",
fmt.Sprintf("Couldn't parse the Terraform resource ID into a valid HA resource identifier: %s", err),
)
return
}
updateRequest := data.ToUpdateRequest(&state)
err = r.client.Update(ctx, resID, updateRequest)
if err == nil {
r.readBack(ctx, &data, &resp.Diagnostics, &resp.State)
} else {
resp.Diagnostics.AddError(
"Error updating HA resource",
fmt.Sprintf("Could not update HA resource '%s', unexpected error: %s",
state.Group.ValueString(), err.Error()),
)
}
}
// Delete deletes an existing HA resource.
func (r *haResourceResource) Delete(
ctx context.Context,
req resource.DeleteRequest,
resp *resource.DeleteResponse,
) {
var data ResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
resID, err := proxmoxtypes.ParseHAResourceID(data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Unexpected error parsing Proxmox HA resource identifier",
fmt.Sprintf("Couldn't parse the Terraform resource ID into a valid HA resource identifier: %s", err),
)
return
}
err = r.client.Delete(ctx, resID)
if err != nil {
if strings.Contains(err.Error(), "no such resource") {
resp.Diagnostics.AddWarning(
"HA resource does not exist",
fmt.Sprintf(
"Could not delete HA resource '%v', it does not exist or has been deleted outside of Terraform.",
resID,
),
)
} else {
resp.Diagnostics.AddError(
"Error deleting HA resource",
fmt.Sprintf("Could not delete HA resource '%v', unexpected error: %s",
resID, err.Error()),
)
}
}
}
// Read reads the HA resource.
func (r *haResourceResource) Read(
ctx context.Context,
req resource.ReadRequest,
resp *resource.ReadResponse,
) {
var data ResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
found, diags := r.read(ctx, &data)
resp.Diagnostics.Append(diags...)
if !resp.Diagnostics.HasError() {
if found {
resp.Diagnostics.Append(resp.State.Set(ctx, data)...)
} else {
resp.State.RemoveResource(ctx)
}
}
}
// ImportState imports a HA resource from the Proxmox cluster.
func (r *haResourceResource) ImportState(
ctx context.Context,
req resource.ImportStateRequest,
resp *resource.ImportStateResponse,
) {
reqID := req.ID
data := ResourceModel{
ID: types.StringValue(reqID),
ResourceID: types.StringValue(reqID),
}
r.readBack(ctx, &data, &resp.Diagnostics, &resp.State)
}
// read reads information about a HA resource from the cluster. The Terraform resource identifier must have been set
// in the model before this function is called.
func (r *haResourceResource) read(ctx context.Context, data *ResourceModel) (bool, diag.Diagnostics) {
var diags diag.Diagnostics
resID, err := proxmoxtypes.ParseHAResourceID(data.ID.ValueString())
if err != nil {
diags.AddError(
"Unexpected error parsing Proxmox HA resource identifier",
fmt.Sprintf("Couldn't parse the Terraform resource ID into a valid HA resource identifier: %s", err),
)
return false, diags
}
res, err := r.client.Get(ctx, resID)
if err != nil {
if !strings.Contains(err.Error(), "no such resource") {
diags.AddError("Could not read HA resource", err.Error())
}
return false, diags
}
data.ImportFromAPI(res)
return true, nil
}
// readBack reads information about a created or modified HA resource from the cluster then updates the response
// state accordingly. It is assumed that the `state`'s identifier is set.
func (r *haResourceResource) readBack(
ctx context.Context,
data *ResourceModel,
respDiags *diag.Diagnostics,
respState *tfsdk.State,
) {
found, diags := r.read(ctx, data)
respDiags.Append(diags...)
if !found {
respDiags.AddError(
"HA resource not found after update",
"Failed to find the resource when trying to read back the updated HA resource's data.",
)
}
if !respDiags.HasError() {
respDiags.Append(respState.Set(ctx, *data)...)
}
}