diff --git a/Makefile b/Makefile index c16ec35f..ddbb59e4 100644 --- a/Makefile +++ b/Makefile @@ -94,7 +94,7 @@ test: .PHONY: testacc testacc: - @# explicitly add TF_ACC=1 to trigger the acceptance tests, `testacc.env` might be missing or incomplete + @# explicitly add TF_ACC=1 to trigger the acceptance tests, `testacc.env` might be missing or incomplete @TF_ACC=1 env $$(cat testacc.env | xargs) go test --timeout=30m -count=1 -v github.com/bpg/terraform-provider-proxmox/fwprovider/tests/... .PHONY: lint diff --git a/docs/resources/virtual_environment_acl.md b/docs/resources/virtual_environment_acl.md new file mode 100644 index 00000000..f3daca96 --- /dev/null +++ b/docs/resources/virtual_environment_acl.md @@ -0,0 +1,72 @@ +--- +layout: page +title: proxmox_virtual_environment_acl +parent: Resources +subcategory: Virtual Environment +description: |- + Manages ACLs on the Proxmox cluster. + ACLs are used to control access to resources in the Proxmox cluster. + Each ACL consists of a path, a user, group or token, a role, and a flag to allow propagation of permissions. +--- + +# Resource: proxmox_virtual_environment_acl + +Manages ACLs on the Proxmox cluster. + +ACLs are used to control access to resources in the Proxmox cluster. +Each ACL consists of a path, a user, group or token, a role, and a flag to allow propagation of permissions. + +## Example Usage + +```terraform +resource "proxmox_virtual_environment_user" "operations_automation" { + comment = "Managed by Terraform" + password = "a-strong-password" + user_id = "operations-automation@pve" +} + +resource "proxmox_virtual_environment_role" "operations_monitoring" { + role_id = "operations-monitoring" + + privileges = [ + "VM.Monitor", + ] +} + +resource "proxmox_virtual_environment_acl" "operations_automation_monitoring" { + user_id = proxmox_virtual_environment_user.operations_automation.user_id + role_id = proxmox_virtual_environment_role.operations_monitoring.role_id + + path = "/vms/1234" + propagate = true +} +``` + + +## Schema + +### Required + +- `path` (String) Access control path +- `role_id` (String) The role to apply + +### Optional + +- `group_id` (String) The group the ACL should apply to (mutually exclusive with `token_id` and `user_id`) +- `propagate` (Boolean) Allow to propagate (inherit) permissions. +- `token_id` (String) The token the ACL should apply to (mutually exclusive with `group_id` and `user_id`) +- `user_id` (String) The user the ACL should apply to (mutually exclusive with `group_id` and `token_id`) + +### Read-Only + +- `id` (String) The unique identifier of this resource. + +## Import + +Import is supported using the following syntax: + +```shell +#!/usr/bin/env sh +# ACL can be imported using its unique identifier, e.g.: {path}?entity_id={group|user@realm|user@realm!token}?role_id={role} +terraform import proxmox_virtual_environment_acl.operations_automation_monitoring /?entity_id=monitor@pve&role_id=operations-monitoring +``` diff --git a/examples/resources/proxmox_virtual_environment_acl/import.sh b/examples/resources/proxmox_virtual_environment_acl/import.sh new file mode 100644 index 00000000..b7faa097 --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_acl/import.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +# ACL can be imported using its unique identifier, e.g.: {path}?entity_id={group|user@realm|user@realm!token}?role_id={role} +terraform import proxmox_virtual_environment_acl.operations_automation_monitoring /?entity_id=monitor@pve&role_id=operations-monitoring diff --git a/examples/resources/proxmox_virtual_environment_acl/resource.tf b/examples/resources/proxmox_virtual_environment_acl/resource.tf new file mode 100644 index 00000000..5e87049e --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_acl/resource.tf @@ -0,0 +1,21 @@ +resource "proxmox_virtual_environment_user" "operations_automation" { + comment = "Managed by Terraform" + password = "a-strong-password" + user_id = "operations-automation@pve" +} + +resource "proxmox_virtual_environment_role" "operations_monitoring" { + role_id = "operations-monitoring" + + privileges = [ + "VM.Monitor", + ] +} + +resource "proxmox_virtual_environment_acl" "operations_automation_monitoring" { + user_id = proxmox_virtual_environment_user.operations_automation.user_id + role_id = proxmox_virtual_environment_role.operations_monitoring.role_id + + path = "/vms/1234" + propagate = true +} diff --git a/fwprovider/access/resource_acl.go b/fwprovider/access/resource_acl.go new file mode 100644 index 00000000..62372bc4 --- /dev/null +++ b/fwprovider/access/resource_acl.go @@ -0,0 +1,274 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package access + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + + "github.com/bpg/terraform-provider-proxmox/fwprovider/structure" + "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/helpers/ptr" + proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types" +) + +var ( + _ resource.Resource = (*aclResource)(nil) + _ resource.ResourceWithConfigure = (*aclResource)(nil) + _ resource.ResourceWithImportState = (*aclResource)(nil) + _ resource.ResourceWithConfigValidators = (*aclResource)(nil) +) + +type aclResource struct { + client proxmox.Client +} + +// NewACLResource creates a new ACL resource. +func NewACLResource() resource.Resource { + return &aclResource{} +} + +func (r *aclResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Manages ACLs on the Proxmox cluster", + MarkdownDescription: "Manages ACLs on the Proxmox cluster.\n\n" + + "ACLs are used to control access to resources in the Proxmox cluster.\n" + + "Each ACL consists of a path, a user, group or token, a role, and a flag to allow propagation of permissions.", + Attributes: map[string]schema.Attribute{ + "group_id": schema.StringAttribute{ + Description: "The group the ACL should apply to (mutually exclusive with `token_id` and `user_id`)", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "id": structure.IDAttribute(), + "path": schema.StringAttribute{ + Description: "Access control path", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "propagate": schema.BoolAttribute{ + Description: "Allow to propagate (inherit) permissions.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + }, + "role_id": schema.StringAttribute{ + Description: "The role to apply", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "token_id": schema.StringAttribute{ + Description: "The token the ACL should apply to (mutually exclusive with `group_id` and `user_id`)", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "user_id": schema.StringAttribute{ + Description: "The user the ACL should apply to (mutually exclusive with `group_id` and `token_id`)", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +func (r *aclResource) ConfigValidators(_ context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + resourcevalidator.Conflicting( + path.MatchRoot("group_id"), + path.MatchRoot("token_id"), + path.MatchRoot("user_id"), + ), + } +} + +func (r *aclResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(proxmox.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *proxmox.Client, got: %T. Please report this issue to the provider developers.", + req.ProviderData), + ) + + return + } + + r.client = client +} + +func (r *aclResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_acl" +} + +func (r *aclResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan aclResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + if resp.Diagnostics.HasError() { + return + } + + body := plan.intoUpdateBody() + + err := r.client.Access().UpdateACL(ctx, body) + if err != nil { + resp.Diagnostics.AddError("Unable to create ACL", apiCallFailed+err.Error()) + return + } + + err = plan.generateID() + if err != nil { + resp.Diagnostics.AddError("Unable to create ACL", "failed to generate ID: "+err.Error()) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *aclResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state aclResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + acls, err := r.client.Access().GetACL(ctx) + if err != nil { + resp.Diagnostics.AddError("Unable read ACL", apiCallFailed+err.Error()) + return + } + + for _, acl := range acls { + switch acl.Type { + case "group": + if acl.UserOrGroupID != state.GroupID.ValueString() { + continue + } + case "token": + if acl.UserOrGroupID != state.TokenID.ValueString() { + continue + } + case "user": + if acl.UserOrGroupID != state.UserID.ValueString() { + continue + } + default: + // ignore unknown values + continue + } + + if acl.Path != state.Path { + continue + } + + if acl.RoleID != state.RoleID { + continue + } + + state.Propagate = ptr.Or(acl.Propagate.PointerBool(), true) + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) + + return + } + + resp.State.RemoveResource(ctx) +} + +func (r *aclResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var ( + state aclResourceModel + plan aclResourceModel + ) + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + if resp.Diagnostics.HasError() { + return + } + + stateBody := state.intoUpdateBody() + stateBody.Delete = proxmoxtypes.CustomBool(true).Pointer() + + err := r.client.Access().UpdateACL(ctx, stateBody) + if err != nil { + resp.Diagnostics.AddError("Unable to delete old ACL", apiCallFailed+err.Error()) + return + } + + planBody := plan.intoUpdateBody() + + err = r.client.Access().UpdateACL(ctx, planBody) + if err != nil { + resp.Diagnostics.AddError("Unable to create ACL", apiCallFailed+err.Error()) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *aclResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state aclResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + stateBody := state.intoUpdateBody() + stateBody.Delete = proxmoxtypes.CustomBool(true).Pointer() + + err := r.client.Access().UpdateACL(ctx, stateBody) + if err != nil { + resp.Diagnostics.AddError("Unable to delete old ACL", apiCallFailed+err.Error()) + return + } +} + +func (r *aclResource) ImportState( + ctx context.Context, + req resource.ImportStateRequest, + resp *resource.ImportStateResponse, +) { + model, err := parseACLResourceModelFromID(req.ID) + if err != nil { + resp.Diagnostics.AddError("Unable to import ACL", "failed to parse ID: "+err.Error()) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) +} + +const apiCallFailed = "API call failed: " diff --git a/fwprovider/access/resource_acl_model.go b/fwprovider/access/resource_acl_model.go new file mode 100644 index 00000000..1ea3a4b0 --- /dev/null +++ b/fwprovider/access/resource_acl_model.go @@ -0,0 +1,125 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package access + +import ( + "fmt" + "net/url" + "strings" + + "github.com/gorilla/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + + proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types" + + "github.com/bpg/terraform-provider-proxmox/proxmox/access" +) + +type aclResourceModel struct { + ID types.String `tfsdk:"id"` + + GroupID types.String `tfsdk:"group_id"` + Path string `tfsdk:"path"` + Propagate bool `tfsdk:"propagate"` + RoleID string `tfsdk:"role_id"` + TokenID types.String `tfsdk:"token_id"` + UserID types.String `tfsdk:"user_id"` +} + +type aclResourceIDFields struct { + EntityID string `schema:"entity_id"` + RoleID string `schema:"role_id"` +} + +const aclIDFormat = "{path}?entity_id={group|user@realm|user@realm!token}?role_id={role}" + +func (r *aclResourceModel) generateID() error { + encoder := schema.NewEncoder() + + fields := aclResourceIDFields{ + EntityID: r.GroupID.ValueString() + r.TokenID.ValueString() + r.UserID.ValueString(), + RoleID: r.RoleID, + } + v := url.Values{} + + err := encoder.Encode(fields, v) + if err != nil { + return fmt.Errorf("failed to encode ACL resource ID: %w", err) + } + + r.ID = types.StringValue(r.Path + "?" + v.Encode()) + + return nil +} + +func parseACLResourceModelFromID(id string) (*aclResourceModel, error) { + path, query, found := strings.Cut(id, "?") + + if !found { + return nil, fmt.Errorf("invalid ACL resource ID format %#v, expected %v", id, aclIDFormat) + } + + v, err := url.ParseQuery(query) + if err != nil { + return nil, fmt.Errorf("invalid ACL resource ID format %#v, expected %v: %w", id, aclIDFormat, err) + } + + decoder := schema.NewDecoder() + + fields := aclResourceIDFields{} + + err = decoder.Decode(&fields, v) + if err != nil { + return nil, fmt.Errorf("invalid ACL resource ID format %#v, expected %v: %w", id, aclIDFormat, err) + } + + model := &aclResourceModel{ + ID: types.StringValue(id), + GroupID: types.StringNull(), + Path: path, + Propagate: false, + RoleID: fields.RoleID, + TokenID: types.StringNull(), + UserID: types.StringNull(), + } + + switch { + case strings.Contains(fields.EntityID, "!"): + model.TokenID = types.StringValue(fields.EntityID) + case strings.Contains(fields.EntityID, "@"): + model.UserID = types.StringValue(fields.EntityID) + default: + model.GroupID = types.StringValue(fields.EntityID) + } + + return model, nil +} + +func (r *aclResourceModel) intoUpdateBody() *access.ACLUpdateRequestBody { + body := &access.ACLUpdateRequestBody{ + Groups: nil, + Path: r.Path, + Propagate: proxmoxtypes.CustomBoolPtr(r.Propagate), + Roles: []string{r.RoleID}, + Tokens: nil, + Users: nil, + } + + if !r.GroupID.IsNull() { + body.Groups = []string{r.GroupID.ValueString()} + } + + if !r.TokenID.IsNull() { + body.Tokens = []string{r.TokenID.ValueString()} + } + + if !r.UserID.IsNull() { + body.Users = []string{r.UserID.ValueString()} + } + + return body +} diff --git a/fwprovider/access/resource_user_token.go b/fwprovider/access/resource_user_token.go index 331e0e5f..54e2671f 100644 --- a/fwprovider/access/resource_user_token.go +++ b/fwprovider/access/resource_user_token.go @@ -144,7 +144,7 @@ func (r *userTokenResource) Create(ctx context.Context, req resource.CreateReque body := access.UserTokenCreateRequestBody{ Comment: plan.Comment.ValueStringPointer(), - PrivSeparate: proxmoxtypes.CustomBoolPtr(plan.PrivSeparation.ValueBoolPointer()), + PrivSeparate: proxmoxtypes.CustomBoolPtr(plan.PrivSeparation.ValueBool()), } if !plan.ExpirationDate.IsNull() && plan.ExpirationDate.ValueString() != "" { @@ -215,7 +215,7 @@ func (r *userTokenResource) Update(ctx context.Context, req resource.UpdateReque body := access.UserTokenUpdateRequestBody{ Comment: plan.Comment.ValueStringPointer(), - PrivSeparate: proxmoxtypes.CustomBoolPtr(plan.PrivSeparation.ValueBoolPointer()), + PrivSeparate: proxmoxtypes.CustomBoolPtr(plan.PrivSeparation.ValueBool()), } if !plan.ExpirationDate.IsNull() && plan.ExpirationDate.ValueString() != "" { diff --git a/fwprovider/provider.go b/fwprovider/provider.go index 355676d7..e0d1d0d4 100644 --- a/fwprovider/provider.go +++ b/fwprovider/provider.go @@ -451,6 +451,7 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc vm.NewVMResource, NewClusterOptionsResource, NewDownloadFileResource, + access.NewACLResource, } } diff --git a/fwprovider/structure/attribute.go b/fwprovider/structure/attribute.go index 462141fc..142c05e6 100644 --- a/fwprovider/structure/attribute.go +++ b/fwprovider/structure/attribute.go @@ -15,11 +15,11 @@ import ( // IDAttribute generates an attribute definition suitable for the always-present `id` attribute. func IDAttribute(desc ...string) schema.StringAttribute { attr := schema.StringAttribute{ - Computed: true, + Computed: true, + Description: "The unique identifier of this resource.", PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, - Description: "The unique identifier of this resource.", } if len(desc) > 0 { diff --git a/fwprovider/tests/resource_acl_test.go b/fwprovider/tests/resource_acl_test.go new file mode 100644 index 00000000..1aec2743 --- /dev/null +++ b/fwprovider/tests/resource_acl_test.go @@ -0,0 +1,174 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package tests + +import ( + "context" + "fmt" + "net/url" + "regexp" + "testing" + + "github.com/brianvoe/gofakeit/v7" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/stretchr/testify/require" + + "github.com/bpg/terraform-provider-proxmox/proxmox/access" +) + +func TestAccAcl_User(t *testing.T) { + te := initTestEnvironment(t) + + userID := fmt.Sprintf("%s@pve", gofakeit.Username()) + te.addTemplateVars(map[string]any{ + "UserID": userID, + }) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: te.accProviders, + CheckDestroy: nil, + PreCheck: func() { + err := te.accessClient().CreateUser(context.Background(), &access.UserCreateRequestBody{ + ID: userID, + Password: gofakeit.Password(true, true, true, true, false, 8), + }) + require.NoError(t, err) + + t.Cleanup(func() { + err := te.accessClient().DeleteUser(context.Background(), userID) + require.NoError(t, err) + }) + }, + Steps: []resource.TestStep{ + { + Config: te.renderConfig(`resource "proxmox_virtual_environment_acl" "test" { + user_id = "{{.UserID}}" + path = "/" + role_id = "NoAccess" + }`), + Check: resource.ComposeTestCheckFunc( + testResourceAttributes("proxmox_virtual_environment_acl.test", map[string]string{ + "path": "/", + "role_id": "NoAccess", + "user_id": userID, + "propagate": "true", + }), + testNoResourceAttributesSet("proxmox_virtual_environment_acl.test", []string{ + "group_id", + "token_id", + }), + ), + }, + { + ResourceName: "proxmox_virtual_environment_acl.test", + ImportState: true, + ImportStateIdFunc: testAccACLImportStateIDFunc(), + ImportStateVerify: true, + }, + { + Config: te.renderConfig(`resource "proxmox_virtual_environment_acl" "test" { + user_id = "{{.UserID}}" + path = "/" + role_id = "PVEPoolUser" + }`), + Check: resource.ComposeTestCheckFunc( + testResourceAttributes("proxmox_virtual_environment_acl.test", map[string]string{ + "path": "/", + "role_id": "PVEPoolUser", + "user_id": userID, + "propagate": "true", + }), + testNoResourceAttributesSet("proxmox_virtual_environment_acl.test", []string{ + "group_id", + "token_id", + }), + ), + }, + }, + }) +} + +func TestAccAcl_Validators(t *testing.T) { + t.Parallel() + + te := initTestEnvironment(t) + + resource.UnitTest(t, resource.TestCase{ + ProtoV6ProviderFactories: te.accProviders, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + PlanOnly: true, + Config: `resource "proxmox_virtual_environment_acl" "test" { + group_id = "test" + path = "/" + role_id = "test" + token_id = "test" + }`, + ExpectError: regexp.MustCompile(`.*Error: Invalid Attribute Combination`), + }, + { + PlanOnly: true, + Config: `resource "proxmox_virtual_environment_acl" "test" { + path = "/" + role_id = "test" + token_id = "test" + user_id = "test" + }`, + ExpectError: regexp.MustCompile(`.*Error: Invalid Attribute Combination`), + }, + { + PlanOnly: true, + Config: `resource "proxmox_virtual_environment_acl" "test" { + group_id = "test" + path = "/" + role_id = "test" + user_id = "test" + }`, + ExpectError: regexp.MustCompile(`.*Error: Invalid Attribute Combination`), + }, + { + PlanOnly: true, + Config: `resource "proxmox_virtual_environment_acl" "test" { + group_id = "test" + path = "/" + role_id = "test" + token_id = "test" + user_id = "test" + }`, + ExpectError: regexp.MustCompile(`.*Error: Invalid Attribute Combination`), + }, + }, + }) +} + +func testAccACLImportStateIDFunc() resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + resourceName := "proxmox_virtual_environment_acl.test" + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("not found: %s", resourceName) + } + + path := rs.Primary.Attributes["path"] + + groupID := rs.Primary.Attributes["group_id"] + tokenID := rs.Primary.Attributes["token_id"] + userID := rs.Primary.Attributes["user_id"] + + roleID := rs.Primary.Attributes["role_id"] + + v := url.Values{ + "entity_id": []string{groupID + tokenID + userID}, + "role_id": []string{roleID}, + } + + return path + "?" + v.Encode(), nil + } +} diff --git a/fwprovider/tests/resource_download_file_test.go b/fwprovider/tests/resource_download_file_test.go index 23fa65a5..462044aa 100644 --- a/fwprovider/tests/resource_download_file_test.go +++ b/fwprovider/tests/resource_download_file_test.go @@ -14,8 +14,8 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/stretchr/testify/require" + "github.com/bpg/terraform-provider-proxmox/proxmox/helpers/ptr" "github.com/bpg/terraform-provider-proxmox/proxmox/nodes/storage" - "github.com/bpg/terraform-provider-proxmox/proxmox/types" ) const ( @@ -133,11 +133,11 @@ func TestAccResourceDownloadFile(t *testing.T) { _ = te.nodeStorageClient().DeleteDatastoreFile(ctx, "iso/fake_file.iso") //nolint: errcheck err := te.nodeStorageClient().DownloadFileByURL(ctx, &storage.DownloadURLPostRequestBody{ - Content: types.StrPtr("iso"), - FileName: types.StrPtr("fake_file.iso"), - Node: types.StrPtr(te.nodeName), - Storage: types.StrPtr(te.datastoreID), - URL: types.StrPtr(fakeFileISO), + Content: ptr.Ptr("iso"), + FileName: ptr.Ptr("fake_file.iso"), + Node: ptr.Ptr(te.nodeName), + Storage: ptr.Ptr(te.datastoreID), + URL: ptr.Ptr(fakeFileISO), }) require.NoError(t, err) diff --git a/fwprovider/tests/resource_user_test.go b/fwprovider/tests/resource_user_test.go index 158f2ba7..3a596d41 100644 --- a/fwprovider/tests/resource_user_test.go +++ b/fwprovider/tests/resource_user_test.go @@ -23,9 +23,9 @@ func TestAccResourceUser(t *testing.T) { te := initTestEnvironment(t) - username := fmt.Sprintf("%s@pve", gofakeit.Username()) + userID := fmt.Sprintf("%s@pve", gofakeit.Username()) te.addTemplateVars(map[string]any{ - "Username": username, + "UserID": userID, }) tests := []struct { @@ -36,35 +36,35 @@ func TestAccResourceUser(t *testing.T) { { Config: te.renderConfig(`resource "proxmox_virtual_environment_user" "user" { comment = "Managed by Terraform" - email = "{{.Username}}" + email = "{{.UserID}}" enabled = true expiration_date = "2034-01-01T22:00:00Z" first_name = "First" last_name = "Last" - user_id = "{{.Username}}" + user_id = "{{.UserID}}" }`), Check: testResourceAttributes("proxmox_virtual_environment_user.user", map[string]string{ "comment": "Managed by Terraform", - "email": username, + "email": userID, "enabled": "true", "expiration_date": "2034-01-01T22:00:00Z", "first_name": "First", "last_name": "Last", - "user_id": username, + "user_id": userID, }), }, { Config: te.renderConfig(`resource "proxmox_virtual_environment_user" "user" { enabled = false expiration_date = "2035-01-01T22:00:00Z" - user_id = "{{.Username}}" + user_id = "{{.UserID}}" first_name = "First One" }`), Check: testResourceAttributes("proxmox_virtual_environment_user.user", map[string]string{ "enabled": "false", "expiration_date": "2035-01-01T22:00:00Z", "first_name": "First One", - "user_id": username, + "user_id": userID, }), }, { @@ -89,11 +89,11 @@ func TestAccResourceUserToken(t *testing.T) { t.Parallel() te := initTestEnvironment(t) - username := fmt.Sprintf("%s@pve", gofakeit.Username()) + userID := fmt.Sprintf("%s@pve", gofakeit.Username()) tokenName := gofakeit.Word() te.addTemplateVars(map[string]any{ - "Username": username, + "UserID": userID, "TokenName": tokenName, }) @@ -106,13 +106,13 @@ func TestAccResourceUserToken(t *testing.T) { "create and update user token", func() { err := te.accessClient().CreateUser(context.Background(), &access.UserCreateRequestBody{ - ID: username, + ID: userID, Password: gofakeit.Password(true, true, true, true, false, 8), }) require.NoError(t, err) t.Cleanup(func() { - err := te.accessClient().DeleteUser(context.Background(), username) + err := te.accessClient().DeleteUser(context.Background(), userID) require.NoError(t, err) }) }, @@ -122,14 +122,14 @@ func TestAccResourceUserToken(t *testing.T) { comment = "Managed by Terraform" expiration_date = "2034-01-01T22:00:00Z" token_name = "{{.TokenName}}" - user_id = "{{.Username}}" + user_id = "{{.UserID}}" }`), Check: testResourceAttributes("proxmox_virtual_environment_user_token.user_token", map[string]string{ "comment": "Managed by Terraform", "expiration_date": "2034-01-01T22:00:00Z", - "id": fmt.Sprintf("%s!%s", username, tokenName), - "user_id": username, - "value": fmt.Sprintf("%s!%s=.*", username, tokenName), + "id": fmt.Sprintf("%s!%s", userID, tokenName), + "user_id": userID, + "value": fmt.Sprintf("%s!%s=.*", userID, tokenName), }), }, { @@ -138,7 +138,7 @@ func TestAccResourceUserToken(t *testing.T) { expiration_date = "2033-01-01T01:01:01Z" privileges_separation = false token_name = "{{.TokenName}}" - user_id = "{{.Username}}" + user_id = "{{.UserID}}" }`), Check: resource.ComposeTestCheckFunc( testResourceAttributes("proxmox_virtual_environment_user_token.user_token", map[string]string{ @@ -146,7 +146,7 @@ func TestAccResourceUserToken(t *testing.T) { "expiration_date": "2033-01-01T01:01:01Z", "privileges_separation": "false", "token_name": tokenName, - "user_id": username, + "user_id": userID, }), testNoResourceAttributesSet("proxmox_virtual_environment_user_token.user_token", []string{ "value", diff --git a/fwprovider/types/tags/value.go b/fwprovider/types/tags/value.go index ffa5b1a6..c5181807 100644 --- a/fwprovider/types/tags/value.go +++ b/fwprovider/types/tags/value.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types" + "github.com/bpg/terraform-provider-proxmox/proxmox/helpers/ptr" ) // Ensure the implementations satisfy the required interfaces. @@ -67,7 +67,7 @@ func (v Value) ValueStringPointer(ctx context.Context, diags *diag.Diagnostics) } } - return proxmoxtypes.StrPtr(strings.Join(sanitizedTags, ";")) + return ptr.Ptr(strings.Join(sanitizedTags, ";")) } // SetValue converts a string of tags to a tags set value. diff --git a/fwprovider/vm/resource.go b/fwprovider/vm/resource.go index 8ffa3264..68aef1d7 100644 --- a/fwprovider/vm/resource.go +++ b/fwprovider/vm/resource.go @@ -137,7 +137,7 @@ func (r *vmResource) create(ctx context.Context, plan vmModel, diags *diag.Diagn Description: plan.Description.ValueStringPointer(), Name: plan.Name.ValueStringPointer(), Tags: plan.Tags.ValueStringPointer(ctx, diags), - Template: proxmoxtypes.CustomBoolPtr(plan.Template.ValueBoolPointer()), + Template: proxmoxtypes.CustomBoolPtr(plan.Template.ValueBool()), VMID: int(plan.ID.ValueInt64()), } diff --git a/go.mod b/go.mod index 25f92372..70103d83 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/google/go-cmp v0.6.0 github.com/google/go-querystring v1.1.0 github.com/google/uuid v1.6.0 + github.com/gorilla/schema v1.2.1 github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 github.com/hashicorp/terraform-plugin-framework v1.8.0 github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 diff --git a/go.sum b/go.sum index 6993df5b..18340e0e 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,8 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/schema v1.2.1 h1:tjDxcmdb+siIqkTNoV+qRH2mjYdr2hHe5MKXbp61ziM= +github.com/gorilla/schema v1.2.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/proxmox/access/acl.go b/proxmox/access/acl.go index 24df34f0..cd403083 100644 --- a/proxmox/access/acl.go +++ b/proxmox/access/acl.go @@ -20,7 +20,7 @@ func (c *Client) aclPath() string { } // GetACL retrieves the access control list. -func (c *Client) GetACL(ctx context.Context) ([]*ACLGetResponseData, error) { +func (c *Client) GetACL(ctx context.Context) ([]ACLGetResponseData, error) { resBody := &ACLGetResponseBody{} err := c.DoRequest(ctx, http.MethodGet, c.aclPath(), nil, resBody) diff --git a/proxmox/access/acl_types.go b/proxmox/access/acl_types.go index 88fcda0e..0c9b1d4d 100644 --- a/proxmox/access/acl_types.go +++ b/proxmox/access/acl_types.go @@ -12,7 +12,7 @@ import ( // ACLGetResponseBody contains the body from an access control list response. type ACLGetResponseBody struct { - Data []*ACLGetResponseData `json:"data,omitempty"` + Data []ACLGetResponseData `json:"data,omitempty"` } // ACLGetResponseData contains the data from an access control list response. @@ -31,5 +31,6 @@ type ACLUpdateRequestBody struct { Path string `json:"path" url:"path"` Propagate *types.CustomBool `json:"propagate,omitempty" url:"propagate,omitempty,int"` Roles []string `json:"roles" url:"roles,comma"` + Tokens []string `json:"tokens,omitempty" url:"tokens,omitempty,comma"` Users []string `json:"users,omitempty" url:"users,omitempty,comma"` } diff --git a/proxmox/helpers/ptr/ptr.go b/proxmox/helpers/ptr/ptr.go new file mode 100644 index 00000000..e533d4dd --- /dev/null +++ b/proxmox/helpers/ptr/ptr.go @@ -0,0 +1,21 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ptr + +// Ptr creates a ptr from a value to use it inline. +func Ptr[T any](val T) *T { + return &val +} + +// Or will dereference a pointer and return the given value if it's nil. +func Or[T any](p *T, or T) T { + if p != nil { + return *p + } + + return or +} diff --git a/proxmox/nodes/vms/customstoragedevice.go b/proxmox/nodes/vms/customstoragedevice.go index 5c0acf25..cfe03134 100644 --- a/proxmox/nodes/vms/customstoragedevice.go +++ b/proxmox/nodes/vms/customstoragedevice.go @@ -215,35 +215,6 @@ func (d CustomStorageDevice) EncodeValues(key string, v *url.Values) error { return nil } -// Copy returns a deep copy of the CustomStorageDevice. -func (d CustomStorageDevice) Copy() *CustomStorageDevice { - return &CustomStorageDevice{ - AIO: types.CopyString(d.AIO), - Backup: d.Backup.Copy(), - BurstableReadSpeedMbps: types.CopyInt(d.BurstableReadSpeedMbps), - BurstableWriteSpeedMbps: types.CopyInt(d.BurstableWriteSpeedMbps), - Cache: types.CopyString(d.Cache), - DatastoreID: types.CopyString(d.DatastoreID), - Discard: types.CopyString(d.Discard), - Enabled: d.Enabled, - FileID: types.CopyString(d.FileID), - FileVolume: d.FileVolume, - Format: types.CopyString(d.Format), - Interface: types.CopyString(d.Interface), - IopsRead: types.CopyInt(d.IopsRead), - IopsWrite: types.CopyInt(d.IopsWrite), - IOThread: d.IOThread.Copy(), - MaxIopsRead: types.CopyInt(d.MaxIopsRead), - MaxIopsWrite: types.CopyInt(d.MaxIopsWrite), - MaxReadSpeedMbps: types.CopyInt(d.MaxReadSpeedMbps), - MaxWriteSpeedMbps: types.CopyInt(d.MaxWriteSpeedMbps), - Media: types.CopyString(d.Media), - Replicate: d.Replicate.Copy(), - Size: d.Size.Copy(), - SSD: d.SSD.Copy(), - } -} - // CustomStorageDevices handles map of QEMU storage device per disk interface. type CustomStorageDevices map[string]*CustomStorageDevice diff --git a/proxmox/nodes/vms/vms_types_test.go b/proxmox/nodes/vms/vms_types_test.go index 548df1a6..5f72cb56 100644 --- a/proxmox/nodes/vms/vms_types_test.go +++ b/proxmox/nodes/vms/vms_types_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/bpg/terraform-provider-proxmox/proxmox/helpers/ptr" "github.com/bpg/terraform-provider-proxmox/proxmox/types" ) @@ -29,26 +30,26 @@ func TestCustomStorageDevice_UnmarshalJSON(t *testing.T) { name: "simple volume", line: `"local-lvm:vm-2041-disk-0,discard=on,ssd=1,iothread=1,size=8G,cache=writeback"`, want: &CustomStorageDevice{ - Cache: types.StrPtr("writeback"), - Discard: types.StrPtr("on"), + Cache: ptr.Ptr("writeback"), + Discard: ptr.Ptr("on"), Enabled: true, FileVolume: "local-lvm:vm-2041-disk-0", - IOThread: types.BoolPtr(true), + IOThread: types.CustomBoolPtr(true), Size: ds8gig, - SSD: types.BoolPtr(true), + SSD: types.CustomBoolPtr(true), }, }, { name: "raw volume type", line: `"nfs:2041/vm-2041-disk-0.raw,discard=ignore,ssd=1,iothread=1,size=8G"`, want: &CustomStorageDevice{ - Discard: types.StrPtr("ignore"), + Discard: ptr.Ptr("ignore"), Enabled: true, FileVolume: "nfs:2041/vm-2041-disk-0.raw", - Format: types.StrPtr("raw"), - IOThread: types.BoolPtr(true), + Format: ptr.Ptr("raw"), + IOThread: types.CustomBoolPtr(true), Size: ds8gig, - SSD: types.BoolPtr(true), + SSD: types.CustomBoolPtr(true), }, }, } @@ -84,21 +85,21 @@ func TestCustomStorageDevice_IsCloudInitDrive(t *testing.T) { }, { name: "on directory storage", device: CustomStorageDevice{ - Media: types.StrPtr("cdrom"), + Media: ptr.Ptr("cdrom"), FileVolume: "local:131/vm-131-cloudinit.qcow2", }, want: true, }, { name: "on block storage", device: CustomStorageDevice{ - Media: types.StrPtr("cdrom"), + Media: ptr.Ptr("cdrom"), FileVolume: "local-lvm:vm-131-cloudinit", }, want: true, }, { name: "wrong VM ID", device: CustomStorageDevice{ - Media: types.StrPtr("cdrom"), + Media: ptr.Ptr("cdrom"), FileVolume: "local-lvm:vm-123-cloudinit", }, want: false, @@ -132,13 +133,13 @@ func TestCustomStorageDevice_StorageInterface(t *testing.T) { { name: "virtio0", device: CustomStorageDevice{ - Interface: types.StrPtr("virtio0"), + Interface: ptr.Ptr("virtio0"), }, want: "virtio", }, { name: "scsi13", device: CustomStorageDevice{ - Interface: types.StrPtr("scsi13"), + Interface: ptr.Ptr("scsi13"), }, want: "scsi", }, @@ -174,10 +175,10 @@ func TestCustomStorageDevices_ByStorageInterface(t *testing.T) { iface: "sata", devices: CustomStorageDevices{ "virtio0": &CustomStorageDevice{ - Interface: types.StrPtr("virtio0"), + Interface: ptr.Ptr("virtio0"), }, "scsi13": &CustomStorageDevice{ - Interface: types.StrPtr("scsi13"), + Interface: ptr.Ptr("scsi13"), }, }, want: CustomStorageDevices{}, @@ -187,21 +188,21 @@ func TestCustomStorageDevices_ByStorageInterface(t *testing.T) { iface: "virtio", devices: CustomStorageDevices{ "virtio0": &CustomStorageDevice{ - Interface: types.StrPtr("virtio0"), + Interface: ptr.Ptr("virtio0"), }, "scsi13": &CustomStorageDevice{ - Interface: types.StrPtr("scsi13"), + Interface: ptr.Ptr("scsi13"), }, "virtio1": &CustomStorageDevice{ - Interface: types.StrPtr("virtio1"), + Interface: ptr.Ptr("virtio1"), }, }, want: CustomStorageDevices{ "virtio0": &CustomStorageDevice{ - Interface: types.StrPtr("virtio0"), + Interface: ptr.Ptr("virtio0"), }, "virtio1": &CustomStorageDevice{ - Interface: types.StrPtr("virtio1"), + Interface: ptr.Ptr("virtio1"), }, }, }, @@ -239,10 +240,10 @@ func TestCustomPCIDevice_UnmarshalJSON(t *testing.T) { want: &CustomPCIDevice{ DeviceIDs: &[]string{"81:00.4"}, MDev: nil, - PCIExpress: types.BoolPtr(false), - ROMBAR: types.BoolPtr(true), + PCIExpress: types.CustomBoolPtr(false), + ROMBAR: types.CustomBoolPtr(true), ROMFile: nil, - XVGA: types.BoolPtr(false), + XVGA: types.CustomBoolPtr(false), }, }, { @@ -250,12 +251,12 @@ func TestCustomPCIDevice_UnmarshalJSON(t *testing.T) { line: `"mapping=mappeddevice,pcie=0,rombar=1,x-vga=0"`, want: &CustomPCIDevice{ DeviceIDs: nil, - Mapping: types.StrPtr("mappeddevice"), + Mapping: ptr.Ptr("mappeddevice"), MDev: nil, - PCIExpress: types.BoolPtr(false), - ROMBAR: types.BoolPtr(true), + PCIExpress: types.CustomBoolPtr(false), + ROMBAR: types.CustomBoolPtr(true), ROMFile: nil, - XVGA: types.BoolPtr(false), + XVGA: types.CustomBoolPtr(false), }, }, } @@ -289,8 +290,8 @@ func TestCustomNUMADevice_UnmarshalJSON(t *testing.T) { want: &CustomNUMADevice{ CPUIDs: []string{"1-2", "3-4"}, HostNodeNames: &[]string{"1-2"}, - Memory: types.IntPtr(1024), - Policy: types.StrPtr("preferred"), + Memory: ptr.Ptr(1024), + Policy: ptr.Ptr("preferred"), }, }, { @@ -298,7 +299,7 @@ func TestCustomNUMADevice_UnmarshalJSON(t *testing.T) { line: `"cpus=1-2,memory=1024"`, want: &CustomNUMADevice{ CPUIDs: []string{"1-2"}, - Memory: types.IntPtr(1024), + Memory: ptr.Ptr(1024), }, }, } @@ -328,15 +329,15 @@ func TestCustomUSBDevice_UnmarshalJSON(t *testing.T) { name: "id only usb device", line: `"host=0000:81"`, want: &CustomUSBDevice{ - HostDevice: types.StrPtr("0000:81"), + HostDevice: ptr.Ptr("0000:81"), }, }, { name: "usb device with more details", line: `"host=81:00,usb3=0"`, want: &CustomUSBDevice{ - HostDevice: types.StrPtr("81:00"), - USB3: types.BoolPtr(false), + HostDevice: ptr.Ptr("81:00"), + USB3: types.CustomBoolPtr(false), }, }, { @@ -344,8 +345,8 @@ func TestCustomUSBDevice_UnmarshalJSON(t *testing.T) { line: `"mapping=mappeddevice,usb=0"`, want: &CustomUSBDevice{ HostDevice: nil, - Mapping: types.StrPtr("mappeddevice"), - USB3: types.BoolPtr(false), + Mapping: ptr.Ptr("mappeddevice"), + USB3: types.CustomBoolPtr(false), }, }, } diff --git a/proxmox/types/common_types.go b/proxmox/types/common_types.go index 79ccc605..d3607610 100644 --- a/proxmox/types/common_types.go +++ b/proxmox/types/common_types.go @@ -15,6 +15,8 @@ import ( "time" "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/bpg/terraform-provider-proxmox/proxmox/helpers/ptr" ) // CustomBool allows a JSON boolean value to also be an integer. @@ -38,13 +40,9 @@ 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) +// CustomBoolPtr creates a pointer to a CustomBool. +func CustomBoolPtr(b bool) *CustomBool { + return ptr.Ptr(CustomBool(b)) } // MarshalJSON converts a boolean to a JSON value. @@ -88,15 +86,6 @@ func (r *CustomBool) FromValue(tfValue types.Bool) { *r = CustomBool(tfValue.ValueBool()) } -// Copy returns a copy of the boolean. -func (r *CustomBool) Copy() *CustomBool { - if r == nil { - return nil - } - - return BoolPtr(bool(*r)) -} - // MarshalJSON converts a boolean to a JSON value. func (r *CustomCommaSeparatedList) MarshalJSON() ([]byte, error) { s := strings.Join(*r, ",") diff --git a/proxmox/types/disk_size.go b/proxmox/types/disk_size.go index ecf40dfc..84a209dd 100644 --- a/proxmox/types/disk_size.go +++ b/proxmox/types/disk_size.go @@ -76,17 +76,6 @@ func (r *DiskSize) UnmarshalJSON(b []byte) error { return nil } -// Copy returns a deep copy of the disk size. -func (r *DiskSize) Copy() *DiskSize { - if r == nil { - return nil - } - - c := *r - - return &c -} - // ParseDiskSize parses a disk size string into a number of bytes. func ParseDiskSize(size string) (DiskSize, error) { matches := sizeRegex.FindStringSubmatch(size) diff --git a/proxmox/types/helpers.go b/proxmox/types/helpers.go deleted file mode 100644 index 6be21804..00000000 --- a/proxmox/types/helpers.go +++ /dev/null @@ -1,41 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package types - -// StrPtr returns a pointer to a string. -func StrPtr(s string) *string { - return &s -} - -// IntPtr returns a pointer to an int. -func IntPtr(i int) *int { - return &i -} - -// BoolPtr returns a pointer to a bool. -func BoolPtr(s bool) *CustomBool { - customBool := CustomBool(s) - return &customBool -} - -// CopyString copies content of a string pointer. -func CopyString(s *string) *string { - if s == nil { - return nil - } - - return StrPtr(*s) -} - -// CopyInt copies content of an int pointer. -func CopyInt(i *int) *int { - if i == nil { - return nil - } - - return IntPtr(*i) -} diff --git a/proxmoxtf/resource/vm/vm.go b/proxmoxtf/resource/vm/vm.go index fb3ec2ae..f6e12a60 100644 --- a/proxmoxtf/resource/vm/vm.go +++ b/proxmoxtf/resource/vm/vm.go @@ -17,6 +17,7 @@ import ( "strings" "time" + "github.com/bpg/terraform-provider-proxmox/proxmox/helpers/ptr" "golang.org/x/exp/maps" "github.com/bpg/terraform-provider-proxmox/proxmoxtf/resource/vm/disk" @@ -3270,7 +3271,7 @@ func vmGetSMBIOS(d *schema.ResourceData) *vms.CustomSMBIOS { } if smbios.UUID == nil || *smbios.UUID == "" { - smbios.UUID = types.StrPtr(uuid.New().String()) + smbios.UUID = ptr.Ptr(uuid.New().String()) } return &smbios diff --git a/tools/tools.go b/tools/tools.go index 5c15c686..4c689cee 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -45,5 +45,6 @@ import ( //go:generate cp ../build/docs-gen/resources/virtual_environment_haresource.md ../docs/resources/ //go:generate cp ../build/docs-gen/resources/virtual_environment_cluster_options.md ../docs/resources/ //go:generate cp ../build/docs-gen/resources/virtual_environment_download_file.md ../docs/resources/ +//go:generate cp ../build/docs-gen/resources/virtual_environment_acl.md ../docs/resources/ //go:generate cp ../build/docs-gen/resources/virtual_environment_user_token.md ../docs/resources/ //go:generate cp ../build/docs-gen/resources/virtual_environment_vm2.md ../docs/resources/