From 467d3dd123d61eab80b017bca3ba72229fc9742d Mon Sep 17 00:00:00 2001 From: Alexander Petermann Date: Wed, 5 Nov 2025 13:40:21 +0100 Subject: [PATCH 1/8] fix: allow container creation with correct mountoptions Signed-off-by: Alexander Petermann --- proxmoxtf/resource/container/container.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxmoxtf/resource/container/container.go b/proxmoxtf/resource/container/container.go index acb0c7908..88f03aff5 100644 --- a/proxmoxtf/resource/container/container.go +++ b/proxmoxtf/resource/container/container.go @@ -1765,7 +1765,7 @@ func containerCreateCustom(ctx context.Context, d *schema.ResourceData, m interf } diskSize := diskBlock[mkDiskSize].(int) - if diskDatastoreID != "" && (diskSize != dvDiskSize || len(mountPoints) > 0) { + if diskDatastoreID != "" && (diskSize != dvDiskSize || len(mountPoints) > 0 || len(diskMountOptions) > 0) { // 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 rootFS = &containers.CustomRootFS{ From d5297c0c3c14c2ad4c32114f86680cfd708de7f1 Mon Sep 17 00:00:00 2001 From: Alexander Petermann Date: Wed, 5 Nov 2025 13:40:55 +0100 Subject: [PATCH 2/8] fix: use current rootFS Volume in containerUpdate Signed-off-by: Alexander Petermann --- proxmoxtf/resource/container/container.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/proxmoxtf/resource/container/container.go b/proxmoxtf/resource/container/container.go index 88f03aff5..e645491e7 100644 --- a/proxmoxtf/resource/container/container.go +++ b/proxmoxtf/resource/container/container.go @@ -2985,11 +2985,15 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) } rootFS := &containers.CustomRootFS{} - // Disk ID for the rootfs is always 0 - diskID := 0 - vmID := d.Get(mkVMID).(int) - rootFS.Volume = diskBlock[mkDiskDatastoreID].(string) - rootFS.Volume = getContainerDiskVolume(rootFS.Volume, vmID, diskID) + containerConfig, e := containerAPI.GetContainer(ctx) + if e != nil { + if errors.Is(e, api.ErrResourceDoesNotExist) { + d.SetId("") + return nil + } + return diag.FromErr(e) + } + rootFS.Volume = containerConfig.RootFS.Volume acl := types.CustomBool(diskBlock[mkDiskACL].(bool)) mountOptions := diskBlock[mkDiskMountOptions].([]interface{}) @@ -3534,10 +3538,6 @@ func parseImportIDWithNodeName(id string) (string, string, error) { return nodeName, id, nil } -func getContainerDiskVolume(rawVolume string, vmID int, diskID int) string { - return fmt.Sprintf("%s:vm-%d-disk-%d", rawVolume, vmID, diskID) -} - func skipDnsDiffIfEmpty(k, oldValue, newValue string, d *schema.ResourceData) bool { dnsDataKey := mkInitialization + ".0." + mkInitializationDNS if k == dnsDataKey+".#" { From feb605f197e431750ecd2555f9f15adeea270341 Mon Sep 17 00:00:00 2001 From: Alexander Petermann Date: Wed, 5 Nov 2025 14:20:51 +0100 Subject: [PATCH 3/8] feature(lxc): allow to resize the rootDisk (only bigger) Signed-off-by: Alexander Petermann --- proxmox/nodes/containers/containers.go | 10 +++++ proxmox/nodes/containers/containers_types.go | 5 +++ proxmoxtf/resource/container/container.go | 42 +++++++++++++++++--- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/proxmox/nodes/containers/containers.go b/proxmox/nodes/containers/containers.go index d274be6d0..c8cab13ab 100644 --- a/proxmox/nodes/containers/containers.go +++ b/proxmox/nodes/containers/containers.go @@ -370,3 +370,13 @@ func (c *Client) WaitForContainerConfigUnlock(ctx context.Context, ignoreErrorRe return nil } + +// Resize 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 resize disk: %w", err) + } + + return nil +} diff --git a/proxmox/nodes/containers/containers_types.go b/proxmox/nodes/containers/containers_types.go index 036097b7d..838018345 100644 --- a/proxmox/nodes/containers/containers_types.go +++ b/proxmox/nodes/containers/containers_types.go @@ -275,6 +275,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 diff --git a/proxmoxtf/resource/container/container.go b/proxmoxtf/resource/container/container.go index e645491e7..f162c7875 100644 --- a/proxmoxtf/resource/container/container.go +++ b/proxmoxtf/resource/container/container.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "regexp" + "slices" "sort" "strconv" "strings" @@ -333,7 +334,7 @@ func Container() *schema.Resource { Type: schema.TypeList, Description: "The disks", Optional: true, - ForceNew: true, + ForceNew: false, DefaultFunc: func() (interface{}, error) { return []interface{}{ map[string]interface{}{ @@ -374,7 +375,7 @@ func Container() *schema.Resource { Type: schema.TypeInt, Description: "The rootfs size in gigabytes", Optional: true, - ForceNew: true, + ForceNew: false, Default: dvDiskSize, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), }, @@ -2999,25 +3000,56 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) mountOptions := diskBlock[mkDiskMountOptions].([]interface{}) quota := types.CustomBool(diskBlock[mkDiskQuota].(bool)) replicate := types.CustomBool(diskBlock[mkDiskReplicate].(bool)) + + oldSize := containerConfig.RootFS.Size size := types.DiskSizeFromGigabytes(int64(diskBlock[mkDiskSize].(int))) + if *oldSize > *size { + d.SetId("") + return diag.Errorf("New disk size (%s) has to be greater the current disk (%s)!", oldSize, size) + } + + if oldSize != size { + containerAPI.ResizeContainerDisk(ctx, &containers.ResizeRequestBody{ + Disk: ptr.Ptr("rootfs"), + Size: ptr.Ptr(size.String()), + }) + } rootFS.ACL = &acl + if acl != *containerConfig.RootFS.ACL { + rebootRequired = true + } rootFS.Quota = "a + if (quota && (containerConfig.RootFS.Quota == nil || *containerConfig.RootFS.Quota != quota)) || (!quota && (containerConfig.RootFS.Quota != nil && containerConfig.RootFS.Quota != "a)) { + rebootRequired = true + } rootFS.Replicate = &replicate + if replicate != *containerConfig.RootFS.Replicate { + rebootRequired = true + } rootFS.Size = size mountOptionsStrings := make([]string, 0, len(mountOptions)) + currenMountOptions := containerConfig.RootFS.MountOptions for _, option := range mountOptions { - mountOptionsStrings = append(mountOptionsStrings, option.(string)) + optionString := option.(string) + mountOptionsStrings = append(mountOptionsStrings, optionString) + if !slices.Contains(*currenMountOptions, optionString) { + rebootRequired = true + } + } + // handle two cases: + // 1. mountOptions is empty, but the current container has mountOptions + // 2. the current container has mountOptions set + if len(mountOptions) != len(*currenMountOptions) { + rebootRequired = true } // Always set, including empty, to allow clearing mount options rootFS.MountOptions = &mountOptionsStrings updateBody.RootFS = rootFS - - rebootRequired = true } if d.HasChange(mkFeatures) { From 534edff189fa763c9804f4422cf690c88dddb172 Mon Sep 17 00:00:00 2001 From: Alexander Petermann Date: Wed, 5 Nov 2025 23:51:49 +0100 Subject: [PATCH 4/8] chore: cleanup code at gemini suggestions Signed-off-by: Alexander Petermann --- proxmox/nodes/containers/containers_types.go | 4 +- proxmoxtf/resource/container/container.go | 43 +++++++++----------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/proxmox/nodes/containers/containers_types.go b/proxmox/nodes/containers/containers_types.go index 838018345..b24b13b45 100644 --- a/proxmox/nodes/containers/containers_types.go +++ b/proxmox/nodes/containers/containers_types.go @@ -276,8 +276,8 @@ type ShutdownRequestBody struct { } type ResizeRequestBody struct { - Disk *string `json:"disk" url:"disk"` - Size *string `json:"size" url:"size"` + Disk string `json:"disk" url:"disk"` + Size string `json:"size" url:"size"` } // UpdateRequestBody contains the data for an user update request. diff --git a/proxmoxtf/resource/container/container.go b/proxmoxtf/resource/container/container.go index f162c7875..44797dffe 100644 --- a/proxmoxtf/resource/container/container.go +++ b/proxmoxtf/resource/container/container.go @@ -2994,6 +2994,10 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) } return diag.FromErr(e) } + + if containerConfig.RootFS == nil { + return diag.Errorf("RootFS information of container malformed.") + } rootFS.Volume = containerConfig.RootFS.Volume acl := types.CustomBool(diskBlock[mkDiskACL].(bool)) @@ -3004,51 +3008,44 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) oldSize := containerConfig.RootFS.Size size := types.DiskSizeFromGigabytes(int64(diskBlock[mkDiskSize].(int))) if *oldSize > *size { + // TODO: we should never reach this point. The `plan` should recreate the container, not update it. d.SetId("") return diag.Errorf("New disk size (%s) has to be greater the current disk (%s)!", oldSize, size) } if oldSize != size { - containerAPI.ResizeContainerDisk(ctx, &containers.ResizeRequestBody{ - Disk: ptr.Ptr("rootfs"), - Size: ptr.Ptr(size.String()), + e = containerAPI.ResizeContainerDisk(ctx, &containers.ResizeRequestBody{ + Disk: "rootfs", + Size: size.String(), }) + if e != nil { + return diag.FromErr(err) + } } rootFS.ACL = &acl - if acl != *containerConfig.RootFS.ACL { - rebootRequired = true - } rootFS.Quota = "a - if (quota && (containerConfig.RootFS.Quota == nil || *containerConfig.RootFS.Quota != quota)) || (!quota && (containerConfig.RootFS.Quota != nil && containerConfig.RootFS.Quota != "a)) { - rebootRequired = true - } rootFS.Replicate = &replicate - if replicate != *containerConfig.RootFS.Replicate { - rebootRequired = true - } rootFS.Size = size mountOptionsStrings := make([]string, 0, len(mountOptions)) - currenMountOptions := containerConfig.RootFS.MountOptions for _, option := range mountOptions { optionString := option.(string) mountOptionsStrings = append(mountOptionsStrings, optionString) - if !slices.Contains(*currenMountOptions, optionString) { - rebootRequired = true - } - } - // handle two cases: - // 1. mountOptions is empty, but the current container has mountOptions - // 2. the current container has mountOptions set - if len(mountOptions) != len(*currenMountOptions) { - rebootRequired = true } - // Always set, including empty, to allow clearing mount options rootFS.MountOptions = &mountOptionsStrings + // 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 + sort.Strings(*currentMountOptions) + if !slices.Equal(mountOptionsStrings, *currentMountOptions) { + rebootRequired = true + } + updateBody.RootFS = rootFS } From 2c2f804d28b261c918f005a016c43c77065e4089 Mon Sep 17 00:00:00 2001 From: Alexander Petermann Date: Thu, 6 Nov 2025 01:52:59 +0100 Subject: [PATCH 5/8] fix: get recreation on smaller new disk to work for rootfs Signed-off-by: Alexander Petermann --- proxmoxtf/resource/container/container.go | 63 ++++++++++++++++++++--- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/proxmoxtf/resource/container/container.go b/proxmoxtf/resource/container/container.go index 44797dffe..47c9fede9 100644 --- a/proxmoxtf/resource/container/container.go +++ b/proxmoxtf/resource/container/container.go @@ -334,7 +334,6 @@ func Container() *schema.Resource { Type: schema.TypeList, Description: "The disks", Optional: true, - ForceNew: false, DefaultFunc: func() (interface{}, error) { return []interface{}{ map[string]interface{}{ @@ -375,7 +374,6 @@ func Container() *schema.Resource { Type: schema.TypeInt, Description: "The rootfs size in gigabytes", Optional: true, - ForceNew: false, Default: dvDiskSize, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), }, @@ -1026,6 +1024,55 @@ func Container() *schema.Resource { return strconv.Itoa(newValue.(int)) != d.Id() }, ), + 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{}{} + } + + // fmt.Printf("ALEX: ALL DISK: old: %v ---- new: %v\n", old, new) + + 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 + } + } + } + + // fmt.Printf("ALEX: check DISK %v: %v vs %v\n", i, oldSize, newSize) + if oldSize > newSize { + + // fmt.Print("ALEX: check DISK: new is smaller\n") + _ = d.ForceNew(fmt.Sprintf("%s.%d.%s", mkDisk, i, mkDiskSize)) + return true // <-- this is not working + } + } + + return false + }, + ), ), Importer: &schema.ResourceImporter{ StateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) { @@ -3014,11 +3061,11 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) } if oldSize != size { - e = containerAPI.ResizeContainerDisk(ctx, &containers.ResizeRequestBody{ + err = containerAPI.ResizeContainerDisk(ctx, &containers.ResizeRequestBody{ Disk: "rootfs", Size: size.String(), }) - if e != nil { + if err != nil { return diag.FromErr(err) } } @@ -3041,8 +3088,12 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) // The schema already uses a suppress func for order, so we should be consistent. sort.Strings(mountOptionsStrings) currentMountOptions := containerConfig.RootFS.MountOptions - sort.Strings(*currentMountOptions) - if !slices.Equal(mountOptionsStrings, *currentMountOptions) { + currentMountOptionsSorted := []string{} + if currentMountOptions != nil { + currentMountOptionsSorted = append(currentMountOptionsSorted, *currentMountOptions...) + } + sort.Strings(currentMountOptionsSorted) + if !slices.Equal(mountOptionsStrings, currentMountOptionsSorted) { rebootRequired = true } From cdbf94fc035316882f579b11b4521878d122f2ee Mon Sep 17 00:00:00 2001 From: Alexander Petermann Date: Thu, 6 Nov 2025 01:58:19 +0100 Subject: [PATCH 6/8] Update proxmox/nodes/containers/containers.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Alexander Petermann --- proxmox/nodes/containers/containers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proxmox/nodes/containers/containers.go b/proxmox/nodes/containers/containers.go index c8cab13ab..5f1d9c255 100644 --- a/proxmox/nodes/containers/containers.go +++ b/proxmox/nodes/containers/containers.go @@ -371,11 +371,11 @@ func (c *Client) WaitForContainerConfigUnlock(ctx context.Context, ignoreErrorRe return nil } -// Resize Disk +// 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 resize disk: %w", err) + return fmt.Errorf("error resizing container disk: %w", err) } return nil From 79ddf1aac9c150c1320e2a0c39eb5c7f78efddc5 Mon Sep 17 00:00:00 2001 From: Alexander Petermann Date: Thu, 6 Nov 2025 12:59:08 +0100 Subject: [PATCH 7/8] fix: mountpoint size can't be changed right now, so they should trigger a recreate Signed-off-by: Alexander Petermann --- proxmoxtf/resource/container/container.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/proxmoxtf/resource/container/container.go b/proxmoxtf/resource/container/container.go index 47c9fede9..7af1349a6 100644 --- a/proxmoxtf/resource/container/container.go +++ b/proxmoxtf/resource/container/container.go @@ -662,6 +662,7 @@ func Container() *schema.Resource { Type: schema.TypeList, Description: "A mount point", Optional: true, + ForceNew: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ mkMountPointACL: { @@ -721,12 +722,14 @@ func Container() *schema.Resource { 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:" @@ -1038,8 +1041,6 @@ func Container() *schema.Resource { newList = []interface{}{} } - // fmt.Printf("ALEX: ALL DISK: old: %v ---- new: %v\n", old, new) - minDrives := min(len(oldList), len(newList)) for i := range minDrives { @@ -1061,12 +1062,9 @@ func Container() *schema.Resource { } } - // fmt.Printf("ALEX: check DISK %v: %v vs %v\n", i, oldSize, newSize) if oldSize > newSize { - - // fmt.Print("ALEX: check DISK: new is smaller\n") _ = d.ForceNew(fmt.Sprintf("%s.%d.%s", mkDisk, i, mkDiskSize)) - return true // <-- this is not working + return true } } @@ -3060,7 +3058,7 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) return diag.Errorf("New disk size (%s) has to be greater the current disk (%s)!", oldSize, size) } - if oldSize != size { + if !ptr.Eq(oldSize, size) { err = containerAPI.ResizeContainerDisk(ctx, &containers.ResizeRequestBody{ Disk: "rootfs", Size: size.String(), From dd7326a456b096d4dc8872e8b0918a83d8264d70 Mon Sep 17 00:00:00 2001 From: Alexander Petermann Date: Sat, 15 Nov 2025 00:05:47 +0100 Subject: [PATCH 8/8] fix: mountpoint changes should always trigger a recreate Signed-off-by: Alexander Petermann --- proxmoxtf/resource/container/container.go | 38 +++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/proxmoxtf/resource/container/container.go b/proxmoxtf/resource/container/container.go index 7af1349a6..27a00d2ac 100644 --- a/proxmoxtf/resource/container/container.go +++ b/proxmoxtf/resource/container/container.go @@ -10,6 +10,7 @@ import ( "context" "errors" "fmt" + "reflect" "regexp" "slices" "sort" @@ -1027,6 +1028,43 @@ func Container() *schema.Resource { 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 { + oldRaw, newRaw := d.GetChange(mkMountPoint) + // compare all oldRaw and newRaw entries + oldList, _ := oldRaw.([]interface{}) + newList, _ := newRaw.([]interface{}) + + if oldList == nil { + oldList = []interface{}{} + } + if newList == nil { + newList = []interface{}{} + } + + for i := 0; i < len(oldList); i++ { + if len(newList)-1 < i { + return true + } + // compare old and new list entries and call ForceNew on the correspondig string + // make a deep comparison + oldMap, _ := oldList[i].(map[string]interface{}) + 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)) + } + return true + } + } + return false + + }, + ), customdiff.ForceNewIf( mkDisk, func(_ context.Context, d *schema.ResourceDiff, _ interface{}) bool {