0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-06-29 18:21:10 +00:00

feat(lxc): increase number of supported mount points to 256 (#1939)

* feat(lxc): increase number of supported mount points to 256
* fix(container): correct condition for setting replicate value for rootfs


Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
This commit is contained in:
Pavel Boldyrev 2025-04-29 21:15:20 -04:00 committed by GitHub
parent 64147cd24e
commit a99220e9fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 357 additions and 246 deletions

View File

@ -120,7 +120,10 @@ func TestAccResourceContainer(t *testing.T) {
ctInfo, err := ct.GetContainer(ctx) ctInfo, err := ct.GetContainer(ctx)
require.NoError(te.t, err, "failed to get container") require.NoError(te.t, err, "failed to get container")
require.NotNil(te.t, ctInfo.DevicePassthrough0) dev0, ok := ctInfo.PassthroughDevices["dev0"]
require.True(te.t, ok, `"dev0" passthrough device not found`)
require.NotNil(te.t, dev0, `"dev0" passthrough device is <nil>`)
require.Equal(te.t, "/dev/zero", dev0.Path)
return nil return nil
}, },
@ -309,7 +312,10 @@ func TestAccResourceContainer(t *testing.T) {
ctInfo, err := ct.GetContainer(ctx) ctInfo, err := ct.GetContainer(ctx)
require.NoError(te.t, err, "failed to get container") require.NoError(te.t, err, "failed to get container")
require.NotNil(te.t, ctInfo.DevicePassthrough0) dev0, ok := ctInfo.PassthroughDevices["dev0"]
require.True(te.t, ok, `"dev0" passthrough device not found`)
require.NotNil(te.t, dev0, `"dev0" passthrough device is <nil>`)
require.Equal(te.t, "/dev/zero", dev0.Path)
return nil return nil
}, },

View File

