mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-07-08 06:45:00 +00:00
332 lines
8.8 KiB
Go
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),
|
|
)
|
|
}
|
|
}
|
|
}
|