diff --git a/docs/data-sources/virtual_environment_hardware_mapping_dir.md b/docs/data-sources/virtual_environment_hardware_mapping_dir.md new file mode 100644 index 00000000..a87488f6 --- /dev/null +++ b/docs/data-sources/virtual_environment_hardware_mapping_dir.md @@ -0,0 +1,45 @@ +--- +layout: page +title: proxmox_virtual_environment_hardware_mapping_dir +parent: Data Sources +subcategory: Virtual Environment +description: |- + Retrieves a directory mapping from a Proxmox VE cluster. +--- + +# Data Source: proxmox_virtual_environment_hardware_mapping_dir + +Retrieves a directory mapping from a Proxmox VE cluster. + +## Example Usage + +```terraform +data "proxmox_virtual_environment_hardware_mapping_dir" "example" { + name = "example" +} + +output "data_proxmox_virtual_environment_hardware_mapping_dir" { + value = data.proxmox_virtual_environment_hardware_mapping_dir.example +} +``` + + +## Schema + +### Required + +- `name` (String) The name of this directory mapping. + +### Read-Only + +- `comment` (String) The comment of this directory mapping. +- `id` (String) The unique identifier of this directory mapping data source. +- `map` (Attributes Set) The actual map of devices for the directory mapping. (see [below for nested schema](#nestedatt--map)) + + +### Nested Schema for `map` + +Read-Only: + +- `node` (String) The node name attribute of the map. +- `path` (String) The path attribute of the map. diff --git a/docs/data-sources/virtual_environment_hardware_mappings.md b/docs/data-sources/virtual_environment_hardware_mappings.md index 98e2b65a..d2f3ec73 100644 --- a/docs/data-sources/virtual_environment_hardware_mappings.md +++ b/docs/data-sources/virtual_environment_hardware_mappings.md @@ -14,6 +14,11 @@ Retrieves a list of hardware mapping resources. ## Example Usage ```terraform +data "proxmox_virtual_environment_hardware_mappings" "example-dir" { + check_node = "pve" + type = "dir" +} + data "proxmox_virtual_environment_hardware_mappings" "example-pci" { check_node = "pve" type = "pci" diff --git a/docs/resources/virtual_environment_hardware_mapping_dir.md b/docs/resources/virtual_environment_hardware_mapping_dir.md new file mode 100644 index 00000000..e95bff26 --- /dev/null +++ b/docs/resources/virtual_environment_hardware_mapping_dir.md @@ -0,0 +1,62 @@ +--- +layout: page +title: proxmox_virtual_environment_hardware_mapping_dir +parent: Resources +subcategory: Virtual Environment +description: |- + Manages a directory mapping in a Proxmox VE cluster. +--- + +# Resource: proxmox_virtual_environment_hardware_mapping_dir + +Manages a directory mapping in a Proxmox VE cluster. + +## Example Usage + +```terraform +resource "proxmox_virtual_environment_hardware_mapping_dir" "example" { + comment = "This is a comment" + name = "example" + # The actual map of devices. + map = [ + { + node = "pve" + path = "/mnt/data" + }, + ] +} +``` + + +## Schema + +### Required + +- `map` (Attributes Set) The actual map of devices for the hardware mapping. (see [below for nested schema](#nestedatt--map)) +- `name` (String) The name of this directory mapping. + +### Optional + +- `comment` (String) The comment of this directory mapping. + +### Read-Only + +- `id` (String) The unique identifier of this directory mapping resource. + + +### Nested Schema for `map` + +Required: + +- `node` (String) The node this mapping applies to. +- `path` (String) The path of the map. For directory mappings the path is required and refers to the POSIX path of the directory as visible from the node. + +## Import + +Import is supported using the following syntax: + +```shell +#!/usr/bin/env sh +# A directory mapping can be imported using their name, e.g.: +terraform import proxmox_virtual_environment_hardware_mapping_dir.example example +``` diff --git a/example/resource_virtual_environment_vm.tf b/example/resource_virtual_environment_vm.tf index d6d948c1..56554b7e 100644 --- a/example/resource_virtual_environment_vm.tf +++ b/example/resource_virtual_environment_vm.tf @@ -240,6 +240,15 @@ resource "proxmox_virtual_environment_vm" "data_vm" { } } +resource "proxmox_virtual_environment_hardware_mapping_dir" "dir_mapping" { + name = "terraform-provider-proxmox-dir-mapping" + + map = [{ + node = data.proxmox_virtual_environment_nodes.example.names[0] + path = "/mnt" + }] +} + output "resource_proxmox_virtual_environment_vm_example_id" { value = proxmox_virtual_environment_vm.example.id } diff --git a/examples/data-sources/proxmox_virtual_environment_hardware_mapping_dir/data-source.tf b/examples/data-sources/proxmox_virtual_environment_hardware_mapping_dir/data-source.tf new file mode 100644 index 00000000..19c1339e --- /dev/null +++ b/examples/data-sources/proxmox_virtual_environment_hardware_mapping_dir/data-source.tf @@ -0,0 +1,7 @@ +data "proxmox_virtual_environment_hardware_mapping_dir" "example" { + name = "example" +} + +output "data_proxmox_virtual_environment_hardware_mapping_dir" { + value = data.proxmox_virtual_environment_hardware_mapping_dir.example +} diff --git a/examples/data-sources/proxmox_virtual_environment_hardware_mappings/data-source.tf b/examples/data-sources/proxmox_virtual_environment_hardware_mappings/data-source.tf index 768729e3..568f25e4 100644 --- a/examples/data-sources/proxmox_virtual_environment_hardware_mappings/data-source.tf +++ b/examples/data-sources/proxmox_virtual_environment_hardware_mappings/data-source.tf @@ -1,3 +1,8 @@ +data "proxmox_virtual_environment_hardware_mappings" "example-dir" { + check_node = "pve" + type = "dir" +} + data "proxmox_virtual_environment_hardware_mappings" "example-pci" { check_node = "pve" type = "pci" diff --git a/examples/resources/proxmox_virtual_environment_hardware_mapping_dir/import.sh b/examples/resources/proxmox_virtual_environment_hardware_mapping_dir/import.sh new file mode 100644 index 00000000..d04aa4c3 --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_hardware_mapping_dir/import.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +# A directory mapping can be imported using their name, e.g.: +terraform import proxmox_virtual_environment_hardware_mapping_dir.example example diff --git a/examples/resources/proxmox_virtual_environment_hardware_mapping_dir/resource.tf b/examples/resources/proxmox_virtual_environment_hardware_mapping_dir/resource.tf new file mode 100644 index 00000000..84bc18f1 --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_hardware_mapping_dir/resource.tf @@ -0,0 +1,11 @@ +resource "proxmox_virtual_environment_hardware_mapping_dir" "example" { + comment = "This is a comment" + name = "example" + # The actual map of devices. + map = [ + { + node = "pve" + path = "/mnt/data" + }, + ] +} diff --git a/fwprovider/nodes/hardwaremapping/datasource.go b/fwprovider/nodes/hardwaremapping/datasource.go index f5b2056e..dcbc73a6 100644 --- a/fwprovider/nodes/hardwaremapping/datasource.go +++ b/fwprovider/nodes/hardwaremapping/datasource.go @@ -111,10 +111,12 @@ func (d *dataSource) Read(ctx context.Context, req datasource.ReadRequest, resp // name. // Note that the Proxmox VE API, for whatever reason, only returns one error at a time, even though the field is an // array. - if (len(data.ChecksPCI) > 0) || len(data.ChecksUSB) > 0 { + if (len(data.Checks) > 0) || len(data.ChecksUSB) > 0 { switch data.Type { + case proxmoxtypes.TypeDir: + hm.Checks = append(hm.Checks, createCheckDiagnostics(data.ID, data.Checks)...) case proxmoxtypes.TypePCI: - hm.Checks = append(hm.Checks, createCheckDiagnostics(data.ID, data.ChecksPCI)...) + hm.Checks = append(hm.Checks, createCheckDiagnostics(data.ID, data.Checks)...) case proxmoxtypes.TypeUSB: hm.Checks = append(hm.Checks, createCheckDiagnostics(data.ID, data.ChecksUSB)...) } @@ -188,6 +190,7 @@ func (d *dataSource) Schema( Validators: []validator.String{ stringvalidator.OneOf( []string{ + proxmoxtypes.TypeDir.String(), proxmoxtypes.TypePCI.String(), proxmoxtypes.TypeUSB.String(), }..., diff --git a/fwprovider/nodes/hardwaremapping/datasource_dir.go b/fwprovider/nodes/hardwaremapping/datasource_dir.go new file mode 100644 index 00000000..155b9b23 --- /dev/null +++ b/fwprovider/nodes/hardwaremapping/datasource_dir.go @@ -0,0 +1,140 @@ +/* + * 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 hardwaremapping + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/bpg/terraform-provider-proxmox/fwprovider/attribute" + "github.com/bpg/terraform-provider-proxmox/fwprovider/config" + customtypes "github.com/bpg/terraform-provider-proxmox/fwprovider/types/hardwaremapping" + mappings "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/mapping" + proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types/hardwaremapping" +) + +// Ensure the implementation satisfies the required interfaces. +var ( + _ datasource.DataSource = &dirDataSource{} + _ datasource.DataSourceWithConfigure = &dirDataSource{} +) + +// dirDataSource is the data source implementation for a directory mapping. +type dirDataSource struct { + client *mappings.Client +} + +// Configure adds the provider-configured client to the data source. +func (d *dirDataSource) Configure( + _ context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { + if req.ProviderData == nil { + return + } + + cfg, ok := req.ProviderData.(config.DataSource) + if !ok { + resp.Diagnostics.AddError( + "Unexpected DataSource Configure Type", + fmt.Sprintf("Expected config.DataSource, got: %T", req.ProviderData), + ) + + return + } + + d.client = cfg.Client.Cluster().HardwareMapping() +} + +// Metadata returns the data source type name. +func (d *dirDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_hardware_mapping_dir" +} + +// Read fetches the specified directory mapping from the Proxmox VE API. +func (d *dirDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var hm modelDir + + resp.Diagnostics.Append(req.Config.Get(ctx, &hm)...) + + if resp.Diagnostics.HasError() { + return + } + + hmID := hm.Name.ValueString() + // Ensure to keep both in sync since the name represents the ID. + hm.ID = hm.Name + + data, err := d.client.Get(ctx, proxmoxtypes.TypeDir, hmID) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Unable to read directory mapping %q", hmID), + err.Error(), + ) + + return + } + + hm.importFromAPI(ctx, data) + resp.Diagnostics.Append(resp.State.Set(ctx, &hm)...) +} + +// Schema defines the schema for the directory mapping. +func (d *dirDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + comment := dataSourceSchemaBaseAttrComment + comment.Optional = false + comment.Computed = true + comment.Description = "The comment of this directory mapping." + + resp.Schema = schema.Schema{ + Description: "Retrieves a directory mapping from a Proxmox VE cluster.", + Attributes: map[string]schema.Attribute{ + schemaAttrNameComment: comment, + schemaAttrNameMap: schema.SetNestedAttribute{ + Computed: true, + Description: "The actual map of devices for the directory mapping.", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + schemaAttrNameMapNode: schema.StringAttribute{ + Computed: true, + Description: "The node name attribute of the map.", + }, + schemaAttrNameMapPath: schema.StringAttribute{ + // For directory mappings the path is required and refers + // to the POSIX path of the directory as visible from the node. + Computed: true, + CustomType: customtypes.PathType{}, + Description: "The path attribute of the map.", + }, + }, + }, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + }, + schemaAttrNameName: schema.StringAttribute{ + Description: "The name of this directory mapping.", + Required: true, + }, + schemaAttrNameTerraformID: attribute.ResourceID( + "The unique identifier of this directory mapping data source.", + ), + }, + } +} + +// NewDirDataSource returns a new data source for a directory mapping. +// This is a helper function to simplify the provider implementation. +func NewDirDataSource() datasource.DataSource { + return &dirDataSource{} +} diff --git a/fwprovider/nodes/hardwaremapping/models.go b/fwprovider/nodes/hardwaremapping/models.go index 66ca5015..69e6c5f3 100644 --- a/fwprovider/nodes/hardwaremapping/models.go +++ b/fwprovider/nodes/hardwaremapping/models.go @@ -89,6 +89,15 @@ const ( schemaAttrNameHWMIDs = "ids" ) +// modelDirMap maps the schema data for the map of a directory mapping. +type modelDirMap struct { + // Node is the "node name" for the map. + Node types.String `tfsdk:"node"` + + // Path is the "path" for the map. + Path customtypes.PathValue `tfsdk:"path"` +} + // modelPCIMap maps the schema data for the map of a PCI hardware mapping. type modelPCIMap struct { // Comment is the "comment" for the map. @@ -140,6 +149,26 @@ type modelUSBMap struct { Path customtypes.PathValue `tfsdk:"path"` } +// modelDir maps the schema data for a directory mapping. +type modelDir struct { + // Comment is the comment of the directory mapping. + // Note that the Proxmox VE API attribute is named "description", but we map it as a comment since this naming is + // generally across the Proxmox VE web UI and API documentations. This still follows the [Terraform "best practices"] + // as it improves the user experience by matching the field name to the naming used in the human-facing interfaces. + // + // [Terraform "best practices"]: https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles#resource-and-attribute-schema-should-closely-match-the-underlying-api + Comment types.String `tfsdk:"comment"` + + // ID is the Terraform identifier. + ID types.String `tfsdk:"id"` + + // Name is the name of the directory mapping. + Name types.String `tfsdk:"name"` + + // Map is the map of the directory mapping. + Map []modelDirMap `tfsdk:"map"` +} + // modelPCI maps the schema data for a PCI hardware mapping. type modelPCI struct { // Comment is the comment of the PCI hardware mapping. @@ -223,6 +252,78 @@ type modelNodeCheckDiag struct { Severity types.String `tfsdk:"severity"` } +// importFromAPI imports the contents of a directory mapping model from the Proxmox VE API's response data. +func (hm *modelDir) importFromAPI(_ context.Context, data *apitypes.GetResponseData) { + // Ensure that both the ID and name are in sync. + hm.Name = hm.ID + // The attribute is named "description" by the Proxmox VE API, but we map it as a comment since this naming is + // generally across the Proxmox VE web UI and API documentations. + hm.Comment = types.StringPointerValue(data.Description) + maps := make([]modelDirMap, len(data.Map)) + + for idx, pveMap := range data.Map { + tfMap := modelDirMap{ + Node: types.StringValue(pveMap.Node), + Path: customtypes.NewPathPointerValue(pveMap.Path), + } + + maps[idx] = tfMap + } + + hm.Map = maps +} + +// toCreateRequest builds the request data structure for creating a new directory mapping. +func (hm *modelDir) toCreateRequest() *apitypes.CreateRequestBody { + return &apitypes.CreateRequestBody{ + DataBase: hm.toRequestBase(), + ID: hm.ID.ValueString(), + } +} + +// toRequestBase builds the common request data structure for the directory mapping creation or update API calls. +func (hm *modelDir) toRequestBase() apitypes.DataBase { + dataBase := apitypes.DataBase{ + // The attribute is named "description" by the Proxmox VE API, but we map it as a comment since this naming is + // generally across the Proxmox VE web UI and API documentations. + Description: hm.Comment.ValueStringPointer(), + } + maps := make([]proxmoxtypes.Map, len(hm.Map)) + + for idx, tfMap := range hm.Map { + pveMap := proxmoxtypes.Map{ + Node: tfMap.Node.ValueString(), + Path: tfMap.Path.ValueStringPointer(), + } + + maps[idx] = pveMap + } + + dataBase.Map = maps + + return dataBase +} + +// toUpdateRequest builds the request data structure for updating an existing USB hardware mapping. +func (hm *modelDir) toUpdateRequest(currentState *modelDir) *apitypes.UpdateRequestBody { + var del []string + + if hm.Comment.IsNull() && !currentState.Comment.IsNull() { + // The Proxmox VE API attribute is named "description" while we name it "comment" internally since this naming is + // generally used across the Proxmox VE web UI and API documentations. + // This still follows the Terraform "best practices" [1] as it improves the user experience by matching the field + // name to the naming used in the human-facing interfaces. + // References: + // 1. https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles#resource-and-attribute-schema-should-closely-match-the-underlying-api + del = append(del, proxmoxtypes.AttrNameDescription) + } + + return &apitypes.UpdateRequestBody{ + DataBase: hm.toRequestBase(), + Delete: del, + } +} + // importFromAPI imports the contents of a PCI hardware mapping model from the Proxmox VE API's response data. func (hm *modelPCI) importFromAPI(_ context.Context, data *apitypes.GetResponseData) { // Ensure that both the ID and name are in sync. diff --git a/fwprovider/nodes/hardwaremapping/resource_dir.go b/fwprovider/nodes/hardwaremapping/resource_dir.go new file mode 100644 index 00000000..3eac6cf6 --- /dev/null +++ b/fwprovider/nodes/hardwaremapping/resource_dir.go @@ -0,0 +1,280 @@ +/* + * 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 hardwaremapping + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/bpg/terraform-provider-proxmox/fwprovider/attribute" + "github.com/bpg/terraform-provider-proxmox/fwprovider/config" + customtypes "github.com/bpg/terraform-provider-proxmox/fwprovider/types/hardwaremapping" + mappings "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/mapping" + proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types/hardwaremapping" +) + +// Ensure the resource implements the required interfaces. +var ( + _ resource.Resource = &dirResource{} + _ resource.ResourceWithConfigure = &dirResource{} + _ resource.ResourceWithImportState = &dirResource{} +) + +// dirResource contains the directory mapping resource's internal data. +type dirResource struct { + // client is the hardware mapping API client. + client *mappings.Client +} + +// read reads information about a directory mapping from the Proxmox VE API. +func (r *dirResource) read(ctx context.Context, hm *modelDir) (bool, diag.Diagnostics) { + var diags diag.Diagnostics + + hmName := hm.Name.ValueString() + + data, err := r.client.Get(ctx, proxmoxtypes.TypeDir, hmName) + if err != nil { + if strings.Contains(err.Error(), "no such resource") { + diags.AddError("Could not read directory mapping", err.Error()) + } + + return false, diags + } + + hm.importFromAPI(ctx, data) + + return true, nil +} + +// readBack reads information about a created or modified directory mapping from the Proxmox VE API then updates the +// response state accordingly. +// The Terraform resource identifier must have been set in the state before this method is called! +func (r *dirResource) readBack(ctx context.Context, hm *modelDir, respDiags *diag.Diagnostics, respState *tfsdk.State) { + found, diags := r.read(ctx, hm) + + respDiags.Append(diags...) + + if !found { + respDiags.AddError( + "directory mapping resource not found after update", + "Failed to find the resource when trying to read back the updated directory mapping's data.", + ) + } + + if !respDiags.HasError() { + respDiags.Append(respState.Set(ctx, *hm)...) + } +} + +// Configure adds the provider-configured client to the resource. +func (r *dirResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + cfg, ok := req.ProviderData.(config.Resource) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected config.Resource, got: %T", req.ProviderData), + ) + + return + } + + r.client = cfg.Client.Cluster().HardwareMapping() +} + +// Create creates a new directory mapping. +func (r *dirResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var hm modelDir + + resp.Diagnostics.Append(req.Plan.Get(ctx, &hm)...) + + if resp.Diagnostics.HasError() { + return + } + + hmName := hm.Name.ValueString() + // Ensure to keep both in sync since the name represents the ID. + hm.ID = hm.Name + + apiReq := hm.toCreateRequest() + + if err := r.client.Create(ctx, proxmoxtypes.TypeDir, apiReq); err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Could not create directory mapping %q.", hmName), + err.Error(), + ) + + return + } + + r.readBack(ctx, &hm, &resp.Diagnostics, &resp.State) +} + +// Delete deletes an existing directory mapping. +func (r *dirResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var hm modelDir + + resp.Diagnostics.Append(req.State.Get(ctx, &hm)...) + + if resp.Diagnostics.HasError() { + return + } + + hmID := hm.Name.ValueString() + + if err := r.client.Delete(ctx, proxmoxtypes.TypeDir, hmID); err != nil { + if strings.Contains(err.Error(), "no such resource") { + resp.Diagnostics.AddWarning( + "directory mapping does not exist", + fmt.Sprintf( + "Could not delete directory mapping %q, it does not exist or has been deleted outside of Terraform.", + hmID, + ), + ) + } else { + resp.Diagnostics.AddError(fmt.Sprintf("Could not delete directory mapping %q.", hmID), err.Error()) + } + } +} + +// ImportState imports a directory mapping from the Proxmox VE API. +func (r *dirResource) ImportState( + ctx context.Context, + req resource.ImportStateRequest, + resp *resource.ImportStateResponse, +) { + data := modelDir{ + ID: types.StringValue(req.ID), + Name: types.StringValue(req.ID), + } + + resource.ImportStatePassthroughID(ctx, path.Root(schemaAttrNameTerraformID), req, resp) + r.readBack(ctx, &data, &resp.Diagnostics, &resp.State) +} + +// Metadata defines the name of the directory mapping. +func (r *dirResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_hardware_mapping_dir" +} + +// Read reads the directory mapping. +// + +func (r *dirResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data modelDir + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + found, diags := r.read(ctx, &data) + resp.Diagnostics.Append(diags...) + + if !resp.Diagnostics.HasError() { + if found { + resp.Diagnostics.Append(resp.State.Set(ctx, data)...) + } else { + resp.State.RemoveResource(ctx) + } + } +} + +// Schema defines the schema for the directory mapping. +func (r *dirResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + comment := resourceSchemaBaseAttrComment + comment.Description = "The comment of this directory mapping." + + resp.Schema = schema.Schema{ + Description: "Manages a directory mapping in a Proxmox VE cluster.", + Attributes: map[string]schema.Attribute{ + schemaAttrNameComment: comment, + schemaAttrNameMap: schema.SetNestedAttribute{ + Description: "The actual map of devices for the hardware mapping.", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + schemaAttrNameMapNode: schema.StringAttribute{ + Description: "The node this mapping applies to.", + Required: true, + }, + schemaAttrNameMapPath: schema.StringAttribute{ + CustomType: customtypes.PathType{}, + Description: "The path of the map. For directory mappings the path is required and refers" + + " to the POSIX path of the directory as visible from the node.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + customtypes.PathDirValueRegEx, + ErrResourceMessageInvalidPath(proxmoxtypes.TypeDir), + ), + }, + }, + }, + }, + Required: true, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + }, + schemaAttrNameName: schema.StringAttribute{ + Description: "The name of this directory mapping.", + Required: true, + }, + schemaAttrNameTerraformID: attribute.ResourceID( + "The unique identifier of this directory mapping resource.", + ), + }, + } +} + +// Update updates an existing directory mapping. +func (r *dirResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var hmCurrent, hmPlan modelDir + + resp.Diagnostics.Append(req.Plan.Get(ctx, &hmPlan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &hmCurrent)...) + + if resp.Diagnostics.HasError() { + return + } + + hmName := hmPlan.Name.ValueString() + + apiReq := hmPlan.toUpdateRequest(&hmCurrent) + + if err := r.client.Update(ctx, proxmoxtypes.TypeDir, hmName, apiReq); err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Could not update directory mapping %q.", hmName), + err.Error(), + ) + + return + } + + r.readBack(ctx, &hmPlan, &resp.Diagnostics, &resp.State) +} + +// NewDirResource returns a new resource for managing a directory mapping. +// This is a helper function to simplify the provider implementation. +func NewDirResource() resource.Resource { + return &dirResource{} +} diff --git a/fwprovider/nodes/hardwaremapping/resource_hardware_mapping_test.go b/fwprovider/nodes/hardwaremapping/resource_hardware_mapping_test.go index 45aab933..c7438be4 100644 --- a/fwprovider/nodes/hardwaremapping/resource_hardware_mapping_test.go +++ b/fwprovider/nodes/hardwaremapping/resource_hardware_mapping_test.go @@ -27,15 +27,18 @@ import ( ) const ( + accTestHardwareMappingNameDir = "proxmox_virtual_environment_hardware_mapping_dir.test" accTestHardwareMappingNamePCI = "proxmox_virtual_environment_hardware_mapping_pci.test" accTestHardwareMappingNameUSB = "proxmox_virtual_environment_hardware_mapping_usb.test" ) type accTestHardwareMappingFakeData struct { - Comments []string `fake:"{sentence:3}" fakesize:"2"` - MapComments []string `fake:"{sentence:3}" fakesize:"2"` - MapDeviceIDs []string `fake:"{linuxdeviceid}" fakesize:"2"` - MapIOMMUGroups []uint `fake:"{number:1,20}" fakesize:"2"` + Comments []string `fake:"{sentence:3}" fakesize:"2"` + MapComments []string `fake:"{sentence:3}" fakesize:"2"` + MapDeviceIDs []string `fake:"{linuxdeviceid}" fakesize:"2"` + MapIOMMUGroups []uint `fake:"{number:1,20}" fakesize:"2"` + // These paths must exist on the host system, use a hardcoded list + MapPathsDir []string `fake:"{randomstring:[/home,/root,/mnt,/tmp]}" fakesize:"2"` MapPathsPCI []string `fake:"{linuxdevicepathpci}" fakesize:"2"` MapPathsUSB []string `fake:"{linuxdevicepathusb}" fakesize:"2"` MapSubsystemIDs []string `fake:"{linuxdeviceid}" fakesize:"2"` @@ -94,6 +97,226 @@ func testAccResourceHardwareMappingInit(t *testing.T) (*accTestHardwareMappingFa return &data, te } +// TestAccResourceHardwareMappingDirValidInput runs tests for directory mapping resource definitions with valid input +// where all possible attributes are +// specified. +// All implementations of the [github.com/hashicorp/terraform-plugin-framework/resource.Resource] interface are tested +// in sequential steps. +func TestAccResourceHardwareMappingDirValidInput(t *testing.T) { + data, te := testAccResourceHardwareMappingInit(t) + + resource.Test( + t, resource.TestCase{ + ProtoV6ProviderFactories: te.AccProviders, + Steps: []resource.TestStep{ + // Test the "Create" and "Read" implementations where all possible attributes are specified. + { + Config: fmt.Sprintf( + ` + resource "proxmox_virtual_environment_hardware_mapping_dir" "test" { + comment = "%s" + name = "%s" + map = [ + { + node = "%s" + path = "%s" + }, + ] + } + `, + data.Comments[0], + data.Names[0], + te.NodeName, + data.MapPathsDir[0], + ), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(accTestHardwareMappingNameDir, "comment", data.Comments[0]), + resource.TestCheckResourceAttrSet(accTestHardwareMappingNameDir, "id"), + resource.TestCheckTypeSetElemNestedAttrs( + accTestHardwareMappingNameDir, "map.*", map[string]string{ + "node": te.NodeName, + "path": data.MapPathsDir[0], + }, + ), + resource.TestCheckResourceAttr(accTestHardwareMappingNameDir, "name", data.Names[0]), + ), + }, + + // Test the "ImportState" implementation and ensure that PCI-only attributes are not set. + { + ImportState: true, + ImportStateId: data.Names[0], + ImportStateVerify: true, + ResourceName: accTestHardwareMappingNameDir, + }, + + // Test the "Update" implementation where all possible attributes are specified. + { + Config: fmt.Sprintf( + ` + resource "proxmox_virtual_environment_hardware_mapping_dir" "test" { + comment = "%s" + name = "%s" + map = [ + { + node = "%s" + path = "%s" + }, + ] + } + `, + data.Comments[1], + data.Names[0], + te.NodeName, + data.MapPathsDir[1], + ), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(accTestHardwareMappingNameDir, "comment", data.Comments[1]), + resource.TestCheckResourceAttrSet(accTestHardwareMappingNameDir, "id"), + resource.TestCheckTypeSetElemNestedAttrs( + accTestHardwareMappingNameDir, "map.*", map[string]string{ + "node": te.NodeName, + "path": data.MapPathsDir[1], + }, + ), + resource.TestCheckResourceAttr(accTestHardwareMappingNameDir, "name", data.Names[0]), + ), + }, + }, + }, + ) +} + +// TestAccResourceHardwareMappingDirValidInputMinimal runs tests for directory mapping resource definitions with +// valid input that only have the minimum +// amount of attributes set to test computed and default values within the resulting plan and state. The last step sets +// the undefined values to test the update +// logic. +// All implementations of the [github.com/hashicorp/terraform-plugin-framework/resource.Resource] interface are tested +// in sequential steps. +func TestAccResourceHardwareMappingDirValidInputMinimal(t *testing.T) { + data, te := testAccResourceHardwareMappingInit(t) + + resource.Test( + t, resource.TestCase{ + ProtoV6ProviderFactories: te.AccProviders, + Steps: []resource.TestStep{ + // Test the "Create" and "Read" implementations with only the minimum amount of attributes being set. + { + Config: fmt.Sprintf( + ` + resource "proxmox_virtual_environment_hardware_mapping_dir" "test" { + name = "%s" + map = [ + { + node = "%s" + path = "%s" + }, + ] + } + `, + data.Names[0], + te.NodeName, + data.MapPathsDir[0], + ), + ConfigStateChecks: []statecheck.StateCheck{ + // Optional attributes should all be unset. + statecheck.ExpectKnownValue(accTestHardwareMappingNameDir, + tfjsonpath.New("comment"), + knownvalue.Null()), + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(accTestHardwareMappingNameDir, "id"), + resource.TestCheckTypeSetElemNestedAttrs( + accTestHardwareMappingNameDir, "map.*", map[string]string{ + "node": te.NodeName, + "path": data.MapPathsDir[0], + }, + ), + resource.TestCheckResourceAttr(accTestHardwareMappingNameDir, "name", data.Names[0]), + ), + }, + + // Test the "Update" implementation by setting all previously undefined attributes. + { + Config: fmt.Sprintf( + ` + resource "proxmox_virtual_environment_hardware_mapping_dir" "test" { + comment = "%s" + name = "%s" + map = [ + { + node = "%s" + path = "%s" + }, + ] + } + `, + data.Comments[0], + data.Names[0], + te.NodeName, + data.MapPathsDir[0], + ), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(accTestHardwareMappingNameDir, "comment", data.Comments[0]), + resource.TestCheckResourceAttrSet(accTestHardwareMappingNameDir, "id"), + resource.TestCheckTypeSetElemNestedAttrs( + accTestHardwareMappingNameDir, "map.*", map[string]string{ + "node": te.NodeName, + "path": data.MapPathsDir[0], + }, + ), + resource.TestCheckResourceAttr(accTestHardwareMappingNameDir, "name", data.Names[0]), + ), + }, + }, + }, + ) +} + +// TestAccResourceHardwareMappingDirInvalidInput runs tests for directory mapping resource definitions where all +// possible attributes are specified. +// Only the "Create" method implementation of the [github.com/hashicorp/terraform-plugin-framework/resource.Resource] +// interface is tested in sequential steps. +func TestAccResourceHardwareMappingDirInvalidInput(t *testing.T) { + data, te := testAccResourceHardwareMappingInit(t) + + resource.Test( + t, resource.TestCase{ + ProtoV6ProviderFactories: te.AccProviders, + Steps: []resource.TestStep{ + // Test the "Create" method implementation where all possible attributes are specified, but an error is expected + // when using an invalid device path. + { + Config: fmt.Sprintf( + ` + resource "proxmox_virtual_environment_hardware_mapping_usb" "test" { + comment = "%s" + name = "%s" + map = [ + { + comment = "%s" + id = "%s" + node = "%s" + # Only valid Linux USB device paths should pass the verification. + path = "xyz3:1337foobar" + }, + ] + } + `, + data.Comments[0], + data.Names[0], + data.Comments[1], + data.MapDeviceIDs[0], + te.NodeName, + ), + ExpectError: regexp.MustCompile(`valid Linux device path for hardware mapping of type "usb"`), + }, + }, + }, + ) +} + // TestAccResourceHardwareMappingPCIValidInput runs tests for PCI hardware mapping resource definitions with valid input // where all possible attributes are // specified. diff --git a/fwprovider/provider.go b/fwprovider/provider.go index b0ae8345..d6793321 100644 --- a/fwprovider/provider.go +++ b/fwprovider/provider.go @@ -506,6 +506,7 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc apt.NewStandardRepositoryResource, ha.NewHAGroupResource, ha.NewHAResourceResource, + hardwaremapping.NewDirResource, hardwaremapping.NewPCIResource, hardwaremapping.NewUSBResource, metrics.NewMetricsServerResource, @@ -532,6 +533,7 @@ func (p *proxmoxProvider) DataSources(_ context.Context) []func() datasource.Dat ha.NewHAResourceDataSource, ha.NewHAResourcesDataSource, hardwaremapping.NewDataSource, + hardwaremapping.NewDirDataSource, hardwaremapping.NewPCIDataSource, hardwaremapping.NewUSBDataSource, metrics.NewMetricsServerDatasource, diff --git a/fwprovider/test/resource_vm_test.go b/fwprovider/test/resource_vm_test.go index ce610878..210bda4b 100644 --- a/fwprovider/test/resource_vm_test.go +++ b/fwprovider/test/resource_vm_test.go @@ -9,9 +9,11 @@ package test import ( + "fmt" "regexp" "testing" + "github.com/brianvoe/gofakeit/v7" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/plancheck" @@ -22,6 +24,10 @@ func TestAccResourceVM(t *testing.T) { t.Parallel() te := InitEnvironment(t) + dirName := fmt.Sprintf("dir_%s", gofakeit.Word()) + te.AddTemplateVars(map[string]interface{}{ + "DirName": dirName, + }) tests := []struct { name string @@ -396,51 +402,50 @@ func TestAccResourceVM(t *testing.T) { ), }, }}, - // Depends on #1902 - // {"create virtiofs block", []resource.TestStep{ - // { - // Config: te.RenderConfig(` - // resource "proxmox_virtual_environment_hardware_mapping_dir" "test" { - // name = "test" + {"create virtiofs block", []resource.TestStep{ + { + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_hardware_mapping_dir" "test" { + name = "{{.DirName}}" - // map { - // node = "{{.NodeName}}" - // path = "/mnt" - // } - // }`, WithRootUser()), - // Check: resource.ComposeTestCheckFunc( - // ResourceAttributes("proxmox_virtual_environment_hardware_mapping_dir.test", map[string]string{ - // "name": "test", - // "map.0.node": "{{.NodeName}}", - // "map.0.path": "/mnt", - // }), - // ), - // }, - // { - // Config: te.RenderConfig(` - // resource "proxmox_virtual_environment_vm" "test_vm" { - // node_name = "{{.NodeName}}" - // started = false + map = [{ + node = "{{.NodeName}}" + path = "/mnt" + }] + }`, WithRootUser()), + Check: resource.ComposeTestCheckFunc( + ResourceAttributes("proxmox_virtual_environment_hardware_mapping_dir.test", map[string]string{ + "name": dirName, + "map.0.node": te.NodeName, + "map.0.path": "/mnt", + }), + ), + }, + { + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_vm" "test_vm" { + node_name = "{{.NodeName}}" + started = false - // virtiofs { - // mapping = "test" - // cache = "always" - // direct_io = true - // expose_acl = false - // expose_xattr = false - // } - // }`, WithRootUser()), - // Check: resource.ComposeTestCheckFunc( - // ResourceAttributes("proxmox_virtual_environment_vm.test_vm", map[string]string{ - // "virtiofs.0.mapping": "test", - // "virtiofs.0.cache": "always", - // "virtiofs.0.direct_io": "true", - // "virtiofs.0.expose_acl": "false", - // "virtiofs.0.expose_xattr": "false", - // }), - // ), - // }, - // }}, + virtiofs { + mapping = "{{.DirName}}" + cache = "always" + direct_io = true + expose_acl = false + expose_xattr = false + } + }`, WithRootUser()), + Check: resource.ComposeTestCheckFunc( + ResourceAttributes("proxmox_virtual_environment_vm.test_vm", map[string]string{ + "virtiofs.0.mapping": dirName, + "virtiofs.0.cache": "always", + "virtiofs.0.direct_io": "true", + "virtiofs.0.expose_acl": "false", + "virtiofs.0.expose_xattr": "false", + }), + ), + }, + }}, } for _, tt := range tests { diff --git a/fwprovider/types/hardwaremapping/path.go b/fwprovider/types/hardwaremapping/path.go index 284c1ae7..b7bbadd5 100644 --- a/fwprovider/types/hardwaremapping/path.go +++ b/fwprovider/types/hardwaremapping/path.go @@ -37,6 +37,9 @@ var ErrValueConversion = func(format string, attrs ...any) error { } var ( + // PathDirValueRegEx is the regular expression for a POSIX path. + PathDirValueRegEx = regexp.MustCompile(`^/.+$`) + // PathPCIValueRegEx is the regular expression for a PCI hardware mapping path. PathPCIValueRegEx = regexp.MustCompile(`^[a-f0-9]{4,}:[a-f0-9]{2}:[a-f0-9]{2}(\.[a-f0-9])?$`) @@ -128,6 +131,8 @@ func (v PathValue) Equal(o attr.Value) bool { // IsProxmoxType checks whether the value match the given hardware mapping type. func (v PathValue) IsProxmoxType(hmType proxmoxtypes.Type) bool { switch hmType { + case proxmoxtypes.TypeDir: + return PathDirValueRegEx.MatchString(v.ValueString()) case proxmoxtypes.TypePCI: return PathPCIValueRegEx.MatchString(v.ValueString()) case proxmoxtypes.TypeUSB: diff --git a/main.go b/main.go index 579cfc7d..0395120e 100644 --- a/main.go +++ b/main.go @@ -42,6 +42,7 @@ import ( //go:generate cp ./build/docs-gen/data-sources/virtual_environment_datastores.md ./docs/data-sources/ //go:generate cp ./build/docs-gen/data-sources/virtual_environment_hagroup.md ./docs/data-sources/ //go:generate cp ./build/docs-gen/data-sources/virtual_environment_hagroups.md ./docs/data-sources/ +//go:generate cp ./build/docs-gen/data-sources/virtual_environment_hardware_mapping_dir.md ./docs/data-sources/ //go:generate cp ./build/docs-gen/data-sources/virtual_environment_hardware_mapping_pci.md ./docs/data-sources/ //go:generate cp ./build/docs-gen/data-sources/virtual_environment_hardware_mapping_usb.md ./docs/data-sources/ //go:generate cp ./build/docs-gen/data-sources/virtual_environment_hardware_mappings.md ./docs/data-sources/ @@ -58,6 +59,7 @@ import ( //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_hagroup.md ./docs/resources/ +//go:generate cp ./build/docs-gen/resources/virtual_environment_hardware_mapping_dir.md ./docs/resources/ //go:generate cp ./build/docs-gen/resources/virtual_environment_hardware_mapping_pci.md ./docs/resources/ //go:generate cp ./build/docs-gen/resources/virtual_environment_hardware_mapping_usb.md ./docs/resources/ //go:generate cp ./build/docs-gen/resources/virtual_environment_haresource.md ./docs/resources/ diff --git a/proxmox/cluster/mapping/types.go b/proxmox/cluster/mapping/types.go index 39c250cb..c3f41eb6 100644 --- a/proxmox/cluster/mapping/types.go +++ b/proxmox/cluster/mapping/types.go @@ -70,11 +70,11 @@ type GetResponseData struct { type ListResponseData struct { DataBase - // ChecksPCI might contain relevant diagnostics about incorrect [typesHWM.TypePCI] configurations. + // Checks might contain relevant diagnostics about incorrect [typesHWM.TypePCI] configurations. // The name of the node must be passed to the Proxmox VE API call which maps to the "check-node" URL parameter. // Note that the Proxmox VE API, for whatever reason, only returns one error at a time, even though the field is an // array. - ChecksPCI []NodeCheckDiag `json:"checks,omitempty"` + Checks []NodeCheckDiag `json:"checks,omitempty"` // ChecksUSB might contain relevant diagnostics about incorrect [typesHWM.TypeUSB] configurations. // The name of the node must be passed to the Proxmox VE API call which maps to the "check-node" URL parameter. diff --git a/proxmox/types/hardwaremapping/map.go b/proxmox/types/hardwaremapping/map.go index 808c3460..a43bd789 100644 --- a/proxmox/types/hardwaremapping/map.go +++ b/proxmox/types/hardwaremapping/map.go @@ -116,11 +116,14 @@ func (hm Map) String() string { return fmt.Sprintf("%s%s%s", k, string(attrValueSeparator), v) } attrs := make([]string, 0, attrCountMax) - attrs = append( - attrs, - joinKV(attrNameDeviceID, hm.ID.String()), - joinKV(attrNameNode, hm.Node), - ) + + // ID is optional for directory mappings + if hm.ID != "" { + attrs = append(attrs, joinKV(attrNameDeviceID, hm.ID.String())) + } + + // Node is common among all mappings + attrs = append(attrs, joinKV(attrNameNode, hm.Node)) if hm.Path != nil { attrs = append(attrs, joinKV(attrNamePath, *hm.Path)) diff --git a/proxmox/types/hardwaremapping/type.go b/proxmox/types/hardwaremapping/type.go index 6a7eafa1..92c00ff8 100644 --- a/proxmox/types/hardwaremapping/type.go +++ b/proxmox/types/hardwaremapping/type.go @@ -12,11 +12,15 @@ import ( //nolint:gochecknoglobals var ( + // TypeDir is an identifier for a directory mapping type. + // Do not modify this package-global variable as it acts as a safer variant compared to "iota" based constants! + TypeDir = Type{"dir"} + // TypePCI is an identifier for a PCI hardware mapping type. // Do not modify this package-global variable as it acts as a safer variant compared to "iota" based constants! TypePCI = Type{"pci"} - // TypeUSB is an identifier for a PCI hardware mapping type. + // TypeUSB is an identifier for a USB hardware mapping type. // Do not modify this package-global variable as it acts as a safer variant compared to "iota" based constants! TypeUSB = Type{"usb"} ) @@ -81,6 +85,8 @@ func (t *Type) UnmarshalJSON(b []byte) error { // An error is returned if the input string does not match any known type. func ParseType(input string) (Type, error) { switch input { + case TypeDir.String(): + return TypeDir, nil case TypePCI.String(): return TypePCI, nil case TypeUSB.String():