diff --git a/docs/data-sources/virtual_environment_containers.md b/docs/data-sources/virtual_environment_containers.md new file mode 100644 index 00000000..7978f482 --- /dev/null +++ b/docs/data-sources/virtual_environment_containers.md @@ -0,0 +1,63 @@ +--- +layout: page +title: proxmox_virtual_environment_containers +parent: Data Sources +subcategory: Virtual Environment +--- + +# Data Source: proxmox_virtual_environment_containers + +Retrieves information about all containers in the Proxmox cluster. + +## Example Usage + +```hcl +data "proxmox_virtual_environment_containers" "ubuntu_containers" { + tags = ["ubuntu"] +} + +data "proxmox_virtual_environment_containers" "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. All cluster nodes will be queried in case this is omitted +- `tags` - (Optional) A list of tags to filter the containers. The container must have all + the tags to be included in the result. +- `filter` - (Optional) Filter blocks. The container must satisfy all filter blocks to be included in the result. + - `name` - Name of the container attribute to filter on. One of [`name`, `template`, `status`, `node_name`] + - `values` - List of values to pass the filter. Container's attribute should match at least one value in the list. + +## Attribute Reference + +- `containers` - The containers list. + - `name` - The container name. + - `node_name` - The node name. + - `tags` - A list of tags of the container. + - `vm_id` - The container identifier. + - `status` - Status of the container + - `template` - Is container a template (true) or a regular container (false) \ No newline at end of file diff --git a/example/data_source_virtual_environment_containers.tf b/example/data_source_virtual_environment_containers.tf new file mode 100644 index 00000000..f7a4024d --- /dev/null +++ b/example/data_source_virtual_environment_containers.tf @@ -0,0 +1,34 @@ +data "proxmox_virtual_environment_containers" "example" { + depends_on = [proxmox_virtual_environment_container.example] + tags = ["example"] + + lifecycle { + postcondition { + condition = length(self.containers) == 1 + error_message = "Only 1 container should have this tag" + } + } +} + +data "proxmox_virtual_environment_containers" "template_example" { + depends_on = [proxmox_virtual_environment_container.example] + tags = ["example"] + + filter { + name = "template" + values = [false] + } + + filter { + name = "status" + values = ["running"] + } +} + +output "proxmox_virtual_environment_containers_example" { + value = data.proxmox_virtual_environment_containers.example.containers +} + +output "proxmox_virtual_environment_template_containers_example" { + value = data.proxmox_virtual_environment_containers.template_example.containers +} diff --git a/example/resource_virtual_environment_container.tf b/example/resource_virtual_environment_container.tf index 3ebb9d93..0e9f3e70 100644 --- a/example/resource_virtual_environment_container.tf +++ b/example/resource_virtual_environment_container.tf @@ -73,6 +73,11 @@ resource "proxmox_virtual_environment_container" "example" { vm_id = proxmox_virtual_environment_container.example_template.id } + tags = [ + "container", + ] + + initialization { hostname = "terraform-provider-proxmox-example-lxc" } diff --git a/proxmox/nodes/containers/containers.go b/proxmox/nodes/containers/containers.go index 0dbfc1a2..d274be6d 100644 --- a/proxmox/nodes/containers/containers.go +++ b/proxmox/nodes/containers/containers.go @@ -118,6 +118,22 @@ func (c *Client) GetContainerNetworkInterfaces(ctx context.Context) ([]GetNetwor return resBody.Data, nil } +// ListContainers retrieves a list of containers. +func (c *Client) ListContainers(ctx context.Context) ([]*ListResponseData, error) { + resBody := &ListResponseBody{} + + err := c.DoRequest(ctx, http.MethodGet, c.basePath(), nil, resBody) + if err != nil { + return nil, fmt.Errorf("error retrieving Containers: %w", err) + } + + if resBody.Data == nil { + return nil, api.ErrNoDataObjectInResponse + } + + return resBody.Data, nil +} + // WaitForContainerNetworkInterfaces waits for a container to publish its network interfaces. func (c *Client) WaitForContainerNetworkInterfaces( ctx context.Context, diff --git a/proxmox/nodes/containers/containers_types.go b/proxmox/nodes/containers/containers_types.go index 3663e2d1..0d7e5bcd 100644 --- a/proxmox/nodes/containers/containers_types.go +++ b/proxmox/nodes/containers/containers_types.go @@ -228,6 +228,20 @@ type GetStatusResponseData struct { VMID *types.CustomInt `json:"vmid,omitempty"` } +// ListResponseBody contains the body from a container list response. +type ListResponseBody struct { + Data []*ListResponseData `json:"data,omitempty"` +} + +// ListResponseData contains the data from an container list response. +type ListResponseData struct { + 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"` +} + // GetNetworkInterfaceResponseBody contains the body from a container get network interface response. type GetNetworkInterfaceResponseBody struct { Data []GetNetworkInterfacesData `json:"data,omitempty"` diff --git a/proxmoxtf/datasource/containers.go b/proxmoxtf/datasource/containers.go new file mode 100644 index 00000000..0a49d7ee --- /dev/null +++ b/proxmoxtf/datasource/containers.go @@ -0,0 +1,254 @@ +/* + * 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 datasource + +import ( + "context" + "errors" + "fmt" + "regexp" + "slices" + "sort" + "strconv" + "strings" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + proxmoxapi "github.com/bpg/terraform-provider-proxmox/proxmox/api" + "github.com/bpg/terraform-provider-proxmox/proxmox/types" + "github.com/bpg/terraform-provider-proxmox/proxmoxtf" +) + +const ( + mkDataSourceVirtualEnvironmentContainers = "containers" +) + +// Containers returns a resource for the Proxmox Containers. +// +//nolint:dupl // TODO: refactor to avoid duplication +func Containers() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + mkDataSourceVirtualEnvironmentContainerNodeName: { + Type: schema.TypeString, + Optional: true, + Description: "The node name. All cluster nodes will be queried in case this is omitted", + }, + mkDataSourceVirtualEnvironmentContainerTags: { + Type: schema.TypeList, + Description: "Tags of the Container to match", + 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", + }, + }, + }, + }, + mkDataSourceVirtualEnvironmentContainers: { + Type: schema.TypeList, + Description: "Containers", + Computed: true, + Elem: &schema.Resource{ + Schema: Container().Schema, + }, + }, + }, + ReadContext: containersRead, + } +} + +// containersRead reads the Containers. +func containersRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var diags diag.Diagnostics + + config := m.(proxmoxtf.ProviderConfiguration) + + api, err := config.GetClient() + if err != nil { + return diag.FromErr(err) + } + + nodeNames, err := getNodeNames(ctx, d, api) + if err != nil { + return diag.FromErr(err) + } + + var filterTags []string + + tagsData := d.Get(mkDataSourceVirtualEnvironmentContainerTags).([]interface{}) + for _, tagData := range tagsData { + tag := strings.TrimSpace(tagData.(string)) + if len(tag) > 0 { + filterTags = append(filterTags, tag) + } + } + + sort.Strings(filterTags) + + filters := d.Get(mkDataSourceFilter).([]interface{}) + + var containers []interface{} + + for _, nodeName := range nodeNames { + listData, e := api.Node(nodeName).Container(0).ListContainers(ctx) + if e != nil { + var httpError *proxmoxapi.HTTPError + if errors.As(e, &httpError) && httpError.Code == 595 { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: fmt.Sprintf("node %q is not available - Container list may be incomplete", nodeName), + }) + + continue + } + + diags = append(diags, diag.FromErr(e)...) + } + + sort.Slice(listData, func(i, j int) bool { + return listData[i].VMID < listData[j].VMID + }) + + for _, data := range listData { + container := map[string]interface{}{ + mkDataSourceVirtualEnvironmentContainerNodeName: nodeName, + mkDataSourceVirtualEnvironmentContainerVMID: data.VMID, + } + + if data.Name != nil { + container[mkDataSourceVirtualEnvironmentContainerName] = *data.Name + } else { + container[mkDataSourceVirtualEnvironmentContainerName] = "" + } + + var tags []string + if data.Tags != nil && *data.Tags != "" { + tags = strings.Split(*data.Tags, ";") + sort.Strings(tags) + container[mkDataSourceVirtualEnvironmentContainerTags] = tags + } + + if len(filterTags) > 0 { + match := true + + for _, tag := range filterTags { + if !slices.Contains(tags, tag) { + match = false + break + } + } + + if !match { + continue + } + } + + if data.Template != (*types.CustomBool)(nil) && *data.Template { + container[mkDataSourceVirtualEnvironmentContainerTemplate] = true + } else { + container[mkDataSourceVirtualEnvironmentContainerTemplate] = false + } + + container[mkDataSourceVirtualEnvironmentContainerStatus] = *data.Status + + if len(filters) > 0 { + allFiltersMatched, err := checkContainerMatchFilters(container, filters) + diags = append(diags, diag.FromErr(err)...) + + if !allFiltersMatched { + continue + } + } + + containers = append(containers, container) + } + } + + err = d.Set(mkDataSourceVirtualEnvironmentContainers, containers) + diags = append(diags, diag.FromErr(err)...) + + d.SetId(uuid.New().String()) + + return diags +} + +//nolint:dupl // TODO: refactor to avoid duplication +func checkContainerMatchFilters(container 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 containerValue string + + switch filterName { + case "template": + containerValue = strconv.FormatBool(container[mkDataSourceVirtualEnvironmentContainerTemplate].(bool)) + case "status": + containerValue = container[mkDataSourceVirtualEnvironmentContainerStatus].(string) + case "name": + containerValue = container[mkDataSourceVirtualEnvironmentContainerName].(string) + case "node_name": + containerValue = container[mkDataSourceVirtualEnvironmentContainerNodeName].(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(containerValue) { + atLeastOneValueMatched = true + break + } + } else if filterValue == containerValue { + atLeastOneValueMatched = true + break + } + } + + if !atLeastOneValueMatched { + return false, nil + } + } + + return true, nil +} diff --git a/proxmoxtf/datasource/containers_test.go b/proxmoxtf/datasource/containers_test.go new file mode 100644 index 00000000..344a7dac --- /dev/null +++ b/proxmoxtf/datasource/containers_test.go @@ -0,0 +1,65 @@ +/* + * 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 datasource + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/bpg/terraform-provider-proxmox/proxmoxtf/test" +) + +// TestContainersInstantiation tests whether the dataSourceVirtualEnvironmentContainers instance can be instantiated. +func TestContainersInstantiation(t *testing.T) { + t.Parallel() + + s := Containers() + + if s == nil { + t.Fatalf("Cannot instantiate dataSourceVirtualEnvironmentContainers") + } +} + +// TestContainersSchema tests the dataSourceVirtualEnvironmentContainers schema. +func TestContainersSchema(t *testing.T) { + t.Parallel() + + s := Containers().Schema + + test.AssertComputedAttributes(t, s, []string{ + mkDataSourceVirtualEnvironmentContainers, + }) + + test.AssertValueTypes(t, s, map[string]schema.ValueType{ + mkDataSourceVirtualEnvironmentContainerNodeName: schema.TypeString, + mkDataSourceVirtualEnvironmentContainerTags: schema.TypeList, + mkDataSourceFilter: schema.TypeList, + mkDataSourceVirtualEnvironmentContainers: schema.TypeList, + }) + + containersSchema := test.AssertNestedSchemaExistence(t, s, mkDataSourceVirtualEnvironmentContainers) + + test.AssertComputedAttributes(t, containersSchema, []string{ + mkDataSourceVirtualEnvironmentContainerName, + mkDataSourceVirtualEnvironmentContainerTags, + }) + + test.AssertValueTypes(t, containersSchema, map[string]schema.ValueType{ + mkDataSourceVirtualEnvironmentContainerName: schema.TypeString, + mkDataSourceVirtualEnvironmentContainerNodeName: schema.TypeString, + mkDataSourceVirtualEnvironmentContainerTags: schema.TypeList, + mkDataSourceVirtualEnvironmentContainerVMID: 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, + }) +} diff --git a/proxmoxtf/datasource/vms.go b/proxmoxtf/datasource/vms.go index 72384356..1f9b025c 100644 --- a/proxmoxtf/datasource/vms.go +++ b/proxmoxtf/datasource/vms.go @@ -35,6 +35,8 @@ const ( ) // VMs returns a resource for the Proxmox VMs. +// +//nolint:dupl // TODO: refactor to avoid duplication func VMs() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -225,6 +227,7 @@ func getNodeNames(ctx context.Context, d *schema.ResourceData, api proxmox.Clien return nodeNames, nil } +//nolint:dupl // TODO: refactor to avoid duplication func checkVMMatchFilters(vm map[string]interface{}, filters []interface{}) (bool, error) { for _, v := range filters { filter := v.(map[string]interface{}) diff --git a/proxmoxtf/provider/datasources.go b/proxmoxtf/provider/datasources.go index b5796e96..8ddcd806 100644 --- a/proxmoxtf/provider/datasources.go +++ b/proxmoxtf/provider/datasources.go @@ -14,21 +14,22 @@ import ( func createDatasourceMap() map[string]*schema.Resource { return map[string]*schema.Resource{ - "proxmox_virtual_environment_dns": datasource.DNS(), - "proxmox_virtual_environment_group": datasource.Group(), - "proxmox_virtual_environment_groups": datasource.Groups(), - "proxmox_virtual_environment_hosts": datasource.Hosts(), - "proxmox_virtual_environment_node": datasource.Node(), - "proxmox_virtual_environment_nodes": datasource.Nodes(), - "proxmox_virtual_environment_pool": datasource.Pool(), - "proxmox_virtual_environment_pools": datasource.Pools(), - "proxmox_virtual_environment_role": datasource.Role(), - "proxmox_virtual_environment_roles": datasource.Roles(), - "proxmox_virtual_environment_time": datasource.Time(), - "proxmox_virtual_environment_user": datasource.User(), - "proxmox_virtual_environment_users": datasource.Users(), - "proxmox_virtual_environment_vm": datasource.VM(), - "proxmox_virtual_environment_vms": datasource.VMs(), - "proxmox_virtual_environment_container": datasource.Container(), + "proxmox_virtual_environment_dns": datasource.DNS(), + "proxmox_virtual_environment_group": datasource.Group(), + "proxmox_virtual_environment_groups": datasource.Groups(), + "proxmox_virtual_environment_hosts": datasource.Hosts(), + "proxmox_virtual_environment_node": datasource.Node(), + "proxmox_virtual_environment_nodes": datasource.Nodes(), + "proxmox_virtual_environment_pool": datasource.Pool(), + "proxmox_virtual_environment_pools": datasource.Pools(), + "proxmox_virtual_environment_role": datasource.Role(), + "proxmox_virtual_environment_roles": datasource.Roles(), + "proxmox_virtual_environment_time": datasource.Time(), + "proxmox_virtual_environment_user": datasource.User(), + "proxmox_virtual_environment_users": datasource.Users(), + "proxmox_virtual_environment_vm": datasource.VM(), + "proxmox_virtual_environment_vms": datasource.VMs(), + "proxmox_virtual_environment_container": datasource.Container(), + "proxmox_virtual_environment_containers": datasource.Containers(), } }