0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-07-08 14:55:02 +00:00
terraform-provider-proxmox/fwprovider/cluster/sdn/resource_sdn_subnets.go
MacherelR 48bb57f0c7 fix(sdn): resolve linter warnings and apply gofumpt formatting
Signed-off-by: MacherelR <64424331+MacherelR@users.noreply.github.com>
2025-06-24 08:32:48 +02:00

380 lines
10 KiB
Go

package sdn
import (
"context"
"errors"
"fmt"
"net"
"strings"
"github.com/hashicorp/terraform-plugin-framework/path"
"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/types"
"github.com/bpg/terraform-provider-proxmox/fwprovider/attribute"
"github.com/bpg/terraform-provider-proxmox/fwprovider/config"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/subnets"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
var (
_ resource.Resource = &sdnSubnetResource{}
_ resource.ResourceWithConfigure = &sdnSubnetResource{}
_ resource.ResourceWithImportState = &sdnSubnetResource{}
)
type sdnSubnetResource struct {
client *subnets.Client
}
func NewSDNSubnetResource() resource.Resource {
return &sdnSubnetResource{}
}
func (r *sdnSubnetResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_sdn_subnet"
}
func (r *sdnSubnetResource) 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().SDNSubnets()
}
func (r *sdnSubnetResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manages SDN Subnets in Proxmox VE.",
Attributes: map[string]schema.Attribute{
"id": attribute.ResourceID(),
"subnet": schema.StringAttribute{
Required: true,
Description: "The name/ID of the subnet.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"canonical_name": schema.StringAttribute{
Computed: true,
Description: "Canonical name of the subnet (e.g. zoneM-10.10.0.0-24).",
},
"type": schema.StringAttribute{
Computed: true,
Description: "Subnet type (set default at 'subnet')",
Default: stringdefault.StaticString("subnet"),
},
"vnet": schema.StringAttribute{
Required: true,
Description: "The VNet to which this subnet belongs.",
},
"dhcp_dns_server": schema.StringAttribute{
Optional: true,
Description: "The DNS server used for DHCP.",
},
"dhcp_range": schema.ListNestedAttribute{
Optional: true,
Description: "List of DHCP ranges (start and end IPs).",
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"start_address": schema.StringAttribute{
Required: true,
Description: "Start of the DHCP range.",
},
"end_address": schema.StringAttribute{
Required: true,
Description: "End of the DHCP range.",
},
},
},
},
"dnszoneprefix": schema.StringAttribute{
Optional: true,
Description: "Prefix used for DNS zone delegation.",
},
"gateway": schema.StringAttribute{
Optional: true,
Description: "The gateway address for the subnet.",
},
"snat": schema.BoolAttribute{
Optional: true,
Description: "Whether SNAT is enabled for the subnet.",
},
},
}
}
func (r *sdnSubnetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan sdnSubnetModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
if plan.Vnet.IsNull() || plan.Vnet.IsUnknown() {
resp.Diagnostics.AddAttributeError(
path.Root("vnet"),
"missing required field",
"Missing the parent vnet's ID attribute, which is required to define a subnet")
return
}
err := r.client.CreateSubnet(ctx, plan.Vnet.ValueString(), plan.toAPIRequestBody(ctx))
if err != nil {
resp.Diagnostics.AddError("Error creating subnet", err.Error())
return
}
tflog.Debug(ctx, "Created object's ID", map[string]any{"plan name:": plan.Subnet})
plan.ID = plan.Subnet
/* Because proxmox API doesn't return the created object's properties and the subnet's name gets modified by
proxmox internally.
Read it back to get the canonical-ID from proxmox.*/
canonicalID, err := resolveCanonicalSubnetID(ctx, r.client, plan.Vnet.ValueString(), plan.Subnet.ValueString())
if err != nil {
resp.Diagnostics.AddError("Error resolving canonical subnet ID", err.Error())
return
}
plan.ID = types.StringValue(canonicalID)
plan.CanonicalName = types.StringValue(canonicalID)
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *sdnSubnetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state sdnSubnetModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
subnet, err := r.client.GetSubnet(ctx, state.Vnet.ValueString(), state.ID.ValueString())
if err != nil {
if errors.Is(err, api.ErrResourceDoesNotExist) {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Error reading subnet", err.Error())
return
}
readModel := &sdnSubnetModel{}
readModel.Subnet = state.Subnet
readModel.importFromAPI(state.ID.ValueString(), subnet)
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
}
func (r *sdnSubnetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan sdnSubnetModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
reqData := plan.toAPIRequestBody(ctx)
if plan.Vnet.IsNull() || plan.Vnet.IsUnknown() {
resp.Diagnostics.AddAttributeError(
path.Root("vnet"),
"missing required field",
"Missing the parent vnet's ID attribute, which is required to define a subnet")
return
}
err := r.client.UpdateSubnet(ctx, plan.Vnet.ValueString(), reqData)
if err != nil {
resp.Diagnostics.AddError("Error updating subnet", err.Error())
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *sdnSubnetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state sdnSubnetModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
err := r.client.DeleteSubnet(ctx, state.Vnet.ValueString(), state.ID.ValueString())
if err != nil && !errors.Is(err, api.ErrResourceDoesNotExist) {
resp.Diagnostics.AddError("Error deleting subnet", err.Error())
}
}
func (r *sdnSubnetResource) ImportState(
ctx context.Context,
req resource.ImportStateRequest,
resp *resource.ImportStateResponse,
) {
// Expect ID format: "vnet/subnet".
parts := strings.Split(req.ID, "/")
if len(parts) != 2 {
resp.Diagnostics.AddError(
"Unexpected Import Identifier",
"Expected import identifier in format 'vnet-id/subnet-id'.",
)
return
}
vnetID := parts[0]
subnetID := parts[1]
subnet, err := r.client.GetSubnet(ctx, vnetID, subnetID)
if err != nil {
if errors.Is(err, api.ErrResourceDoesNotExist) {
resp.Diagnostics.AddError("Subnet does not exist", err.Error())
return
}
resp.Diagnostics.AddError("Unable to import subnet", err.Error())
return
}
readModel := &sdnSubnetModel{}
readModel.importFromAPI(req.ID, subnet)
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
}
func resolveCanonicalSubnetID(
ctx context.Context,
client *subnets.Client,
vnet string,
originalID string,
) (string, error) {
subnets, err := client.GetSubnets(ctx, vnet)
if err != nil {
return "", fmt.Errorf("failed to list subnets for canonical name resolution: %w", err)
}
for _, subnet := range subnets {
if subnet.ID == originalID {
return subnet.ID, nil
}
// Proxmox canonical format is usually zone-prefixed.
// e.g., zoneM-10-10-0-0-24 instead of 10.10.0.0/24.
if strings.HasSuffix(subnet.ID, strings.ReplaceAll(originalID, "/", "-")) {
return subnet.ID, nil
}
}
return "", fmt.Errorf("could not resolve canonical subnet ID for %s", originalID)
}
/*
ValidateConfig checks that the subnet's field are correctly set.
Particularly that gateway, dhcp and dns are within CIDR.
*/
func (r *sdnSubnetResource) ValidateConfig(
ctx context.Context,
req resource.ValidateConfigRequest,
resp *resource.ValidateConfigResponse,
) {
var config sdnSubnetModel
diags := req.Config.Get(ctx, &config)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
_, ipnet, err := net.ParseCIDR(config.Subnet.ValueString())
if err != nil {
resp.Diagnostics.AddAttributeError(
path.Root("subnet"),
"Invalid Subnet",
fmt.Sprintf("Could not parse subnet: %s", err),
)
return
}
checkIPInCIDR := func(attrName string, ipVal types.String) {
if !ipVal.IsNull() {
ip := net.ParseIP(ipVal.ValueString())
if ip == nil {
resp.Diagnostics.AddAttributeError(
path.Root(attrName),
"Invalid IP Address",
fmt.Sprintf("Could not parse IP address: %s", ipVal.ValueString()),
)
return
}
if !ipnet.Contains(ip) {
resp.Diagnostics.AddAttributeError(
path.Root(attrName),
"Invalid IP for Subnet",
fmt.Sprintf("%s must be within the subnet %s", ipVal.ValueString(), config.Subnet.ValueString()),
)
}
}
}
checkIPInCIDR("gateway", config.Gateway)
checkIPInCIDR("dhcp_dns_server", config.DhcpDnsServer)
for i, r := range config.DhcpRange {
if !r.StartAddress.IsNull() {
ip := net.ParseIP(r.StartAddress.ValueString())
if !ipnet.Contains(ip) {
resp.Diagnostics.AddAttributeError(
path.Root("dhcp_range").AtListIndex(i).AtMapKey("start_address"),
"Invalid DHCP Range Start Address",
fmt.Sprintf("Start address %s must be within the subnet %s", ip, config.Subnet.ValueString()),
)
}
}
if !r.EndAddress.IsNull() {
ip := net.ParseIP(r.EndAddress.ValueString())
if !ipnet.Contains(ip) {
resp.Diagnostics.AddAttributeError(
path.Root("dhcp_range").AtListIndex(i).AtMapKey("end_address"),
"Invalid DHCP Range End Address",
fmt.Sprintf("End address %s must be within the subnet %s", ip, config.Subnet.ValueString()),
)
}
}
}
}