@ -10,12 +10,22 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/url" "net/url"
"regexp"
"strconv" "strconv"
"strings" "strings"
"github.com/bpg/terraform-provider-proxmox/proxmox/types" "github.com/bpg/terraform-provider-proxmox/proxmox/types"
) )
var (
// regexDeviceKey matches device keys like dev0, dev1, etc.
regexDeviceKey = regexp.MustCompile(`^dev\d+$`)
// regexNetworkKey matches network interface keys like net0, net1, etc.
regexNetworkKey = regexp.MustCompile(`^net\d+$`)
// regexMountPointKey matches mount point keys like mp0, mp1, etc.
regexMountPointKey = regexp.MustCompile(`^mp\d+$`)
)
// CloneRequestBody contains the data for an container clone request. // CloneRequestBody contains the data for an container clone request.
type CloneRequestBody struct { type CloneRequestBody struct {
BandwidthLimit *int `json:"bwlimit,omitempty" url:"bwlimit,omitempty"` BandwidthLimit *int `json:"bwlimit,omitempty" url:"bwlimit,omitempty"`
@ -31,46 +41,46 @@ type CloneRequestBody struct {
// CreateRequestBody contains the data for a user create request. // CreateRequestBody contains the data for a user create request.
type CreateRequestBody struct { type CreateRequestBody struct {
BandwidthLimit *float64 `json:"bwlimit,omitempty" url:"bwlimit,omitempty"` BandwidthLimit *float64 `json:"bwlimit,omitempty" url:"bwlimit,omitempty"`
ConsoleEnabled *types.CustomBool `json:"console,omitempty" url:"console,omitempty,int"` ConsoleEnabled *types.CustomBool `json:"console,omitempty" url:"console,omitempty,int"`
ConsoleMode *string `json:"cmode,omitempty" url:"cmode,omitempty"` ConsoleMode *string `json:"cmode,omitempty" url:"cmode,omitempty"`
CPUArchitecture *string `json:"arch,omitempty" url:"arch,omitempty"` CPUArchitecture *string `json:"arch,omitempty" url:"arch,omitempty"`
CPUCores *int `json:"cores,omitempty" url:"cores,omitempty"` CPUCores *int `json:"cores,omitempty" url:"cores,omitempty"`
CPULimit *int `json:"cpulimit,omitempty" url:"cpulimit,omitempty"` CPULimit *int `json:"cpulimit,omitempty" url:"cpulimit,omitempty"`
CPUUnits *int `json:"cpuunits,omitempty" url:"cpuunits,omitempty"` CPUUnits *int `json:"cpuunits,omitempty" url:"cpuunits,omitempty"`
DatastoreID *string `json:"storage,omitempty" url:"storage,omitempty"` DatastoreID *string `json:"storage,omitempty" url:"storage,omitempty"`
DedicatedMemory *int `json:"memory,omitempty" url:"memory,omitempty"` DedicatedMemory *int `json:"memory,omitempty" url:"memory,omitempty"`
Delete []string `json:"delete,omitempty" url:"delete,omitempty"` Delete []string `json:"delete,omitempty" url:"delete,omitempty"`
Description *string `json:"description,omitempty" url:"description,omitempty"` Description *string `json:"description,omitempty" url:"description,omitempty"`
DNSDomain *string `json:"searchdomain,omitempty" url:"searchdomain,omitempty"` DNSDomain *string `json:"searchdomain,omitempty" url:"searchdomain,omitempty"`
DNSServer *string `json:"nameserver,omitempty" url:"nameserver,omitempty"` DNSServer *string `json:"nameserver,omitempty" url:"nameserver,omitempty"`
Features *CustomFeatures `json:"features,omitempty" url:"features,omitempty"` Features *CustomFeatures `json:"features,omitempty" url:"features,omitempty"`
Force *types.CustomBool `json:"force,omitempty" url:"force,omitempty,int"` Force *types.CustomBool `json:"force,omitempty" url:"force,omitempty,int"`
HookScript *string `json:"hookscript,omitempty" url:"hookscript,omitempty"` HookScript *string `json:"hookscript,omitempty" url:"hookscript,omitempty"`
Hostname *string `json:"hostname,omitempty" url:"hostname,omitempty"` Hostname *string `json:"hostname,omitempty" url:"hostname,omitempty"`
IgnoreUnpackErrors *types.CustomBool `json:"ignore-unpack-errors,omitempty" url:"force,omitempty,int"` IgnoreUnpackErrors *types.CustomBool `json:"ignore-unpack-errors,omitempty" url:"force,omitempty,int"`
Lock *string `json:"lock,omitempty" url:"lock,omitempty,int"` Lock *string `json:"lock,omitempty" url:"lock,omitempty,int"`
MountPoints CustomMountPointArray `json:"mp,omitempty" url:"mp,omitempty,numbered"` MountPoints CustomMountPoints `json:"mp,omitempty" url:"mp,omitempty,numbered"`
DevicePassthrough CustomDevicePassthroughArray `json:"dev,omitempty" url:"dev,omitempty,numbered"` PassthroughDevices CustomPassthroughDevices `json:"dev,omitempty" url:"dev,omitempty,numbered"`
NetworkInterfaces CustomNetworkInterfaceArray `json:"net,omitempty" url:"net,omitempty,numbered"` NetworkInterfaces CustomNetworkInterfaces `json:"net,omitempty" url:"net,omitempty,numbered"`
OSTemplateFileVolume *string `json:"ostemplate,omitempty" url:"ostemplate,omitempty"` OSTemplateFileVolume *string `json:"ostemplate,omitempty" url:"ostemplate,omitempty"`
OSType *string `json:"ostype,omitempty" url:"ostype,omitempty"` OSType *string `json:"ostype,omitempty" url:"ostype,omitempty"`
Password *string `json:"password,omitempty" url:"password,omitempty"` Password *string `json:"password,omitempty" url:"password,omitempty"`
PoolID *string `json:"pool,omitempty" url:"pool,omitempty"` PoolID *string `json:"pool,omitempty" url:"pool,omitempty"`
Protection *types.CustomBool `json:"protection,omitempty" url:"protection,omitempty,int"` Protection *types.CustomBool `json:"protection,omitempty" url:"protection,omitempty,int"`
Restore *types.CustomBool `json:"restore,omitempty" url:"restore,omitempty,int"` Restore *types.CustomBool `json:"restore,omitempty" url:"restore,omitempty,int"`
RootFS *CustomRootFS `json:"rootfs,omitempty" url:"rootfs,omitempty"` RootFS *CustomRootFS `json:"rootfs,omitempty" url:"rootfs,omitempty"`
SSHKeys *CustomSSHKeys `json:"ssh-public-keys,omitempty" url:"ssh-public-keys,omitempty"` SSHKeys *CustomSSHKeys `json:"ssh-public-keys,omitempty" url:"ssh-public-keys,omitempty"`
Start *types.CustomBool `json:"start,omitempty" url:"start,omitempty,int"` Start *types.CustomBool `json:"start,omitempty" url:"start,omitempty,int"`
StartOnBoot *types.CustomBool `json:"onboot,omitempty" url:"onboot,omitempty,int"` StartOnBoot *types.CustomBool `json:"onboot,omitempty" url:"onboot,omitempty,int"`
StartupBehavior *CustomStartupBehavior `json:"startup,omitempty" url:"startup,omitempty"` StartupBehavior *CustomStartupBehavior `json:"startup,omitempty" url:"startup,omitempty"`
Swap *int `json:"swap,omitempty" url:"swap,omitempty"` Swap *int `json:"swap,omitempty" url:"swap,omitempty"`
Tags *string `json:"tags,omitempty" url:"tags,omitempty"` Tags *string `json:"tags,omitempty" url:"tags,omitempty"`
Template *types.CustomBool `json:"template,omitempty" url:"template,omitempty,int"` Template *types.CustomBool `json:"template,omitempty" url:"template,omitempty,int"`
TTY *int `json:"tty,omitempty" url:"tty,omitempty"` TTY *int `json:"tty,omitempty" url:"tty,omitempty"`
Unique *types.CustomBool `json:"unique,omitempty" url:"unique,omitempty,int"` Unique *types.CustomBool `json:"unique,omitempty" url:"unique,omitempty,int"`
Unprivileged *types.CustomBool `json:"unprivileged,omitempty" url:"unprivileged,omitempty,int"` Unprivileged *types.CustomBool `json:"unprivileged,omitempty" url:"unprivileged,omitempty,int"`
VMID *int `json:"vmid,omitempty" url:"vmid,omitempty"` VMID *int `json:"vmid,omitempty" url:"vmid,omitempty"`
} }
// CustomFeatures contains the values for the "features" property. // CustomFeatures contains the values for the "features" property.
@ -96,8 +106,8 @@ type CustomMountPoint struct {
Volume string `json:"volume" url:"volume"` Volume string `json:"volume" url:"volume"`
} }
// CustomMountPointArray is an array of CustomMountPoint. // CustomMountPoints is a map of CustomMountPoint per mount point ID (i.e. `mp1`).
type CustomMountPointArray []CustomMountPoint type CustomMountPoints map[string]*CustomMountPoint
// CustomNetworkInterface contains the values for the "net[n]" properties. // CustomNetworkInterface contains the values for the "net[n]" properties.
type CustomNetworkInterface struct { type CustomNetworkInterface struct {
@ -117,11 +127,11 @@ type CustomNetworkInterface struct {
Type *string `json:"type,omitempty" url:"type,omitempty"` Type *string `json:"type,omitempty" url:"type,omitempty"`
} }
// CustomDevicePassthroughArray is an array of CustomDevicePassthrough. // CustomPassthroughDevices is a map of CustomPassthroughDevice per passthrough device ID (i.e. `dev0`).
type CustomDevicePassthroughArray []CustomDevicePassthrough type CustomPassthroughDevices map[string]*CustomPassthroughDevice
// CustomDevicePassthrough contains the values for the "dev[n]" properties. // CustomPassthroughDevice contains the values for the "dev[n]" properties.
type CustomDevicePassthrough struct { type CustomPassthroughDevice struct {
DenyWrite *types.CustomBool `json:"deny-write,omitempty" url:"deny-write,omitempty,int"` DenyWrite *types.CustomBool `json:"deny-write,omitempty" url:"deny-write,omitempty,int"`
Path string `json:"path" url:"path"` Path string `json:"path" url:"path"`
UID *int `json:"uid,omitempty" url:"uid,omitempty"` UID *int `json:"uid,omitempty" url:"uid,omitempty"`
@ -129,8 +139,8 @@ type CustomDevicePassthrough struct {
Mode *string `json:"mode,omitempty" url:"mode,omitempty"` Mode *string `json:"mode,omitempty" url:"mode,omitempty"`
} }
// CustomNetworkInterfaceArray is an array of CustomNetworkInterface. // CustomNetworkInterfaces is a map of CustomNetworkInterface per network interface ID (i.e. `net0`).
type CustomNetworkInterfaceArray []CustomNetworkInterface type CustomNetworkInterfaces map[string]*CustomNetworkInterface
// CustomRootFS contains the values for the "rootfs" property. // CustomRootFS contains the values for the "rootfs" property.
type CustomRootFS struct { type CustomRootFS struct {
@ -184,30 +194,9 @@ type GetResponseData struct {
Hostname *string `json:"hostname,omitempty"` Hostname *string `json:"hostname,omitempty"`
Lock *types.CustomBool `json:"lock,omitempty"` Lock *types.CustomBool `json:"lock,omitempty"`
LXCConfiguration *[][2]string `json:"lxc,omitempty"` LXCConfiguration *[][2]string `json:"lxc,omitempty"`
DevicePassthrough0 *CustomDevicePassthrough `json:"dev0,omitempty"` MountPoints CustomMountPoints `json:"mp,omitempty"`
DevicePassthrough1 *CustomDevicePassthrough `json:"dev1,omitempty"` PassthroughDevices CustomPassthroughDevices `json:"dev,omitempty"`
DevicePassthrough2 *CustomDevicePassthrough `json:"dev2,omitempty"` NetworkInterfaces CustomNetworkInterfaces `json:"net,omitempty"`
DevicePassthrough3 *CustomDevicePassthrough `json:"dev3,omitempty"`
DevicePassthrough4 *CustomDevicePassthrough `json:"dev4,omitempty"`
DevicePassthrough5 *CustomDevicePassthrough `json:"dev5,omitempty"`
DevicePassthrough6 *CustomDevicePassthrough `json:"dev6,omitempty"`
DevicePassthrough7 *CustomDevicePassthrough `json:"dev7,omitempty"`
MountPoint0 *CustomMountPoint `json:"mp0,omitempty"`
MountPoint1 *CustomMountPoint `json:"mp1,omitempty"`
MountPoint2 *CustomMountPoint `json:"mp2,omitempty"`
MountPoint3 *CustomMountPoint `json:"mp3,omitempty"`
MountPoint4 *CustomMountPoint `json:"mp4,omitempty"`
MountPoint5 *CustomMountPoint `json:"mp5,omitempty"`
MountPoint6 *CustomMountPoint `json:"mp6,omitempty"`
MountPoint7 *CustomMountPoint `json:"mp7,omitempty"`
NetworkInterface0 *CustomNetworkInterface `json:"net0,omitempty"`
NetworkInterface1 *CustomNetworkInterface `json:"net1,omitempty"`
NetworkInterface2 *CustomNetworkInterface `json:"net2,omitempty"`
NetworkInterface3 *CustomNetworkInterface `json:"net3,omitempty"`
NetworkInterface4 *CustomNetworkInterface `json:"net4,omitempty"`
NetworkInterface5 *CustomNetworkInterface `json:"net5,omitempty"`
NetworkInterface6 *CustomNetworkInterface `json:"net6,omitempty"`
NetworkInterface7 *CustomNetworkInterface `json:"net7,omitempty"`
OSType *string `json:"ostype,omitempty"` OSType *string `json:"ostype,omitempty"`
Protection *types.CustomBool `json:"protection,omitempty"` Protection *types.CustomBool `json:"protection,omitempty"`
RootFS *CustomRootFS `json:"rootfs,omitempty"` RootFS *CustomRootFS `json:"rootfs,omitempty"`
@ -299,8 +288,8 @@ func (r *CustomFeatures) EncodeValues(key string, v *url.Values) error {
return nil return nil
} }
// EncodeValues converts a CustomDevicePassthrough struct to a URL value. // EncodeValues converts a CustomPassthroughDevice struct to a URL value.
func (r *CustomDevicePassthrough) EncodeValues(key string, v *url.Values) error { func (r *CustomPassthroughDevice) EncodeValues(key string, v *url.Values) error {
var values []string var values []string
if r.DenyWrite != nil { if r.DenyWrite != nil {
@ -334,14 +323,14 @@ func (r *CustomDevicePassthrough) EncodeValues(key string, v *url.Values) error
return nil return nil
} }
// EncodeValues converts a CustomDevicePassthroughArray array to multiple URL values. // EncodeValues converts a CustomPassthroughDevices array to multiple URL values.
func (r CustomDevicePassthroughArray) EncodeValues( func (r CustomPassthroughDevices) EncodeValues(
key string, _ string,
v *url.Values, v *url.Values,
) error { ) error {
for i, d := range r { for key, d := range r {
if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil { if err := d.EncodeValues(key, v); err != nil {
return fmt.Errorf("failed to encode CustomDevicePassthroughArray: %w", err) return fmt.Errorf("failed to encode CustomPassthroughDevices: %w", err)
} }
} }
@ -354,7 +343,7 @@ func (r *CustomMountPoint) EncodeValues(key string, v *url.Values) error {
if r.ACL != nil { if r.ACL != nil {
if *r.ACL { if *r.ACL {
values = append(values, "acl=%d") values = append(values, "acl=1")
} else { } else {
values = append(values, "acl=0") values = append(values, "acl=0")
} }
@ -421,14 +410,14 @@ func (r *CustomMountPoint) EncodeValues(key string, v *url.Values) error {
return nil return nil
} }
// EncodeValues converts a CustomMountPointArray array to multiple URL values. // EncodeValues converts a CustomMountPoints array to multiple URL values.
func (r CustomMountPointArray) EncodeValues( func (r CustomMountPoints) EncodeValues(
key string, _ string,
v *url.Values, v *url.Values,
) error { ) error {
for i, d := range r { for key, d := range r {
if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil { if err := d.EncodeValues(key, v); err != nil {
return fmt.Errorf("failed to encode CustomMountPointArray: %w", err) return fmt.Errorf("failed to encode CustomMountPoints: %w", err)
} }
} }
@ -509,14 +498,14 @@ func (r *CustomNetworkInterface) EncodeValues(
return nil return nil
} }
// EncodeValues converts a CustomNetworkInterfaceArray array to multiple URL values. // EncodeValues converts a CustomNetworkInterfaces array to multiple URL values.
func (r CustomNetworkInterfaceArray) EncodeValues( func (r CustomNetworkInterfaces) EncodeValues(
key string, _ string,
v *url.Values, v *url.Values,
) error { ) error {
for i, d := range r { for key, d := range r {
if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil { if err := d.EncodeValues(key, v); err != nil {
return fmt.Errorf("failed to encode CustomNetworkInterfaceArray: %w", err) return fmt.Errorf("failed to encode CustomNetworkInterfaces: %w", err)
} }
} }
@ -529,7 +518,7 @@ func (r *CustomRootFS) EncodeValues(key string, v *url.Values) error {
if r.ACL != nil { if r.ACL != nil {
if *r.ACL { if *r.ACL {
values = append(values, "acl=%d") values = append(values, "acl=1")
} else { } else {
values = append(values, "acl=0") values = append(values, "acl=0")
} }
@ -562,7 +551,7 @@ func (r *CustomRootFS) EncodeValues(key string, v *url.Values) error {
} }
if r.Replicate != nil { if r.Replicate != nil {
if *r.ReadOnly { if *r.Replicate {
values = append(values, "replicate=1") values = append(values, "replicate=1")
} else { } else {
values = append(values, "replicate=0") values = append(values, "replicate=0")
@ -659,13 +648,13 @@ func (r *CustomFeatures) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// UnmarshalJSON converts a CustomDevicePassthrough string to an object. // UnmarshalJSON converts a CustomPassthroughDevice string to an object.
func (r *CustomDevicePassthrough) UnmarshalJSON(b []byte) error { func (r *CustomPassthroughDevice) UnmarshalJSON(b []byte) error {
var s string var s string
err := json.Unmarshal(b, &s) err := json.Unmarshal(b, &s)
if err != nil { if err != nil {
return fmt.Errorf("unable to unmarshal CustomDevicePassthrough: %w", err) return fmt.Errorf("unable to unmarshal CustomPassthroughDevice: %w", err)
} }
pairs := strings.Split(s, ",") pairs := strings.Split(s, ",")
@ -947,3 +936,66 @@ func (r *CustomStartupBehavior) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// UnmarshalJSON unmarshals the data from the JSON response, populating the CustomStorageDevices field.
func (d *GetResponseData) UnmarshalJSON(b []byte) error {
type Alias GetResponseData
var data Alias
// get original struct
if err := json.Unmarshal(b, &data); err != nil {
return fmt.Errorf("failed to unmarshal data: %w", err)
}
var byAttr map[string]interface{}
// now get map by attribute name
err := json.Unmarshal(b, &byAttr)
if err != nil {
return fmt.Errorf("failed to unmarshal data: %w", err)
}
data.PassthroughDevices = make(CustomPassthroughDevices)
data.NetworkInterfaces = make(CustomNetworkInterfaces)
data.MountPoints = make(CustomMountPoints)
for key, value := range byAttr {
valueStr, ok := value.(string)
if !ok {
continue // Skip non-string values
}
jsonValue := []byte(`"` + valueStr + `"`)
switch {
case regexDeviceKey.MatchString(key):
var device CustomPassthroughDevice
if e := json.Unmarshal(jsonValue, &device); e != nil {
return fmt.Errorf("failed to unmarshal %s with value %q: %w", key, valueStr, e)
}
data.PassthroughDevices[key] = &device
case regexNetworkKey.MatchString(key):
var net CustomNetworkInterface
if e := json.Unmarshal(jsonValue, &net); e != nil {
return fmt.Errorf("failed to unmarshal %s with value %q: %w", key, valueStr, e)
}
data.NetworkInterfaces[key] = &net
case regexMountPointKey.MatchString(key):
var mp CustomMountPoint
if e := json.Unmarshal(jsonValue, &mp); e != nil {
return fmt.Errorf("failed to unmarshal %s with value %q: %w", key, valueStr, e)
}
data.MountPoints[key] = &mp
}
}
*d = GetResponseData(data)
return nil
}

View File

@ -17,6 +17,18 @@ import (
"github.com/bpg/terraform-provider-proxmox/proxmox/types" "github.com/bpg/terraform-provider-proxmox/proxmox/types"
) )
var (
//nolint:gochecknoglobals
// regexStorageInterface is a regex pattern for matching storage interface names.
regexStorageInterface = func(prefix string) *regexp.Regexp {
return regexp.MustCompile(`^` + prefix + `\d+$`)
}
// regexPCIDevice is a regex pattern for matching PCI device names.
regexPCIDevice = regexp.MustCompile(`^hostpci\d+$`)
// regexVirtiofsShare is a regex pattern for matching virtiofs share names.
regexVirtiofsShare = regexp.MustCompile(`^virtiofs\d+$`)
)
// CloneRequestBody contains the data for an virtual machine clone request. // CloneRequestBody contains the data for an virtual machine clone request.
type CloneRequestBody struct { type CloneRequestBody struct {
BandwidthLimit *int `json:"bwlimit,omitempty" url:"bwlimit,omitempty"` BandwidthLimit *int `json:"bwlimit,omitempty" url:"bwlimit,omitempty"`
@ -477,8 +489,7 @@ func (d *GetResponseData) UnmarshalJSON(b []byte) error {
for _, prefix := range StorageInterfaces { for _, prefix := range StorageInterfaces {
// the device names can overlap with other fields, for example`scsi0` and `scsihw`, so just checking // the device names can overlap with other fields, for example`scsi0` and `scsihw`, so just checking
// the prefix is not enough // the prefix is not enough
r := regexp.MustCompile(`^` + prefix + `\d+$`) if regexStorageInterface(prefix).MatchString(key) {
if r.MatchString(key) {
var device CustomStorageDevice var device CustomStorageDevice
if err := json.Unmarshal([]byte(`"`+value.(string)+`"`), &device); err != nil { if err := json.Unmarshal([]byte(`"`+value.(string)+`"`), &device); err != nil {
return fmt.Errorf("failed to unmarshal %s: %w", key, err) return fmt.Errorf("failed to unmarshal %s: %w", key, err)
@ -488,7 +499,7 @@ func (d *GetResponseData) UnmarshalJSON(b []byte) error {
} }
} }
if r := regexp.MustCompile(`^hostpci\d+$`); r.MatchString(key) { if regexPCIDevice.MatchString(key) {
var device CustomPCIDevice var device CustomPCIDevice
if err := json.Unmarshal([]byte(`"`+value.(string)+`"`), &device); err != nil { if err := json.Unmarshal([]byte(`"`+value.(string)+`"`), &device); err != nil {
return fmt.Errorf("failed to unmarshal %s: %w", key, err) return fmt.Errorf("failed to unmarshal %s: %w", key, err)
@ -497,7 +508,7 @@ func (d *GetResponseData) UnmarshalJSON(b []byte) error {
data.PCIDevices[key] = &device data.PCIDevices[key] = &device
} }
if r := regexp.MustCompile(`^virtiofs\d+$`); r.MatchString(key) { if regexVirtiofsShare.MatchString(key) {
var share CustomVirtiofsShare var share CustomVirtiofsShare
if err := json.Unmarshal([]byte(`"`+value.(string)+`"`), &share); err != nil { if err := json.Unmarshal([]byte(`"`+value.(string)+`"`), &share); err != nil {
return fmt.Errorf("failed to unmarshal %s: %w", key, err) return fmt.Errorf("failed to unmarshal %s: %w", key, err)

View File

@ -89,7 +89,9 @@ const (
dvTimeoutDelete = 60 dvTimeoutDelete = 60
dvUnprivileged = false dvUnprivileged = false
maxResourceVirtualEnvironmentContainerNetworkInterfaces = 8 maxNetworkInterfaces = 10
maxPassthroughDevices = 8
maxMountPoints = 256
mkClone = "clone" mkClone = "clone"
mkCloneDatastoreID = "datastore_id" mkCloneDatastoreID = "datastore_id"
@ -686,7 +688,7 @@ func Container() *schema.Resource {
}, },
}, },
}, },
MaxItems: 8, MaxItems: maxMountPoints,
MinItems: 0, MinItems: 0,
}, },
mkDevicePassthrough: { mkDevicePassthrough: {
@ -730,7 +732,7 @@ func Container() *schema.Resource {
}, },
}, },
}, },
MaxItems: 8, MaxItems: maxPassthroughDevices,
MinItems: 0, MinItems: 0,
}, },
mkNetworkInterface: { mkNetworkInterface: {
@ -795,7 +797,7 @@ func Container() *schema.Resource {
}, },
}, },
}, },
MaxItems: maxResourceVirtualEnvironmentContainerNetworkInterfaces, MaxItems: maxNetworkInterfaces,
MinItems: 0, MinItems: 0,
}, },
mkNodeName: { mkNodeName: {
@ -1264,14 +1266,14 @@ func containerCreateClone(ctx context.Context, d *schema.ResourceData, m interfa
devicePassthrough := d.Get(mkDevicePassthrough).([]interface{}) devicePassthrough := d.Get(mkDevicePassthrough).([]interface{})
devicePassthroughArray := make( passthroughDevices := make(
containers.CustomDevicePassthroughArray, containers.CustomPassthroughDevices,
len(devicePassthrough), len(devicePassthrough),
) )
for di, dv := range devicePassthrough { for di, dv := range devicePassthrough {
devicePassthroughMap := dv.(map[string]interface{}) devicePassthroughMap := dv.(map[string]interface{})
devicePassthroughObject := containers.CustomDevicePassthrough{} devicePassthroughObject := containers.CustomPassthroughDevice{}
denyWrite := types.CustomBool( denyWrite := types.CustomBool(
devicePassthroughMap[mkDevicePassthroughDenyWrite].(bool), devicePassthroughMap[mkDevicePassthroughDenyWrite].(bool),
@ -1287,10 +1289,10 @@ func containerCreateClone(ctx context.Context, d *schema.ResourceData, m interfa
devicePassthroughObject.Path = path devicePassthroughObject.Path = path
devicePassthroughObject.UID = &uid devicePassthroughObject.UID = &uid
devicePassthroughArray[di] = devicePassthroughObject passthroughDevices[fmt.Sprintf("dev%d", di)] = &devicePassthroughObject
} }
updateBody.DevicePassthrough = devicePassthroughArray updateBody.PassthroughDevices = passthroughDevices
networkInterface := d.Get(mkNetworkInterface).([]interface{}) networkInterface := d.Get(mkNetworkInterface).([]interface{})
@ -1301,8 +1303,8 @@ func containerCreateClone(ctx context.Context, d *schema.ResourceData, m interfa
} }
} }
networkInterfaceArray := make( networkInterfaces := make(
containers.CustomNetworkInterfaceArray, containers.CustomNetworkInterfaces,
len(networkInterface), len(networkInterface),
) )
@ -1364,18 +1366,18 @@ func containerCreateClone(ctx context.Context, d *schema.ResourceData, m interfa
networkInterfaceObject.MTU = &mtu networkInterfaceObject.MTU = &mtu
} }
networkInterfaceArray[ni] = networkInterfaceObject networkInterfaces[fmt.Sprintf("net%d", ni)] = &networkInterfaceObject
} }
updateBody.NetworkInterfaces = networkInterfaceArray updateBody.NetworkInterfaces = networkInterfaces
for i, ni := range updateBody.NetworkInterfaces { for key, ni := range updateBody.NetworkInterfaces {
if !ni.Enabled { if !ni.Enabled {
updateBody.Delete = append(updateBody.Delete, fmt.Sprintf("net%d", i)) updateBody.Delete = append(updateBody.Delete, key)
} }
} }
for i := len(updateBody.NetworkInterfaces); i < maxResourceVirtualEnvironmentContainerNetworkInterfaces; i++ { for i := len(updateBody.NetworkInterfaces); i < maxNetworkInterfaces; i++ {
updateBody.Delete = append(updateBody.Delete, fmt.Sprintf("net%d", i)) updateBody.Delete = append(updateBody.Delete, fmt.Sprintf("net%d", i))
} }
@ -1604,11 +1606,11 @@ func containerCreateCustom(ctx context.Context, d *schema.ResourceData, m interf
memorySwap := memoryBlock[mkMemorySwap].(int) memorySwap := memoryBlock[mkMemorySwap].(int)
mountPoint := d.Get(mkMountPoint).([]interface{}) mountPoint := d.Get(mkMountPoint).([]interface{})
mountPointArray := make(containers.CustomMountPointArray, 0, len(mountPoint)) mountPoints := make(containers.CustomMountPoints, len(mountPoint))
// because of default bool values: // because of default bool values:
for _, mp := range mountPoint { for mi, mp := range mountPoint {
mountPointMap := mp.(map[string]interface{}) mountPointMap := mp.(map[string]interface{})
mountPointObject := containers.CustomMountPoint{} mountPointObject := containers.CustomMountPoint{}
@ -1675,13 +1677,13 @@ func containerCreateCustom(ctx context.Context, d *schema.ResourceData, m interf
mountPointObject.MountOptions = &mountOptionsArray mountPointObject.MountOptions = &mountOptionsArray
} }
mountPointArray = append(mountPointArray, mountPointObject) mountPoints[fmt.Sprintf("mp%d", mi)] = &mountPointObject
} }
var rootFS *containers.CustomRootFS var rootFS *containers.CustomRootFS
diskSize := diskBlock[mkDiskSize].(int) diskSize := diskBlock[mkDiskSize].(int)
if diskDatastoreID != "" && (diskSize != dvDiskSize || len(mountPointArray) > 0) { if diskDatastoreID != "" && (diskSize != dvDiskSize || len(mountPoints) > 0) {
// This is a special case where the rootfs size is set to a non-default value at creation time. // This is a special case where the rootfs size is set to a non-default value at creation time.
// see https://pve.proxmox.com/pve-docs/chapter-pct.html#_storage_backed_mount_points // see https://pve.proxmox.com/pve-docs/chapter-pct.html#_storage_backed_mount_points
rootFS = &containers.CustomRootFS{ rootFS = &containers.CustomRootFS{
@ -1690,7 +1692,7 @@ func containerCreateCustom(ctx context.Context, d *schema.ResourceData, m interf
} }
networkInterface := d.Get(mkNetworkInterface).([]interface{}) networkInterface := d.Get(mkNetworkInterface).([]interface{})
networkInterfaceArray := make(containers.CustomNetworkInterfaceArray, len(networkInterface)) networkInterfaces := make(containers.CustomNetworkInterfaces, len(networkInterface))
for ni, nv := range networkInterface { for ni, nv := range networkInterface {
networkInterfaceMap := nv.(map[string]interface{}) networkInterfaceMap := nv.(map[string]interface{})
@ -1750,19 +1752,19 @@ func containerCreateCustom(ctx context.Context, d *schema.ResourceData, m interf
networkInterfaceObject.MTU = &mtu networkInterfaceObject.MTU = &mtu
} }
networkInterfaceArray[ni] = networkInterfaceObject networkInterfaces[fmt.Sprintf("net%d", ni)] = &networkInterfaceObject
} }
devicePassthrough := d.Get(mkDevicePassthrough).([]interface{}) devicePassthrough := d.Get(mkDevicePassthrough).([]interface{})
devicePassthroughArray := make( passthroughDevices := make(
containers.CustomDevicePassthroughArray, containers.CustomPassthroughDevices,
len(devicePassthrough), len(devicePassthrough),
) )
for di, dv := range devicePassthrough { for di, dv := range devicePassthrough {
devicePassthroughMap := dv.(map[string]interface{}) devicePassthroughMap := dv.(map[string]interface{})
devicePassthroughObject := containers.CustomDevicePassthrough{} devicePassthroughObject := containers.CustomPassthroughDevice{}
denyWrite := types.CustomBool( denyWrite := types.CustomBool(
devicePassthroughMap[mkDevicePassthroughDenyWrite].(bool), devicePassthroughMap[mkDevicePassthroughDenyWrite].(bool),
@ -1778,7 +1780,7 @@ func containerCreateCustom(ctx context.Context, d *schema.ResourceData, m interf
devicePassthroughObject.Path = path devicePassthroughObject.Path = path
devicePassthroughObject.UID = &uid devicePassthroughObject.UID = &uid
devicePassthroughArray[di] = devicePassthroughObject passthroughDevices[fmt.Sprintf("dev%d", di)] = &devicePassthroughObject
} }
operatingSystem := d.Get(mkOperatingSystem).([]interface{}) operatingSystem := d.Get(mkOperatingSystem).([]interface{})
@ -1828,10 +1830,10 @@ func containerCreateCustom(ctx context.Context, d *schema.ResourceData, m interf
CPUUnits: &cpuUnits, CPUUnits: &cpuUnits,
DatastoreID: &diskDatastoreID, DatastoreID: &diskDatastoreID,
DedicatedMemory: &memoryDedicated, DedicatedMemory: &memoryDedicated,
DevicePassthrough: devicePassthroughArray, PassthroughDevices: passthroughDevices,
Features: features, Features: features,
MountPoints: mountPointArray, MountPoints: mountPoints,
NetworkInterfaces: networkInterfaceArray, NetworkInterfaces: networkInterfaces,
OSTemplateFileVolume: &operatingSystemTemplateFileID, OSTemplateFileVolume: &operatingSystemTemplateFileID,
OSType: &operatingSystemType, OSType: &operatingSystemType,
Protection: &protection, Protection: &protection,
@ -1941,25 +1943,9 @@ func containerGetExistingNetworkInterface(
return []interface{}{}, fmt.Errorf("error getting container information: %w", err) return []interface{}{}, fmt.Errorf("error getting container information: %w", err)
} }
//nolint:prealloc networkInterfaces := make([]interface{}, 0, len(containerInfo.NetworkInterfaces))
var networkInterfaces []interface{}
networkInterfaceArray := []*containers.CustomNetworkInterface{
containerInfo.NetworkInterface0,
containerInfo.NetworkInterface1,
containerInfo.NetworkInterface2,
containerInfo.NetworkInterface3,
containerInfo.NetworkInterface4,
containerInfo.NetworkInterface5,
containerInfo.NetworkInterface6,
containerInfo.NetworkInterface7,
}
for _, nv := range networkInterfaceArray {
if nv == nil {
continue
}
for _, nv := range containerInfo.NetworkInterfaces {
networkInterface := map[string]interface{}{} networkInterface := map[string]interface{}{}
networkInterface[mkNetworkInterfaceEnabled] = true networkInterface[mkNetworkInterfaceEnabled] = true
@ -2343,85 +2329,56 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d
initialization[mkInitializationHostname] = "" initialization[mkInitializationHostname] = ""
} }
devicePassthroughArray := []*containers.CustomDevicePassthrough{ passthroughDevicesMap := make(map[string]interface{}, len(containerConfig.PassthroughDevices))
containerConfig.DevicePassthrough0,
containerConfig.DevicePassthrough1,
containerConfig.DevicePassthrough2,
containerConfig.DevicePassthrough3,
containerConfig.DevicePassthrough4,
containerConfig.DevicePassthrough5,
containerConfig.DevicePassthrough6,
containerConfig.DevicePassthrough7,
}
devicePassthroughList := make([]interface{}, 0, len(devicePassthroughArray)) for key, dp := range containerConfig.PassthroughDevices {
passthroughDevice := map[string]interface{}{}
for _, dp := range devicePassthroughArray {
if dp == nil {
continue
}
devicePassthrough := map[string]interface{}{}
if dp.DenyWrite != nil { if dp.DenyWrite != nil {
devicePassthrough[mkDevicePassthroughDenyWrite] = *dp.DenyWrite passthroughDevice[mkDevicePassthroughDenyWrite] = *dp.DenyWrite
} else { } else {
devicePassthrough[mkDevicePassthroughDenyWrite] = false passthroughDevice[mkDevicePassthroughDenyWrite] = false
} }
if dp.GID != nil { if dp.GID != nil {
devicePassthrough[mkDevicePassthroughGID] = *dp.GID passthroughDevice[mkDevicePassthroughGID] = *dp.GID
} else { } else {
devicePassthrough[mkDevicePassthroughGID] = 0 passthroughDevice[mkDevicePassthroughGID] = 0
} }
if dp.Mode != nil { if dp.Mode != nil {
devicePassthrough[mkDevicePassthroughMode] = *dp.Mode passthroughDevice[mkDevicePassthroughMode] = *dp.Mode
} else { } else {
devicePassthrough[mkDevicePassthroughMode] = dvDevicePassthroughMode passthroughDevice[mkDevicePassthroughMode] = dvDevicePassthroughMode
} }
devicePassthrough[mkDevicePassthroughPath] = dp.Path passthroughDevice[mkDevicePassthroughPath] = dp.Path
if dp.UID != nil { if dp.UID != nil {
devicePassthrough[mkDevicePassthroughUID] = *dp.UID passthroughDevice[mkDevicePassthroughUID] = *dp.UID
} else { } else {
devicePassthrough[mkDevicePassthroughUID] = 0 passthroughDevice[mkDevicePassthroughUID] = 0
} }
devicePassthroughList = append(devicePassthroughList, devicePassthrough) passthroughDevicesMap[key] = passthroughDevice
} }
currentDevicePassthrough := d.Get(mkDevicePassthrough).([]interface{}) passthroughDevices := utils.OrderedListFromMap(passthroughDevicesMap)
currentPassthroughDevices := d.Get(mkDevicePassthrough).([]interface{})
if len(clone) > 0 { if len(clone) > 0 {
if len(currentDevicePassthrough) > 0 { if len(currentPassthroughDevices) > 0 {
err := d.Set(mkDevicePassthrough, devicePassthroughList) err := d.Set(mkDevicePassthrough, passthroughDevices)
diags = append(diags, diag.FromErr(err)...) diags = append(diags, diag.FromErr(err)...)
} }
} else if len(devicePassthroughList) > 0 { } else if len(passthroughDevicesMap) > 0 {
err := d.Set(mkDevicePassthrough, devicePassthroughList) err := d.Set(mkDevicePassthrough, passthroughDevices)
diags = append(diags, diag.FromErr(err)...) diags = append(diags, diag.FromErr(err)...)
} }
mountPointArray := []*containers.CustomMountPoint{ mountPointsMap := make(map[string]interface{}, len(containerConfig.MountPoints))
containerConfig.MountPoint0,
containerConfig.MountPoint1,
containerConfig.MountPoint2,
containerConfig.MountPoint3,
containerConfig.MountPoint4,
containerConfig.MountPoint5,
containerConfig.MountPoint6,
containerConfig.MountPoint7,
}
mountPointList := make([]interface{}, 0, len(mountPointArray))
for _, mp := range mountPointArray {
if mp == nil {
continue
}
for key, mp := range containerConfig.MountPoints {
mountPoint := map[string]interface{}{} mountPoint := map[string]interface{}{}
if mp.ACL != nil { if mp.ACL != nil {
@ -2476,42 +2433,27 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d
mountPoint[mkMountPointVolume] = mp.Volume mountPoint[mkMountPointVolume] = mp.Volume
mountPointList = append(mountPointList, mountPoint) mountPointsMap[key] = mountPoint
} }
currentMountPoint := d.Get(mkMountPoint).([]interface{}) mountPoints := utils.OrderedListFromMap(mountPointsMap)
currentMountPoints := d.Get(mkMountPoint).([]interface{})
if len(clone) > 0 { if len(clone) > 0 {
if len(currentMountPoint) > 0 { if len(currentMountPoints) > 0 {
err := d.Set(mkMountPoint, mountPointList) err := d.Set(mkMountPoint, mountPoints)
diags = append(diags, diag.FromErr(err)...) diags = append(diags, diag.FromErr(err)...)
} }
} else if len(mountPointList) > 0 { } else if len(mountPointsMap) > 0 {
err := d.Set(mkMountPoint, mountPointList) err := d.Set(mkMountPoint, mountPoints)
diags = append(diags, diag.FromErr(err)...) diags = append(diags, diag.FromErr(err)...)
} }
var ipConfigList []interface{} var ipConfigList []interface{}
networkInterfaceArray := []*containers.CustomNetworkInterface{ networkInterfacesMap := make(map[string]interface{}, len(containerConfig.NetworkInterfaces))
containerConfig.NetworkInterface0,
containerConfig.NetworkInterface1,
containerConfig.NetworkInterface2,
containerConfig.NetworkInterface3,
containerConfig.NetworkInterface4,
containerConfig.NetworkInterface5,
containerConfig.NetworkInterface6,
containerConfig.NetworkInterface7,
}
//nolint:prealloc
var networkInterfaceList []interface{}
for _, nv := range networkInterfaceArray {
if nv == nil {
continue
}
for key, nv := range containerConfig.NetworkInterfaces {
if nv.IPv4Address != nil || nv.IPv4Gateway != nil || nv.IPv6Address != nil || if nv.IPv4Address != nil || nv.IPv4Gateway != nil || nv.IPv6Address != nil ||
nv.IPv6Gateway != nil { nv.IPv6Gateway != nil {
ipConfig := map[string]interface{}{} ipConfig := map[string]interface{}{}
@ -2604,9 +2546,10 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d
networkInterface[mkNetworkInterfaceMTU] = 0 networkInterface[mkNetworkInterfaceMTU] = 0
} }
networkInterfaceList = append(networkInterfaceList, networkInterface) networkInterfacesMap[key] = networkInterface
} }
networkInterfaces := utils.OrderedListFromMap(networkInterfacesMap)
initialization[mkInitializationIPConfig] = ipConfigList initialization[mkInitializationIPConfig] = ipConfigList
currentInitialization := d.Get(mkInitialization).([]interface{}) currentInitialization := d.Get(mkInitialization).([]interface{})
@ -2655,7 +2598,7 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d
if len(currentNetworkInterface) > 0 { if len(currentNetworkInterface) > 0 {
err := d.Set( err := d.Set(
mkNetworkInterface, mkNetworkInterface,
networkInterfaceList, networkInterfaces,
) )
diags = append(diags, diag.FromErr(err)...) diags = append(diags, diag.FromErr(err)...)
} }
@ -2668,7 +2611,7 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d
diags = append(diags, diag.FromErr(e)...) diags = append(diags, diag.FromErr(e)...)
err := d.Set(mkNetworkInterface, networkInterfaceList) err := d.Set(mkNetworkInterface, networkInterfaces)
diags = append(diags, diag.FromErr(err)...) diags = append(diags, diag.FromErr(err)...)
} }
@ -3051,14 +2994,14 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{})
_, newDevicePassthrough := d.GetChange(mkDevicePassthrough) _, newDevicePassthrough := d.GetChange(mkDevicePassthrough)
devicePassthrough := newDevicePassthrough.([]interface{}) devicePassthrough := newDevicePassthrough.([]interface{})
devicePassthroughArray := make( passthroughDevices := make(
containers.CustomDevicePassthroughArray, containers.CustomPassthroughDevices,
len(devicePassthrough), len(devicePassthrough),
) )
for i, dp := range devicePassthrough { for i, dp := range devicePassthrough {
devicePassthroughMap := dp.(map[string]interface{}) devicePassthroughMap := dp.(map[string]interface{})
devicePassthroughObject := containers.CustomDevicePassthrough{} devicePassthroughObject := containers.CustomPassthroughDevice{}
denyWrite := types.CustomBool(devicePassthroughMap[mkDevicePassthroughDenyWrite].(bool)) denyWrite := types.CustomBool(devicePassthroughMap[mkDevicePassthroughDenyWrite].(bool))
gid := devicePassthroughMap[mkDevicePassthroughGID].(int) gid := devicePassthroughMap[mkDevicePassthroughGID].(int)
@ -3072,10 +3015,10 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{})
devicePassthroughObject.Path = path devicePassthroughObject.Path = path
devicePassthroughObject.UID = &uid devicePassthroughObject.UID = &uid
devicePassthroughArray[i] = devicePassthroughObject passthroughDevices[fmt.Sprintf("dev%d", i)] = &devicePassthroughObject
} }
updateBody.DevicePassthrough = devicePassthroughArray updateBody.PassthroughDevices = passthroughDevices
rebootRequired = true rebootRequired = true
} }
@ -3085,8 +3028,8 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{})
_, newMountPoints := d.GetChange(mkMountPoint) _, newMountPoints := d.GetChange(mkMountPoint)
mountPoints := newMountPoints.([]interface{}) mountPoints := newMountPoints.([]interface{})
mountPointArray := make( mountPointsMap := make(
containers.CustomMountPointArray, containers.CustomMountPoints,
len(mountPoints), len(mountPoints),
) )
@ -3143,10 +3086,10 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{})
mountPointObject.MountOptions = &mountOptionsArray mountPointObject.MountOptions = &mountOptionsArray
} }
mountPointArray[i] = mountPointObject mountPointsMap[fmt.Sprintf("mp%d", i)] = &mountPointObject
} }
updateBody.MountPoints = mountPointArray updateBody.MountPoints = mountPointsMap
rebootRequired = true rebootRequired = true
} }
@ -3163,8 +3106,8 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{})
if d.HasChange(mkInitialization) || if d.HasChange(mkInitialization) ||
d.HasChange(mkNetworkInterface) { d.HasChange(mkNetworkInterface) {
networkInterfaceArray := make( networkInterfaces := make(
containers.CustomNetworkInterfaceArray, containers.CustomNetworkInterfaces,
len(networkInterface), len(networkInterface),
) )
@ -3226,18 +3169,18 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{})
networkInterfaceObject.MTU = &mtu networkInterfaceObject.MTU = &mtu
} }
networkInterfaceArray[ni] = networkInterfaceObject networkInterfaces[fmt.Sprintf("net%d", ni)] = &networkInterfaceObject
} }
updateBody.NetworkInterfaces = networkInterfaceArray updateBody.NetworkInterfaces = networkInterfaces
for i, ni := range updateBody.NetworkInterfaces { for key, ni := range updateBody.NetworkInterfaces {
if !ni.Enabled { if !ni.Enabled {
updateBody.Delete = append(updateBody.Delete, fmt.Sprintf("net%d", i)) updateBody.Delete = append(updateBody.Delete, key)
} }
} }
for i := len(updateBody.NetworkInterfaces); i < maxResourceVirtualEnvironmentContainerNetworkInterfaces; i++ { for i := len(updateBody.NetworkInterfaces); i < maxNetworkInterfaces; i++ {
updateBody.Delete = append(updateBody.Delete, fmt.Sprintf("net%d", i)) updateBody.Delete = append(updateBody.Delete, fmt.Sprintf("net%d", i))
} }

View File

@ -8,10 +8,14 @@ package utils
import ( import (
"reflect" "reflect"
"sort" "slices"
"strconv"
"strings"
) )
// OrderedListFromMap generates a list from a map's values. The values are sorted based on the map's keys. // OrderedListFromMap generates a list from a map's values. The values are sorted based on the map's keys.
// The sorting is done using a custom comparison function that compares the keys with special rules, assuming they
// are strings representing device names or similar, i.e. "disk0", "net1", etc.
func OrderedListFromMap(inputMap map[string]interface{}) []interface{} { func OrderedListFromMap(inputMap map[string]interface{}) []interface{} {
itemCount := len(inputMap) itemCount := len(inputMap)
keyList := make([]string, itemCount) keyList := make([]string, itemCount)
@ -22,11 +26,60 @@ func OrderedListFromMap(inputMap map[string]interface{}) []interface{} {
i++ i++
} }
sort.Strings(keyList) slices.SortFunc(keyList, compareWithPrefix)
return OrderedListFromMapByKeyValues(inputMap, keyList) return OrderedListFromMapByKeyValues(inputMap, keyList)
} }
// CompareWithPrefix compares two string values with special rules:
// - If both start with the same prefix, trims the prefix and compares the rest as numbers if possible.
// - If numbers are equal, falls back to string comparison (preserving digit formatting).
// - If numeric parsing fails, falls back to string comparison.
// - If prefixes differ, compares the whole values as strings.
func compareWithPrefix(a, b string) int {
prefix := commonPrefix(a, b)
if prefix != "" {
aRest := strings.TrimPrefix(a, prefix)
bRest := strings.TrimPrefix(b, prefix)
aNum, aErr := strconv.Atoi(aRest)
bNum, bErr := strconv.Atoi(bRest)
if aErr == nil && bErr == nil {
if aNum != bNum {
if aNum < bNum {
return -1
}
return 1
}
// numeric values equal, fallback to string comparison
return strings.Compare(aRest, bRest)
}
return strings.Compare(aRest, bRest)
}
return strings.Compare(a, b)
}
// commonPrefix returns the longest common prefix of two strings.
func commonPrefix(a, b string) string {
minLen := len(a)
if len(b) < minLen {
minLen = len(b)
}
for i := range minLen {
if a[i] != b[i] {
return a[:i]
}
}
return a[:minLen]
}
// ListResourcesAttributeValue generates a list of strings from a Terraform resource list (which is list of maps). // ListResourcesAttributeValue generates a list of strings from a Terraform resource list (which is list of maps).
// The list is generated by extracting a specific key attribute from each resource. If the attribute is not found in a // The list is generated by extracting a specific key attribute from each resource. If the attribute is not found in a
// resource, it is skipped. // resource, it is skipped.

View File

@ -1,8 +1,16 @@
/*
* 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 utils package utils
import ( import (
"reflect" "reflect"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestOrderedListFromMap(t *testing.T) { func TestOrderedListFromMap(t *testing.T) {
@ -72,3 +80,41 @@ func TestOrderedListFromMapByKeyValues(t *testing.T) {
t.Errorf("OrderedListFromMapByKeyValues() = %v, want %v", result, expected) t.Errorf("OrderedListFromMapByKeyValues() = %v, want %v", result, expected)
} }
} }
func TestCompareWithPrefix(t *testing.T) {
t.Parallel()
type args struct {
a string
b string
}
tests := []struct {
name string
args args
want int
}{
{"equal", args{"a", "a"}, 0},
{"a < b", args{"a", "b"}, -1},
{"b > a", args{"b", "a"}, 1},
{"a < b with prefix", args{"a1", "a2"}, -1},
{"b > a with prefix", args{"a2", "a1"}, 1},
{"a < b with different prefix", args{"a1", "b1"}, -1},
{"b > a with different prefix", args{"b1", "a1"}, 1},
{"a < b with different prefix and numbers", args{"a1", "a10"}, -1},
{"b > a with different prefix and numbers", args{"a10", "a1"}, 1},
{"a < b with different prefix and numbers", args{"a10", "b1"}, -1},
{"b > a with different prefix and numbers", args{"b1", "a10"}, 1},
{"a < b with different prefix and numbers", args{"a10", "b10"}, -1},
{"b > a with different prefix and numbers", args{"b10", "a10"}, 1},
{"b > a with different numbers leading zeros", args{"dev01", "dev001"}, 1},
{"a > b with different numbers leading zeros", args{"dev001", "dev01"}, -1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equalf(t, tt.want, compareWithPrefix(tt.args.a, tt.args.b), "compareWithPrefix(%v, %v)", tt.args.a, tt.args.b)
})
}
}