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