From 7209fe03215c7fda32dd74ab9647b9824e0b8d61 Mon Sep 17 00:00:00 2001
From: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
Date: Tue, 23 Apr 2024 22:00:11 -0400
Subject: [PATCH] chore(vm2): experimental support for `clone` and inherited
attributes (#1235)
Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
---
docs/resources/virtual_environment_vm2.md | 26 ++-
fwprovider/tests/resource_vm2_test.go | 106 ++++++++++--
fwprovider/types/tags/attribute.go | 3 +-
fwprovider/types/tags/value.go | 6 +-
fwprovider/vm/resource.go | 153 +++++++++++++-----
fwprovider/vm/resource_model.go | 17 +-
fwprovider/vm/resource_schema.go | 29 ++++
proxmox/nodes/vms/vms.go | 12 +-
proxmox/nodes/vms/vms_types.go | 2 +-
proxmox/types/common_types.go | 9 ++
proxmoxtf/resource/vm/vm.go | 2 +-
.../resources/virtual_environment_vm2.md.tmpl | 12 +-
12 files changed, 302 insertions(+), 75 deletions(-)
diff --git a/docs/resources/virtual_environment_vm2.md b/docs/resources/virtual_environment_vm2.md
index 1f2cb3b5..dcb44d41 100644
--- a/docs/resources/virtual_environment_vm2.md
+++ b/docs/resources/virtual_environment_vm2.md
@@ -9,9 +9,19 @@ description: |-
# Resource: proxmox_virtual_environment_vm2
-~> **DO NOT USE**
+!> **DO NOT USE**
This is an experimental implementation of a Proxmox VM resource using Plugin Framework.
It is a Proof of Concept, highly experimental and **will** change in future. It does not support all features of the Proxmox API for VMs and **MUST NOT** be used in production.
+-> Note: Many attributes are marked as **optional** _and_ **computed** in the schema,
+hence you may seem added to the plan with "(known after apply)" status, even if they are not set in the configuration.
+This is done to support the `clone` operation, when a VM is created from an existing one,
+and attributes of the original VM are copied to the new one.
+
+Computed attributes allow the provider to set those attributes without user input.
+The attributes are marked as optional to allow the user to set (or overwrite) them if needed.
+In order to remove the computed attribute from the plan, you can set it to an empty value (e.g. `""` for string, `[]` for collection).
+
+
@@ -23,12 +33,26 @@ This is an experimental implementation of a Proxmox VM resource using Plugin Fra
### Optional
+- `clone` (Attributes) The cloning configuration. (see [below for nested schema](#nestedatt--clone))
- `description` (String) The description of the VM.
- `id` (Number) The unique identifier of the VM in the Proxmox cluster.
- `name` (String) The name of the VM. Doesn't have to be unique.
- `tags` (Set of String) The tags assigned to the resource.
+- `template` (Boolean) Set to true to create a VM template.
- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts))
+
+### Nested Schema for `clone`
+
+Required:
+
+- `id` (Number) The ID of the VM to clone.
+
+Optional:
+
+- `retries` (Number) The number of retries to perform when cloning the VM (default: 3).
+
+
### Nested Schema for `timeouts`
diff --git a/fwprovider/tests/resource_vm2_test.go b/fwprovider/tests/resource_vm2_test.go
index cb820330..015c031f 100644
--- a/fwprovider/tests/resource_vm2_test.go
+++ b/fwprovider/tests/resource_vm2_test.go
@@ -121,7 +121,6 @@ func TestAccResourceVM2(t *testing.T) {
Config: te.renderConfig(`
resource "proxmox_virtual_environment_vm2" "test_vm" {
node_name = "{{.NodeName}}"
-
name = "test-tags"
tags = ["tag1"]
}`),
@@ -134,28 +133,31 @@ func TestAccResourceVM2(t *testing.T) {
Config: te.renderConfig(`
resource "proxmox_virtual_environment_vm2" "test_vm" {
node_name = "{{.NodeName}}"
-
name = "test-tags"
+ // no tags
}`),
- Check: testNoResourceAttributesSet("proxmox_virtual_environment_vm2.test_vm", []string{
- "tags",
- }),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("proxmox_virtual_environment_vm2.test_vm", "tags.#", "1"),
+ resource.TestCheckTypeSetElemAttr("proxmox_virtual_environment_vm2.test_vm", "tags.*", "tag1"),
+ ),
+ },
+ {
+ Config: te.renderConfig(`
+ resource "proxmox_virtual_environment_vm2" "test_vm" {
+ node_name = "{{.NodeName}}"
+ name = "test-tags"
+ tags = []
+ }`),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("proxmox_virtual_environment_vm2.test_vm", "tags.#", "0"),
+ ),
},
}},
- {"a VM can't have empty tags set", []resource.TestStep{{
- Config: te.renderConfig(`
- resource "proxmox_virtual_environment_vm2" "test_vm" {
- node_name = "{{.NodeName}}"
-
- tags = []
- }`),
- ExpectError: regexp.MustCompile(`tags set must contain at least 1 elements`),
- }}},
{"a VM can't have empty tags", []resource.TestStep{{
Config: te.renderConfig(`
resource "proxmox_virtual_environment_vm2" "test_vm" {
node_name = "{{.NodeName}}"
-
+
tags = ["", "tag1"]
}`),
ExpectError: regexp.MustCompile(`string length must be at least 1, got: 0`),
@@ -164,7 +166,7 @@ func TestAccResourceVM2(t *testing.T) {
Config: te.renderConfig(`
resource "proxmox_virtual_environment_vm2" "test_vm" {
node_name = "{{.NodeName}}"
-
+
tags = [" ", "tag1"]
}`),
ExpectError: regexp.MustCompile(`must be a non-empty and non-whitespace string`),
@@ -198,3 +200,75 @@ func TestAccResourceVM2(t *testing.T) {
})
}
}
+
+func TestAccResourceVM2Clone(t *testing.T) {
+ t.Parallel()
+
+ te := initTestEnvironment(t)
+ vmID := gofakeit.IntRange(90000, 100000)
+ te.addTemplateVars(map[string]any{
+ "VMID": vmID,
+ })
+
+ tests := []struct {
+ name string
+ steps []resource.TestStep
+ }{
+ {"create a clone from template", []resource.TestStep{{
+ Config: te.renderConfig(`
+ resource "proxmox_virtual_environment_vm2" "test_vm" {
+ node_name = "{{.NodeName}}"
+ name = "template"
+ description = "template description"
+ template = true
+ }
+ resource "proxmox_virtual_environment_vm2" "test_vm_clone" {
+ node_name = "{{.NodeName}}"
+ name = "clone"
+ clone = {
+ id = proxmox_virtual_environment_vm2.test_vm.id
+ }
+ }`),
+ Check: resource.ComposeTestCheckFunc(
+ testResourceAttributes("proxmox_virtual_environment_vm2.test_vm", map[string]string{
+ "template": "true",
+ }),
+ testResourceAttributes("proxmox_virtual_environment_vm2.test_vm_clone", map[string]string{
+ // name is overwritten
+ "name": "clone",
+ }),
+ testNoResourceAttributesSet("proxmox_virtual_environment_vm2.test_vm_clone", []string{
+ // description is not copied
+ "description",
+ }),
+ ),
+ }}},
+ {"tags are copied to the clone", []resource.TestStep{{
+ Config: te.renderConfig(`
+ resource "proxmox_virtual_environment_vm2" "test_vm" {
+ node_name = "{{.NodeName}}"
+ template = true
+ tags = ["tag1", "tag2"]
+ }
+ resource "proxmox_virtual_environment_vm2" "test_vm_clone" {
+ node_name = "{{.NodeName}}"
+ clone = {
+ id = proxmox_virtual_environment_vm2.test_vm.id
+ }
+ }`),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckTypeSetElemAttr("proxmox_virtual_environment_vm2.test_vm_clone", "tags.*", "tag1"),
+ resource.TestCheckTypeSetElemAttr("proxmox_virtual_environment_vm2.test_vm_clone", "tags.*", "tag2"),
+ ),
+ }}},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ resource.ParallelTest(t, resource.TestCase{
+ ProtoV6ProviderFactories: te.accProviders,
+ Steps: tt.steps,
+ })
+ })
+ }
+}
diff --git a/fwprovider/types/tags/attribute.go b/fwprovider/types/tags/attribute.go
index aa4c3932..1fe06183 100644
--- a/fwprovider/types/tags/attribute.go
+++ b/fwprovider/types/tags/attribute.go
@@ -20,9 +20,10 @@ func ResourceAttribute() schema.SetAttribute {
},
Description: "The tags assigned to the resource.",
Optional: true,
+ Computed: true,
ElementType: types.StringType,
Validators: []validator.Set{
- setvalidator.SizeAtLeast(1),
+ // NOTE: we allow empty list to remove all previously set tags
setvalidator.ValueStringsAre(
stringvalidator.RegexMatches(
regexp.MustCompile(`(.|\s)*\S(.|\s)*`),
diff --git a/fwprovider/types/tags/value.go b/fwprovider/types/tags/value.go
index a2efe482..ffa5b1a6 100644
--- a/fwprovider/types/tags/value.go
+++ b/fwprovider/types/tags/value.go
@@ -41,8 +41,8 @@ func (v Value) Equal(o attr.Value) bool {
}
// ValueStringPointer returns a pointer to the string representation of tags set value.
-func (v Value) ValueStringPointer(ctx context.Context, diags diag.Diagnostics) *string {
- if v.IsNull() {
+func (v Value) ValueStringPointer(ctx context.Context, diags *diag.Diagnostics) *string {
+ if v.IsNull() || v.IsUnknown() || len(v.Elements()) == 0 {
return nil
}
@@ -71,7 +71,7 @@ func (v Value) ValueStringPointer(ctx context.Context, diags diag.Diagnostics) *
}
// SetValue converts a string of tags to a tags set value.
-func SetValue(tagsStr *string, diags diag.Diagnostics) Value {
+func SetValue(tagsStr *string, diags *diag.Diagnostics) Value {
if tagsStr == nil {
return Value{types.SetNull(types.StringType)}
}
diff --git a/fwprovider/vm/resource.go b/fwprovider/vm/resource.go
index e94085cb..8ffa3264 100644
--- a/fwprovider/vm/resource.go
+++ b/fwprovider/vm/resource.go
@@ -94,33 +94,24 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
- vmID := int(plan.ID.ValueInt64())
- if vmID == 0 {
+ if plan.ID.ValueInt64() == 0 {
id, err := r.client.Cluster().GetVMID(ctx)
if err != nil {
resp.Diagnostics.AddError("Failed to get VM ID", err.Error())
return
}
- vmID = *id
- }
-
- vmAPI := r.client.Node(plan.NodeName.ValueString()).VM(vmID)
-
- createBody := &vms.CreateRequestBody{
- Description: plan.Description.ValueStringPointer(),
- Name: plan.Name.ValueStringPointer(),
- Tags: plan.Tags.ValueStringPointer(ctx, resp.Diagnostics),
- VMID: &vmID,
+ plan.ID = types.Int64Value(int64(*id))
}
if resp.Diagnostics.HasError() {
return
}
- err := vmAPI.CreateVM(ctx, createBody)
- if err != nil {
- resp.Diagnostics.AddError("Failed to create VM", err.Error())
+ if plan.Clone != nil {
+ r.clone(ctx, plan, &resp.Diagnostics)
+ } else {
+ r.create(ctx, plan, &resp.Diagnostics)
}
if resp.Diagnostics.HasError() {
@@ -128,7 +119,7 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res
}
// read back the VM from the PVE API to populate computed fields
- exists := r.read(ctx, vmAPI, &plan, resp.Diagnostics)
+ exists := r.read(ctx, &plan, &resp.Diagnostics)
if !exists {
resp.Diagnostics.AddError("VM does not exist after creation", "")
}
@@ -141,6 +132,70 @@ func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, res
resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
}
+func (r *vmResource) create(ctx context.Context, plan vmModel, diags *diag.Diagnostics) {
+ createBody := &vms.CreateRequestBody{
+ Description: plan.Description.ValueStringPointer(),
+ Name: plan.Name.ValueStringPointer(),
+ Tags: plan.Tags.ValueStringPointer(ctx, diags),
+ Template: proxmoxtypes.CustomBoolPtr(plan.Template.ValueBoolPointer()),
+ VMID: int(plan.ID.ValueInt64()),
+ }
+
+ if diags.HasError() {
+ return
+ }
+
+ // .VM(0) is used to create a new VM, the VM ID is not used in the API URL
+ vmAPI := r.client.Node(plan.NodeName.ValueString()).VM(0)
+
+ err := vmAPI.CreateVM(ctx, createBody)
+ if err != nil {
+ diags.AddError("Failed to create VM", err.Error())
+ }
+}
+
+func (r *vmResource) clone(ctx context.Context, plan vmModel, diags *diag.Diagnostics) {
+ if plan.Clone == nil {
+ diags.AddError("Clone configuration is missing", "")
+ return
+ }
+
+ sourceID := int(plan.Clone.ID.ValueInt64())
+ vmAPI := r.client.Node(plan.NodeName.ValueString()).VM(sourceID)
+
+ // name and description for the clone are optional, but they are not copied from the source VM.
+ cloneBody := &vms.CloneRequestBody{
+ Description: plan.Description.ValueStringPointer(),
+ Name: plan.Name.ValueStringPointer(),
+ VMIDNew: int(plan.ID.ValueInt64()),
+ }
+
+ err := vmAPI.CloneVM(ctx, int(plan.Clone.Retries.ValueInt64()), cloneBody)
+ if err != nil {
+ diags.AddError("Failed to clone VM", err.Error())
+ }
+
+ if diags.HasError() {
+ return
+ }
+
+ // now load the clone's configuration into a temporary model and update what is needed comparing to the plan
+ clone := vmModel{
+ ID: plan.ID,
+ Name: plan.Name,
+ Description: plan.Description,
+ NodeName: plan.NodeName,
+ }
+
+ r.read(ctx, &clone, diags)
+
+ if diags.HasError() {
+ return
+ }
+
+ r.update(ctx, plan, clone, diags)
+}
+
func (r *vmResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state vmModel
@@ -156,9 +211,7 @@ func (r *vmResource) Read(ctx context.Context, req resource.ReadRequest, resp *r
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
- vmAPI := r.client.Node(state.NodeName.ValueString()).VM(int(state.ID.ValueInt64()))
-
- exists := r.read(ctx, vmAPI, &state, resp.Diagnostics)
+ exists := r.read(ctx, &state, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
@@ -193,10 +246,27 @@ func (r *vmResource) Update(ctx context.Context, req resource.UpdateRequest, res
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
+ r.update(ctx, plan, state, &resp.Diagnostics)
+
+ // read back the VM from the PVE API to populate computed fields
+ exists := r.read(ctx, &plan, &resp.Diagnostics)
+ if !exists {
+ resp.Diagnostics.AddError("VM does not exist after update", "")
+ }
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // set state to the updated plan data
+ resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
+}
+
+func (r *vmResource) update(ctx context.Context, plan, state vmModel, diags *diag.Diagnostics) {
vmAPI := r.client.Node(plan.NodeName.ValueString()).VM(int(plan.ID.ValueInt64()))
updateBody := &vms.UpdateRequestBody{
- VMID: proxmoxtypes.Int64PtrToIntPtr(plan.ID.ValueInt64Pointer()),
+ VMID: int(plan.ID.ValueInt64()),
}
var errs []error
@@ -221,35 +291,25 @@ func (r *vmResource) Update(ctx context.Context, req resource.UpdateRequest, res
}
}
- if !plan.Tags.Equal(state.Tags) {
+ // For optional computed fields only:
+ // The first condition is for the clone case, where the tags (captured in `state.Tags`)
+ // have already been copied from the source VM to the clone during the cloning process.
+ // Then, if the clone config does not have tags, we keep the cloned ones in the VM.
+ // Otherwise, if the clone config has empty tags we remove them from the VM.
+ // And finally, if the clone config has tags we update them in th VM
+ if !plan.Tags.Equal(state.Tags) && !plan.Tags.IsUnknown() {
if plan.Tags.IsNull() || len(plan.Tags.Elements()) == 0 {
del("Tags")
} else {
- updateBody.Tags = plan.Tags.ValueStringPointer(ctx, resp.Diagnostics)
+ updateBody.Tags = plan.Tags.ValueStringPointer(ctx, diags)
}
}
err := vmAPI.UpdateVM(ctx, updateBody)
if err != nil {
- resp.Diagnostics.AddError("Failed to update VM", err.Error())
- }
-
- if resp.Diagnostics.HasError() {
+ diags.AddError("Failed to update VM", err.Error())
return
}
-
- // read back the VM from the PVE API to populate computed fields
- exists := r.read(ctx, vmAPI, &plan, resp.Diagnostics)
- if !exists {
- resp.Diagnostics.AddError("VM does not exist after update", "")
- }
-
- if resp.Diagnostics.HasError() {
- return
- }
-
- // set state to the updated plan data
- resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
}
func (r *vmResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
@@ -326,8 +386,6 @@ func (r *vmResource) ImportState(
return
}
- vmAPI := r.client.Node(nodeName).VM(id)
-
var ts timeouts.Value
resp.Diagnostics.Append(resp.State.GetAttribute(ctx, path.Root("timeouts"), &ts)...)
@@ -342,7 +400,7 @@ func (r *vmResource) ImportState(
Timeouts: ts,
}
- exists := r.read(ctx, vmAPI, &state, resp.Diagnostics)
+ exists := r.read(ctx, &state, &resp.Diagnostics)
if !exists {
resp.Diagnostics.AddError(fmt.Sprintf("VM %d does not exist on node %s", id, nodeName), "")
}
@@ -357,7 +415,9 @@ func (r *vmResource) ImportState(
// read retrieves the current state of the resource from the API and updates the state.
// Returns false if the resource does not exist, so the caller can remove it from the state if necessary.
-func (r *vmResource) read(ctx context.Context, vmAPI *vms.Client, model *vmModel, diags diag.Diagnostics) bool {
+func (r *vmResource) read(ctx context.Context, model *vmModel, diags *diag.Diagnostics) bool {
+ vmAPI := r.client.Node(model.NodeName.ValueString()).VM(int(model.ID.ValueInt64()))
+
// Retrieve the entire configuration in order to compare it to the state.
config, err := vmAPI.GetVM(ctx)
if err != nil {
@@ -388,7 +448,12 @@ func (r *vmResource) read(ctx context.Context, vmAPI *vms.Client, model *vmModel
// Optional fields can be removed from the model, use StringPointerValue to handle removal on nil
model.Description = types.StringPointerValue(config.Description)
model.Name = types.StringPointerValue(config.Name)
- model.Tags = tags.SetValue(config.Tags, diags)
+
+ if model.Tags.IsNull() || model.Tags.IsUnknown() { // only for computed
+ model.Tags = tags.SetValue(config.Tags, diags)
+ }
+
+ model.Template = types.BoolPointerValue(config.Template.PointerBool())
return true
}
diff --git a/fwprovider/vm/resource_model.go b/fwprovider/vm/resource_model.go
index 64f51dc6..6b9d8899 100644
--- a/fwprovider/vm/resource_model.go
+++ b/fwprovider/vm/resource_model.go
@@ -8,10 +8,15 @@ import (
)
type vmModel struct {
- Description types.String `tfsdk:"description"`
- ID types.Int64 `tfsdk:"id"`
- Name types.String `tfsdk:"name"`
- NodeName types.String `tfsdk:"node_name"`
- Tags tags.Value `tfsdk:"tags"`
- Timeouts timeouts.Value `tfsdk:"timeouts"`
+ Description types.String `tfsdk:"description"`
+ Clone *struct {
+ ID types.Int64 `tfsdk:"id"`
+ Retries types.Int64 `tfsdk:"retries"`
+ } `tfsdk:"clone"`
+ ID types.Int64 `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ NodeName types.String `tfsdk:"node_name"`
+ Tags tags.Value `tfsdk:"tags"`
+ Template types.Bool `tfsdk:"template"`
+ Timeouts timeouts.Value `tfsdk:"timeouts"`
}
diff --git a/fwprovider/vm/resource_schema.go b/fwprovider/vm/resource_schema.go
index d11beb90..cbbf1d4f 100644
--- a/fwprovider/vm/resource_schema.go
+++ b/fwprovider/vm/resource_schema.go
@@ -8,7 +8,10 @@ import (
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
@@ -27,6 +30,25 @@ func (r *vmResource) Schema(
"
It is a Proof of Concept, highly experimental and **will** change in future. " +
"It does not support all features of the Proxmox API for VMs and **MUST NOT** be used in production.",
Attributes: map[string]schema.Attribute{
+ "clone": schema.SingleNestedAttribute{
+ Description: "The cloning configuration.",
+ Optional: true,
+ PlanModifiers: []planmodifier.Object{
+ objectplanmodifier.RequiresReplace(),
+ },
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int64Attribute{
+ Description: "The ID of the VM to clone.",
+ Required: true,
+ },
+ "retries": schema.Int64Attribute{
+ Description: "The number of retries to perform when cloning the VM (default: 3).",
+ Optional: true,
+ Computed: true,
+ Default: int64default.StaticInt64(3),
+ },
+ },
+ },
"description": schema.StringAttribute{
Description: "The description of the VM.",
Optional: true,
@@ -56,6 +78,13 @@ func (r *vmResource) Schema(
Required: true,
},
"tags": tags.ResourceAttribute(),
+ "template": schema.BoolAttribute{
+ Description: "Set to true to create a VM template.",
+ Optional: true,
+ PlanModifiers: []planmodifier.Bool{
+ boolplanmodifier.RequiresReplace(),
+ },
+ },
"timeouts": timeouts.Attributes(ctx, timeouts.Opts{
Create: true,
Read: true,
diff --git a/proxmox/nodes/vms/vms.go b/proxmox/nodes/vms/vms.go
index 4368164e..8f6e0b9e 100644
--- a/proxmox/nodes/vms/vms.go
+++ b/proxmox/nodes/vms/vms.go
@@ -72,7 +72,17 @@ func (c *Client) CreateVM(ctx context.Context, d *CreateRequestBody) error {
func (c *Client) CreateVMAsync(ctx context.Context, d *CreateRequestBody) (*string, error) {
resBody := &CreateResponseBody{}
- err := c.DoRequest(ctx, http.MethodPost, c.basePath(), d, resBody)
+ err := retry.Do(
+ func() error {
+ return c.DoRequest(ctx, http.MethodPost, c.basePath(), d, resBody)
+ },
+ retry.Context(ctx),
+ retry.RetryIf(func(err error) bool {
+ return strings.Contains(err.Error(), "Reason: got no worker upid")
+ }),
+ retry.LastErrorOnly(true),
+ retry.Attempts(3),
+ )
if err != nil {
return nil, fmt.Errorf("error creating VM: %w", err)
}
diff --git a/proxmox/nodes/vms/vms_types.go b/proxmox/nodes/vms/vms_types.go
index 330b4af0..ca45aed2 100644
--- a/proxmox/nodes/vms/vms_types.go
+++ b/proxmox/nodes/vms/vms_types.go
@@ -290,7 +290,7 @@ type CreateRequestBody struct {
VirtualCPUCount *int `json:"vcpus,omitempty" url:"vcpus,omitempty"`
VirtualIODevices CustomStorageDevices `json:"virtio,omitempty" url:"virtio,omitempty"`
VMGenerationID *string `json:"vmgenid,omitempty" url:"vmgenid,omitempty"`
- VMID *int `json:"vmid,omitempty" url:"vmid,omitempty"`
+ VMID int `json:"vmid,omitempty" url:"vmid,omitempty"`
VMStateDatastoreID *string `json:"vmstatestorage,omitempty" url:"vmstatestorage,omitempty"`
WatchdogDevice *CustomWatchdogDevice `json:"watchdog,omitempty" url:"watchdog,omitempty"`
}
diff --git a/proxmox/types/common_types.go b/proxmox/types/common_types.go
index 5b3d775d..79ccc605 100644
--- a/proxmox/types/common_types.go
+++ b/proxmox/types/common_types.go
@@ -38,6 +38,15 @@ type CustomPrivileges []string
// CustomTimestamp allows a JSON boolean value to also be a unix timestamp.
type CustomTimestamp time.Time
+// CustomBoolPtr creates a CustomBool pointer from a boolean pointer.
+func CustomBoolPtr(v *bool) *CustomBool {
+ if v == nil {
+ return nil
+ }
+
+ return BoolPtr(*v)
+}
+
// MarshalJSON converts a boolean to a JSON value.
func (r CustomBool) MarshalJSON() ([]byte, error) {
buffer := new(bytes.Buffer)
diff --git a/proxmoxtf/resource/vm/vm.go b/proxmoxtf/resource/vm/vm.go
index 1d0c58ff..7657dc87 100644
--- a/proxmoxtf/resource/vm/vm.go
+++ b/proxmoxtf/resource/vm/vm.go
@@ -2669,7 +2669,7 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{})
Template: &template,
USBDevices: usbDeviceObjects,
VGADevice: vgaDevice,
- VMID: &vmID,
+ VMID: vmID,
}
if sataDeviceObjects != nil {
diff --git a/templates/resources/virtual_environment_vm2.md.tmpl b/templates/resources/virtual_environment_vm2.md.tmpl
index e0f19846..e719af7b 100644
--- a/templates/resources/virtual_environment_vm2.md.tmpl
+++ b/templates/resources/virtual_environment_vm2.md.tmpl
@@ -9,9 +9,19 @@ description: |-
# {{.Type}}: {{.Name}}
-~> **DO NOT USE**
+!> **DO NOT USE**
{{ .Description | trimspace }}
+-> Note: Many attributes are marked as **optional** _and_ **computed** in the schema,
+hence you may seem added to the plan with "(known after apply)" status, even if they are not set in the configuration.
+This is done to support the `clone` operation, when a VM is created from an existing one,
+and attributes of the original VM are copied to the new one.
+
+Computed attributes allow the provider to set those attributes without user input.
+The attributes are marked as optional to allow the user to set (or overwrite) them if needed.
+In order to remove the computed attribute from the plan, you can set it to an empty value (e.g. `""` for string, `[]` for collection).
+
+
{{ if .HasExample -}}
## Example Usage