From 65f8ba5bfe3a8ca3872fd87830b1a858f24a7237 Mon Sep 17 00:00:00 2001 From: Konstantin Kornienko <50887992+konstantin-kornienko@users.noreply.github.com> Date: Fri, 5 Jul 2024 22:52:39 +0300 Subject: [PATCH] feat(vm): implement filtering in vms data source. (#1423) * feat(vm): implement filtering in vms data source. * Additional attributes for vm data source (status, template) * fix qodana CI job condition --------- Signed-off-by: Konstantin Kornienko Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- .github/workflows/code-quality.yml | 2 +- docs/data-sources/virtual_environment_vm.md | 2 + docs/data-sources/virtual_environment_vms.md | 33 +++++- .../data_source_virtual_environment_vms.tf | 25 +++++ proxmox/nodes/vms/vms_types.go | 8 +- proxmoxtf/datasource/vm.go | 28 +++++ proxmoxtf/datasource/vm_test.go | 2 + proxmoxtf/datasource/vms.go | 106 +++++++++++++++++- proxmoxtf/datasource/vms_test.go | 8 ++ 9 files changed, 208 insertions(+), 6 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 9dc580cc..996989b3 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -10,7 +10,7 @@ on: jobs: qodana: runs-on: ubuntu-latest - if: github.repository_owner == 'bpg' + if: github.event.pull_request.head.repo.owner.login == 'bpg' || github.event.ref == 'refs/heads/main' permissions: contents: write pull-requests: write diff --git a/docs/data-sources/virtual_environment_vm.md b/docs/data-sources/virtual_environment_vm.md index 59f7aca1..43097ea3 100644 --- a/docs/data-sources/virtual_environment_vm.md +++ b/docs/data-sources/virtual_environment_vm.md @@ -27,3 +27,5 @@ data "proxmox_virtual_environment_vm" "test_vm" { - `name` - The virtual machine name. - `tags` - A list of tags of the VM. +- `status` - Status of the VM +- `template` - Is VM a template (true) or a regular VM (false) diff --git a/docs/data-sources/virtual_environment_vms.md b/docs/data-sources/virtual_environment_vms.md index 8e45293a..794fee2c 100644 --- a/docs/data-sources/virtual_environment_vms.md +++ b/docs/data-sources/virtual_environment_vms.md @@ -15,13 +15,42 @@ Retrieves information about all VMs in the Proxmox cluster. data "proxmox_virtual_environment_vms" "ubuntu_vms" { tags = ["ubuntu"] } + +data "proxmox_virtual_environment_vms" "ubuntu_templates" { + tags = ["template", "latest"] + + filter { + name = "template" + values = [true] + } + + filter { + name = "status" + values = ["stopped"] + } + + filter { + name = "name" + regex = true + values = ["^ubuntu-20.*$"] + } + + filter { + name = "node_name" + regex = true + values = ["node_us_[1-3]", "node_eu_[1-3]"] + } +} ``` ## Argument Reference -- `node_name` - (Optional) The node name. +- `node_name` - (Optional) The node name. All cluster nodes will be queried in case this is omitted - `tags` - (Optional) A list of tags to filter the VMs. The VM must have all the tags to be included in the result. +- `filter` - (Optional) Filter blocks. The VM must satisfy all filter blocks to be included in the result. + - `name` - Name of the VM attribute to filter on. One of [`name`, `template`, `status`, `node_name`] + - `values` - List of values to pass the filter. VM's attribute should match at least one value in the list. ## Attribute Reference @@ -30,3 +59,5 @@ data "proxmox_virtual_environment_vms" "ubuntu_vms" { - `node_name` - The node name. - `tags` - A list of tags of the VM. - `vm_id` - The VM identifier. + - `status` - Status of the VM + - `template` - Is VM a template (true) or a regular VM (false) diff --git a/example/data_source_virtual_environment_vms.tf b/example/data_source_virtual_environment_vms.tf index ceff8199..2aacc101 100644 --- a/example/data_source_virtual_environment_vms.tf +++ b/example/data_source_virtual_environment_vms.tf @@ -10,6 +10,31 @@ data "proxmox_virtual_environment_vms" "example" { } } +data "proxmox_virtual_environment_vms" "template_example" { + depends_on = [proxmox_virtual_environment_vm.example] + tags = ["ubuntu"] + + filter { + name = "template" + values = [false] + } + + filter { + name = "status" + values = ["running"] + } + + filter { + name = "name" + regex = true + values = [".*ubuntu.*"] + } +} + output "proxmox_virtual_environment_vms_example" { value = data.proxmox_virtual_environment_vms.example.vms } + +output "proxmox_virtual_environment_template_vms_example" { + value = data.proxmox_virtual_environment_vms.template_example.vms +} diff --git a/proxmox/nodes/vms/vms_types.go b/proxmox/nodes/vms/vms_types.go index ba4c5034..c39509eb 100644 --- a/proxmox/nodes/vms/vms_types.go +++ b/proxmox/nodes/vms/vms_types.go @@ -354,9 +354,11 @@ type ListResponseBody struct { // ListResponseData contains the data from an virtual machine list response. type ListResponseData struct { - Name *string `json:"name,omitempty"` - Tags *string `json:"tags,omitempty"` - VMID int `json:"vmid,omitempty"` + Name *string `json:"name,omitempty"` + Tags *string `json:"tags,omitempty"` + Template *types.CustomBool `json:"template,omitempty"` + Status *string `json:"status,omitempty"` + VMID int `json:"vmid,omitempty"` } // MigrateRequestBody contains the body for a VM migration request. diff --git a/proxmoxtf/datasource/vm.go b/proxmoxtf/datasource/vm.go index 6ddef343..e6e5dd7b 100644 --- a/proxmoxtf/datasource/vm.go +++ b/proxmoxtf/datasource/vm.go @@ -24,6 +24,8 @@ const ( mkDataSourceVirtualEnvironmentVMName = "name" mkDataSourceVirtualEnvironmentVMNodeName = "node_name" mkDataSourceVirtualEnvironmentVMTags = "tags" + mkDataSourceVirtualEnvironmentVMTemplate = "template" + mkDataSourceVirtualEnvironmentVMStatus = "status" mkDataSourceVirtualEnvironmentVMVMID = "vm_id" ) @@ -47,6 +49,16 @@ func VM() *schema.Resource { Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, }, + mkDataSourceVirtualEnvironmentVMTemplate: { + Type: schema.TypeBool, + Description: "Is VM a template (true) or a regular VM (false)", + Optional: true, + }, + mkDataSourceVirtualEnvironmentVMStatus: { + Type: schema.TypeString, + Description: "Status of the VM", + Optional: true, + }, mkDataSourceVirtualEnvironmentVMVMID: { Type: schema.TypeInt, Description: "The VM identifier", @@ -82,6 +94,11 @@ func vmRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Dia return diag.FromErr(err) } + vmConfig, err := client.Node(nodeName).VM(vmID).GetVM(ctx) + if err != nil { + return diag.FromErr(err) + } + if vmStatus.Name != nil { err = d.Set(mkDataSourceVirtualEnvironmentVMName, *vmStatus.Name) } else { @@ -103,6 +120,17 @@ func vmRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Dia sort.Strings(tags) } + err = d.Set(mkDataSourceVirtualEnvironmentVMStatus, vmStatus.Status) + diags = append(diags, diag.FromErr(err)...) + + if vmConfig.Template == nil { + err = d.Set(mkDataSourceVirtualEnvironmentVMTemplate, false) + } else { + err = d.Set(mkDataSourceVirtualEnvironmentVMTemplate, *vmConfig.Template) + } + + diags = append(diags, diag.FromErr(err)...) + err = d.Set(mkDataSourceVirtualEnvironmentVMTags, tags) diags = append(diags, diag.FromErr(err)...) diff --git a/proxmoxtf/datasource/vm_test.go b/proxmoxtf/datasource/vm_test.go index 8f1cd27c..c4d8c691 100644 --- a/proxmoxtf/datasource/vm_test.go +++ b/proxmoxtf/datasource/vm_test.go @@ -40,6 +40,8 @@ func TestVMSchema(t *testing.T) { mkDataSourceVirtualEnvironmentVMName: schema.TypeString, mkDataSourceVirtualEnvironmentVMNodeName: schema.TypeString, mkDataSourceVirtualEnvironmentVMTags: schema.TypeList, + mkDataSourceVirtualEnvironmentVMTemplate: schema.TypeBool, + mkDataSourceVirtualEnvironmentVMStatus: schema.TypeString, mkDataSourceVirtualEnvironmentVMVMID: schema.TypeInt, }) } diff --git a/proxmoxtf/datasource/vms.go b/proxmoxtf/datasource/vms.go index 84744d7c..481e1f29 100644 --- a/proxmoxtf/datasource/vms.go +++ b/proxmoxtf/datasource/vms.go @@ -9,7 +9,9 @@ package datasource import ( "context" "fmt" + "regexp" "sort" + "strconv" "strings" "github.com/google/uuid" @@ -18,11 +20,16 @@ import ( "golang.org/x/exp/slices" "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/types" "github.com/bpg/terraform-provider-proxmox/proxmoxtf" ) const ( mkDataSourceVirtualEnvironmentVMs = "vms" + mkDataSourceFilter = "filter" + mkDataSourceFilterName = "name" + mkDataSourceFilterValues = "values" + mkDataSourceFilterRegex = "regex" ) // VMs returns a resource for the Proxmox VMs. @@ -32,7 +39,7 @@ func VMs() *schema.Resource { mkDataSourceVirtualEnvironmentVMNodeName: { Type: schema.TypeString, Optional: true, - Description: "The node name", + Description: "The node name. All cluster nodes will be queried in case this is omitted", }, mkDataSourceVirtualEnvironmentVMTags: { Type: schema.TypeList, @@ -40,6 +47,32 @@ func VMs() *schema.Resource { Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, + mkDataSourceFilter: { + Type: schema.TypeList, + Optional: true, + Description: "Filter blocks", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + mkDataSourceFilterName: { + Type: schema.TypeString, + Description: "Attribute to filter on. One of [name, template, status, node_name]", + Required: true, + }, + mkDataSourceFilterValues: { + Type: schema.TypeList, + Description: "List of values to pass the filter (OR logic)", + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + mkDataSourceFilterRegex: { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Treat values as regex patterns", + }, + }, + }, + }, mkDataSourceVirtualEnvironmentVMs: { Type: schema.TypeList, Description: "VMs", @@ -81,6 +114,8 @@ func vmsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Di sort.Strings(filterTags) + filters := d.Get(mkDataSourceFilter).([]interface{}) + var vms []interface{} for _, nodeName := range nodeNames { @@ -127,6 +162,23 @@ func vmsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Di } } + if data.Template != (*types.CustomBool)(nil) && *data.Template { + vm[mkDataSourceVirtualEnvironmentVMTemplate] = true + } else { + vm[mkDataSourceVirtualEnvironmentVMTemplate] = false + } + + vm[mkDataSourceVirtualEnvironmentVMStatus] = *data.Status + + if len(filters) > 0 { + allFiltersMatched, err := checkVMMatchFilters(vm, filters) + diags = append(diags, diag.FromErr(err)...) + + if !allFiltersMatched { + continue + } + } + vms = append(vms, vm) } } @@ -160,3 +212,55 @@ func getNodeNames(ctx context.Context, d *schema.ResourceData, api proxmox.Clien return nodeNames, nil } + +func checkVMMatchFilters(vm map[string]interface{}, filters []interface{}) (bool, error) { + for _, v := range filters { + filter := v.(map[string]interface{}) + filterName := filter["name"] + filterValues := filter["values"].([]interface{}) + filterRegex := filter["regex"].(bool) + + var vmValue string + + switch filterName { + case "template": + vmValue = strconv.FormatBool(vm[mkDataSourceVirtualEnvironmentVMTemplate].(bool)) + case "status": + vmValue = vm[mkDataSourceVirtualEnvironmentVMStatus].(string) + case "name": + vmValue = vm[mkDataSourceVirtualEnvironmentVMName].(string) + case "node_name": + vmValue = vm[mkDataSourceVirtualEnvironmentVMNodeName].(string) + default: + return false, fmt.Errorf( + "unknown filter name '%s', should be one of [name, template, status, node_name]", + filterName, + ) + } + + atLeastOneValueMatched := false + + for _, filterValue := range filterValues { + if filterRegex { + r, err := regexp.Compile(filterValue.(string)) + if err != nil { + return false, fmt.Errorf("error interpreting filter '%s' value '%s' as regex: %w", filterName, filterValue, err) + } + + if r.MatchString(vmValue) { + atLeastOneValueMatched = true + break + } + } else if filterValue == vmValue { + atLeastOneValueMatched = true + break + } + } + + if !atLeastOneValueMatched { + return false, nil + } + } + + return true, nil +} diff --git a/proxmoxtf/datasource/vms_test.go b/proxmoxtf/datasource/vms_test.go index 87e7d485..39e6b823 100644 --- a/proxmoxtf/datasource/vms_test.go +++ b/proxmoxtf/datasource/vms_test.go @@ -38,6 +38,7 @@ func TestVMsSchema(t *testing.T) { test.AssertValueTypes(t, s, map[string]schema.ValueType{ mkDataSourceVirtualEnvironmentVMNodeName: schema.TypeString, mkDataSourceVirtualEnvironmentVMTags: schema.TypeList, + mkDataSourceFilter: schema.TypeList, mkDataSourceVirtualEnvironmentVMs: schema.TypeList, }) @@ -54,4 +55,11 @@ func TestVMsSchema(t *testing.T) { mkDataSourceVirtualEnvironmentVMTags: schema.TypeList, mkDataSourceVirtualEnvironmentVMVMID: schema.TypeInt, }) + + filterSchema := test.AssertNestedSchemaExistence(t, s, mkDataSourceFilter) + test.AssertValueTypes(t, filterSchema, map[string]schema.ValueType{ + mkDataSourceFilterName: schema.TypeString, + mkDataSourceFilterValues: schema.TypeList, + mkDataSourceFilterRegex: schema.TypeBool, + }) }