mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-08-22 11:28:33 +00:00
feat(lxc): add proxmox_virtual_environment_containers
data source (#2090)
* feat(lxc): add `proxmox_virtual_environment_containers` data source - Implements a new data source for fetching container details in Proxmox environments. - Includes support for filtering by attributes such as name, template, status, and tags. - Provides documentation and test coverage for the new functionality. Signed-off-by: k69420s <k69420s@localhost.localdomain> * suppress duplication error in linter, fix example code Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --------- Signed-off-by: k69420s <k69420s@localhost.localdomain> Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
This commit is contained in:
parent
420add8669
commit
45f28051cd
63
docs/data-sources/virtual_environment_containers.md
Normal file
63
docs/data-sources/virtual_environment_containers.md
Normal file
@ -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)
|
34
example/data_source_virtual_environment_containers.tf
Normal file
34
example/data_source_virtual_environment_containers.tf
Normal file
@ -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
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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"`
|
||||
|
254
proxmoxtf/datasource/containers.go
Normal file
254
proxmoxtf/datasource/containers.go
Normal file
@ -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
|
||||
}
|
65
proxmoxtf/datasource/containers_test.go
Normal file
65
proxmoxtf/datasource/containers_test.go
Normal file
@ -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,
|
||||
})
|
||||
}
|
@ -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{})
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user