mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-06-30 18:42:58 +00:00
372 lines
10 KiB
Go
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)...)
|
|
}
|
|
}
|