mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-06-30 02:31:10 +00:00
chore(vm2): experimental support for clone
and inherited attributes (#1235)
Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
This commit is contained in:
parent
ee939a38a3
commit
7209fe0321
@ -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.<br><br>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).
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
@ -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))
|
||||
|
||||
<a id="nestedatt--clone"></a>
|
||||
### 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).
|
||||
|
||||
|
||||
<a id="nestedatt--timeouts"></a>
|
||||
### Nested Schema for `timeouts`
|
||||
|
||||
|
@ -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,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)*`),
|
||||
|
@ -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)}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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(
|
||||
"<br><br>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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user