Skip to content
Draft
10 changes: 10 additions & 0 deletions proxmox/nodes/containers/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,3 +475,13 @@ func (c *Client) WaitForContainerConfigUnlock(ctx context.Context, ignoreErrorRe

return nil
}

// ResizeContainerDisk resizes a container disk.
func (c *Client) ResizeContainerDisk(ctx context.Context, d *ResizeRequestBody) error {
err := c.DoRequest(ctx, http.MethodPut, c.ExpandPath("resize"), d, nil)
if err != nil {
return fmt.Errorf("error resizing container disk: %w", err)
}

return nil
}
5 changes: 5 additions & 0 deletions proxmox/nodes/containers/containers_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,11 @@ type ShutdownRequestBody struct {
Timeout *int `json:"timeout,omitempty" url:"timeout,omitempty"`
}

type ResizeRequestBody struct {
Disk string `json:"disk" url:"disk"`
Size string `json:"size" url:"size"`
}

// UpdateRequestBody contains the data for an user update request.
type UpdateRequestBody CreateRequestBody

Expand Down
132 changes: 126 additions & 6 deletions proxmoxtf/resource/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"context"
"errors"
"fmt"
"reflect"
"regexp"
"slices"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -336,7 +338,6 @@
Type: schema.TypeList,
Description: "The disks",
Optional: true,
ForceNew: true,
DefaultFunc: func() (any, error) {
return []any{
map[string]any{
Expand Down Expand Up @@ -377,7 +378,6 @@
Type: schema.TypeInt,
Description: "The rootfs size in gigabytes",
Optional: true,
ForceNew: true,
Default: dvDiskSize,
ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)),
},
Expand Down Expand Up @@ -666,6 +666,7 @@
Type: schema.TypeList,
Description: "A mount point",
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkMountPointACL: {
Expand Down Expand Up @@ -725,12 +726,14 @@
Description: "Volume size (only used for volume mount points)",
Optional: true,
Default: dvMountPointSize,
ForceNew: true,
ValidateDiagFunc: validators.FileSize(),
},
mkMountPointVolume: {
Type: schema.TypeString,
Description: "Volume, device or directory to mount into the container",
Required: true,
ForceNew: true,
DiffSuppressFunc: func(_, oldVal, newVal string, _ *schema.ResourceData) bool {
// For *new* volume mounts PVE returns an actual volume ID which is saved in the stare,
// so on reapply the provider will try override it:"
Expand Down Expand Up @@ -1053,6 +1056,96 @@
return strconv.Itoa(newValue.(int)) != d.Id()
},
),
// create a customdiff that checks each mount point
customdiff.ForceNewIf(
mkMountPoint,
func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) bool {

Check failure on line 1062 in proxmoxtf/resource/container/container.go

View workflow job for this annotation

GitHub Actions / golangci-lint

any: interface{} can be replaced by any (modernize)
oldRaw, newRaw := d.GetChange(mkMountPoint)
// compare all oldRaw and newRaw entries
oldList, _ := oldRaw.([]interface{})

Check failure on line 1065 in proxmoxtf/resource/container/container.go

View workflow job for this annotation

GitHub Actions / golangci-lint

any: interface{} can be replaced by any (modernize)
newList, _ := newRaw.([]interface{})

Check failure on line 1066 in proxmoxtf/resource/container/container.go

View workflow job for this annotation

GitHub Actions / golangci-lint

any: interface{} can be replaced by any (modernize)

if oldList == nil {
oldList = []interface{}{}

Check failure on line 1069 in proxmoxtf/resource/container/container.go

View workflow job for this annotation

GitHub Actions / golangci-lint

any: interface{} can be replaced by any (modernize)
}
if newList == nil {
newList = []interface{}{}

Check failure on line 1072 in proxmoxtf/resource/container/container.go

View workflow job for this annotation

GitHub Actions / golangci-lint

any: interface{} can be replaced by any (modernize)
}

for i := 0; i < len(oldList); i++ {

Check failure on line 1075 in proxmoxtf/resource/container/container.go

View workflow job for this annotation

GitHub Actions / golangci-lint

for loop can be changed to use an integer range (Go 1.22+) (intrange)
if len(newList)-1 < i {
return true
}
// compare old and new list entries and call ForceNew on the correspondig string

Check failure on line 1079 in proxmoxtf/resource/container/container.go

View workflow job for this annotation

GitHub Actions / golangci-lint

`correspondig` is a misspelling of `corresponding` (misspell)
// make a deep comparison
oldMap, _ := oldList[i].(map[string]interface{})
// the volume entry of oldMap does containe the storage volume PLUS the identifier, which we have to strip
volumeEntry, ok := oldMap["volume"]
if ok {
volumeParts := strings.Split(volumeEntry.(string), ":")
if len(volumeParts) >= 1 {
oldMap["volume"] = volumeParts[0]
}
}

newMap, _ := newList[i].(map[string]interface{})
// deep compare
if !reflect.DeepEqual(oldMap, newMap) {
// get key that is different and call ForceNew
for _, v := range oldMap {
d.ForceNew(fmt.Sprintf("%s.%d.%s", mkMountPoint, i, v))

Check failure on line 1096 in proxmoxtf/resource/container/container.go

View workflow job for this annotation

GitHub Actions / golangci-lint

Error return value of `d.ForceNew` is not checked (errcheck)
}
return true
}
}
return false

Check failure on line 1102 in proxmoxtf/resource/container/container.go

View workflow job for this annotation

GitHub Actions / golangci-lint

File is not properly formatted (gofumpt)
},
),
customdiff.ForceNewIf(
mkDisk,
func(_ context.Context, d *schema.ResourceDiff, _ interface{}) bool {
oldRaw, newRaw := d.GetChange(mkDisk)
oldList, _ := oldRaw.([]interface{})
newList, _ := newRaw.([]interface{})

if oldList == nil {
oldList = []interface{}{}
}
if newList == nil {
newList = []interface{}{}
}

minDrives := min(len(oldList), len(newList))

for i := range minDrives {
oldSize := dvDiskSize
newSize := dvDiskSize
if i < len(oldList) && oldList[i] != nil {
if om, ok := oldList[i].(map[string]interface{}); ok {
if v, ok := om[mkDiskSize].(int); ok {
oldSize = v
}
}
}

if i < len(newList) && newList[i] != nil {
if nm, ok := newList[i].(map[string]interface{}); ok {
if v, ok := nm[mkDiskSize].(int); ok {
newSize = v
}
}
}

if oldSize > newSize {
_ = d.ForceNew(fmt.Sprintf("%s.%d.%s", mkDisk, i, mkDiskSize))

Check failure on line 1141 in proxmoxtf/resource/container/container.go

View workflow job for this annotation

GitHub Actions / golangci-lint

Error return value of `d.ForceNew` is not checked (errcheck)
return true
}
}

return false
},
),
),
Importer: &schema.ResourceImporter{
StateContext: func(_ context.Context, d *schema.ResourceData, _ any) ([]*schema.ResourceData, error) {
Expand Down Expand Up @@ -3019,7 +3112,23 @@
mountOptions := diskBlock[mkDiskMountOptions].([]any)
quota := types.CustomBool(diskBlock[mkDiskQuota].(bool))
replicate := types.CustomBool(diskBlock[mkDiskReplicate].(bool))

oldSize := containerConfig.RootFS.Size
size := types.DiskSizeFromGigabytes(int64(diskBlock[mkDiskSize].(int)))
// we should never reach this point. The `plan` should recreate the container, not update it, if the old size is larger.
if *oldSize > *size {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There is a potential nil pointer dereference here. oldSize is of type *types.DiskSize and can be nil if the size property is not returned by the Proxmox API for the container's rootfs. Dereferencing it with *oldSize on this line will cause a panic. You should add a nil check for oldSize before dereferencing it.

Suggested change
if *oldSize > *size {
if oldSize != nil && *oldSize > *size {

return diag.Errorf("New disk size (%s) has to be greater the current disk (%s)!", oldSize, size)
}

if !ptr.Eq(oldSize, size) {
err = containerAPI.ResizeContainerDisk(ctx, &containers.ResizeRequestBody{
Disk: "rootfs",
Size: size.String(),
})
if err != nil {
return diag.FromErr(err)
}
}

rootFS.ACL = &acl
rootFS.Quota = &quota
Expand All @@ -3029,15 +3138,26 @@
mountOptionsStrings := make([]string, 0, len(mountOptions))

for _, option := range mountOptions {
mountOptionsStrings = append(mountOptionsStrings, option.(string))
optionString := option.(string)
mountOptionsStrings = append(mountOptionsStrings, optionString)
}

// Always set, including empty, to allow clearing mount options
rootFS.MountOptions = &mountOptionsStrings

updateBody.RootFS = rootFS
// To compare contents regardless of order, we can sort them.
// The schema already uses a suppress func for order, so we should be consistent.
sort.Strings(mountOptionsStrings)
currentMountOptions := containerConfig.RootFS.MountOptions
currentMountOptionsSorted := []string{}
if currentMountOptions != nil {
currentMountOptionsSorted = append(currentMountOptionsSorted, *currentMountOptions...)
}
sort.Strings(currentMountOptionsSorted)
if !slices.Equal(mountOptionsStrings, currentMountOptionsSorted) {
rebootRequired = true
}

rebootRequired = true
updateBody.RootFS = rootFS
}

if d.HasChange(mkFeatures) {
Expand Down