0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-07-08 06:45:00 +00:00
terraform-provider-proxmox/fwprovider/cluster/sdn/resource_sdn_vnets.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

332 lines
8.8 KiB
Go

package sdn
import (
"context"
"errors"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/attr"
"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-log/tflog"
"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/vnets"
)
var (
_ resource.Resource = &sdnVnetResource{}
_ resource.ResourceWithConfigure = &sdnVnetResource{}
_ resource.ResourceWithImportState = &sdnVnetResource{}
)
type sdnVnetResource struct {
client *vnets.Client
}
func NewSDNVnetResource() resource.Resource {
return &sdnVnetResource{}
}
func (r *sdnVnetResource) Metadata(
_ context.Context,
req resource.MetadataRequest,
resp *resource.MetadataResponse,
) {
resp.TypeName = req.ProviderTypeName + "_sdn_vnet"
}
func (r *sdnVnetResource) 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().SDNVnets()
}
func (r *sdnVnetResource) Schema(
_ context.Context,
_ resource.SchemaRequest,
resp *resource.SchemaResponse,
) {
resp.Schema = schema.Schema{
Description: "Manages Proxmox VE SDN vnet.",
Attributes: map[string]schema.Attribute{
"id": attribute.ResourceID(),
"name": schema.StringAttribute{
Description: "Unique identifier for the vnet.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"zonetype": schema.StringAttribute{
Required: true,
Description: "Parent's zone type. MUST be specified.",
},
"zone": schema.StringAttribute{
Description: "The zone to which this vnet belongs.",
Required: true,
},
"alias": schema.StringAttribute{
Optional: true,
Description: "An optional alias for this vnet.",
},
"isolate_ports": schema.BoolAttribute{
Optional: true,
Description: "Whether to isolate ports within this vnet.",
},
"tag": schema.Int64Attribute{
Optional: true,
Description: "Tag value for VLAN/VXLAN (depends on zone type).",
},
"type": schema.StringAttribute{
Computed: true,
Description: "Type of vnet (e.g. 'vnet').",
Default: stringdefault.StaticString("vnet"),
},
"vlanaware": schema.BoolAttribute{
Optional: true,
Description: "Whether this vnet is VLAN aware.",
},
},
}
}
func (r *sdnVnetResource) Create(
ctx context.Context,
req resource.CreateRequest,
resp *resource.CreateResponse,
) {
var plan sdnVnetModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
err := r.client.CreateVnet(ctx, plan.toAPIRequestBody())
if err != nil {
resp.Diagnostics.AddError("Error creating vnet", err.Error())
return
}
plan.ID = plan.Name
tflog.Info(ctx, "ZONETYPE value", map[string]any{"zonetype": plan.ZoneType.ValueString()})
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *sdnVnetResource) Read(
ctx context.Context,
req resource.ReadRequest,
resp *resource.ReadResponse,
) {
var state sdnVnetModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
data, err := r.client.GetVnet(ctx, state.ID.ValueString())
if err != nil {
if errors.Is(err, api.ErrResourceDoesNotExist) {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Error reading vnet", err.Error())
return
}
readModel := &sdnVnetModel{}
readModel.importFromAPI(state.ID.ValueString(), data)
// Preserve provider-only field.
readModel.ZoneType = state.ZoneType
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
}
func (r *sdnVnetResource) Update(
ctx context.Context,
req resource.UpdateRequest,
resp *resource.UpdateResponse,
) {
var plan sdnVnetModel
var state sdnVnetModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
var toDelete []string
checkDelete(plan.Alias, state.Alias, &toDelete, "alias")
checkDelete(plan.IsolatePorts, state.IsolatePorts, &toDelete, "isolate-ports")
checkDelete(plan.Tag, state.Tag, &toDelete, "tag")
checkDelete(plan.Type, state.Type, &toDelete, "type")
checkDelete(plan.VlanAware, state.VlanAware, &toDelete, "vlanaware")
reqData := plan.toAPIRequestBody()
reqData.Delete = toDelete
err := r.client.UpdateVnet(ctx, reqData)
if err != nil {
resp.Diagnostics.AddError("Error updating vnet", err.Error())
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *sdnVnetResource) Delete(
ctx context.Context,
req resource.DeleteRequest,
resp *resource.DeleteResponse,
) {
var state sdnVnetModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
err := r.client.DeleteVnet(ctx, state.ID.ValueString())
if err != nil && !errors.Is(err, api.ErrResourceDoesNotExist) {
resp.Diagnostics.AddError("Error deleting vnet", err.Error())
}
}
func (r *sdnVnetResource) ImportState(
ctx context.Context,
req resource.ImportStateRequest,
resp *resource.ImportStateResponse,
) {
data, err := r.client.GetVnet(ctx, req.ID)
if err != nil {
if errors.Is(err, api.ErrResourceDoesNotExist) {
resp.Diagnostics.AddError("Resource does not exist", err.Error())
return
}
resp.Diagnostics.AddError("Failed to import resource", err.Error())
return
}
readModel := &sdnVnetModel{}
readModel.importFromAPI(req.ID, data)
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
}
func checkDelete(planField, stateField attr.Value, toDelete *[]string, apiName string) {
if planField.IsNull() && !stateField.IsNull() {
*toDelete = append(*toDelete, apiName)
}
}
func (r *sdnVnetResource) ValidateConfig(
ctx context.Context,
req resource.ValidateConfigRequest,
resp *resource.ValidateConfigResponse,
) {
var data sdnVnetModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
if data.Zone.IsNull() || data.Zone.IsUnknown() {
return
}
if data.ZoneType.IsNull() || data.ZoneType.IsUnknown() {
resp.Diagnostics.AddAttributeError(
path.Root("zonetype"),
"Missing Required Field",
"No Zone linked, please set the 'zonetype' property. \nEither from a created zone or a datasource import.")
return
}
zoneType := data.ZoneType.ValueString()
required := map[string][]string{
"simple": {"name", "zone"},
"vlan": {"name", "zone", "tag"},
"qinq": {"name", "zone"},
"vxlan": {"name", "zone", "tag"},
"evpn": {"name", "zone", "tag"},
}
authorized := map[string]map[string]bool{
"simple": {"name": true, "alias": true, "zone": true, "isolate_ports": true, "vlanaware": true},
"vlan": {"name": true, "alias": true, "zone": true, "tag": true, "isolate_ports": true, "vlanaware": true},
"qinq": {"name": true, "alias": true, "zone": true, "tag": true, "isolate_ports": true, "vlanaware": true},
"vxlan": {"name": true, "alias": true, "zone": true, "tag": true, "isolate_ports": true, "vlanaware": true},
"evpn": {"name": true, "alias": true, "zone": true, "tag": true, "isolate_ports": true},
}
fieldMap := map[string]attr.Value{
"name": data.Name,
"zone": data.Zone,
"alias": data.Alias,
"tag": data.Tag,
"isolate_ports": data.IsolatePorts,
"vlanaware": data.VlanAware,
"type": data.Type,
}
// Check required fields.
for _, field := range required[zoneType] {
if val, ok := fieldMap[field]; ok {
if val.IsNull() || val.IsUnknown() {
resp.Diagnostics.AddAttributeError(
path.Root(field),
"Missing Required Attribute",
fmt.Sprintf("The attribute %q is required for SDN VNETs in a %q zone.", field, zoneType),
)
}
}
}
for fieldName, val := range fieldMap {
if !authorized[zoneType][fieldName] && !val.IsNull() && !val.IsUnknown() {
resp.Diagnostics.AddAttributeError(
path.Root(fieldName),
"Unauthorized Attribute for Zone Type",
fmt.Sprintf("The attribute %q is not allowed in VNETs under a %q zone.", fieldName, zoneType),
)
}
}
}