0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-07-12 00:35:03 +00:00

feat(lxc): retrieve container IP addresses (#2030)

Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
This commit is contained in:
Pavel Boldyrev 2025-07-11 11:05:46 -04:00 committed by GitHub
parent a2c40c7c79
commit 20572d95e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 166 additions and 14 deletions

View File

@ -236,22 +236,19 @@ output "ubuntu_container_public_key" {
- `timeout_clone` - (Optional) Timeout for cloning a container in seconds (defaults to 1800).
- `timeout_delete` - (Optional) Timeout for deleting a container in seconds (defaults to 60).
- `timeout_update` - (Optional) Timeout for updating a container in seconds (defaults to 1800).
- `unprivileged` - (Optional) Whether the container runs as unprivileged on
the host (defaults to `false`).
- `unprivileged` - (Optional) Whether the container runs as unprivileged on the host (defaults to `false`).
- `vm_id` - (Optional) The container identifier
- `features` - (Optional) The container feature flags. Changing flags (except nesting) is only allowed for `root@pam` authenticated user.
- `nesting` - (Optional) Whether the container is nested (defaults
to `false`)
- `fuse` - (Optional) Whether the container supports FUSE mounts (defaults
to `false`)
- `keyctl` - (Optional) Whether the container supports `keyctl()` system
call (defaults to `false`)
- `nesting` - (Optional) Whether the container is nested (defaults to `false`)
- `fuse` - (Optional) Whether the container supports FUSE mounts (defaults to `false`)
- `keyctl` - (Optional) Whether the container supports `keyctl()` system call (defaults to `false`)
- `mount` - (Optional) List of allowed mount types (`cifs` or `nfs`)
- `hook_script_file_id` - (Optional) The identifier for a file containing a hook script (needs to be executable, e.g. by using the `proxmox_virtual_environment_file.file_mode` attribute).
## Attribute Reference
There are no additional attributes available for this resource.
- `ipv4` - The map of IPv4 addresses per network devices. Returns the first address for each network device, if multiple addresses are assigned.
- `ipv6` - The map of IPv6 addresses per network device. Returns the first address for each network device, if multiple addresses are assigned.
## Import

View File

@ -128,7 +128,7 @@ resource "proxmox_virtual_environment_file" "ubuntu_container_template" {
node_name = "first-node"
source_file {
path = "https://download.proxmox.com/images/system/ubuntu-20.04-standard_20.04-1_amd64.tar.gz"
path = "http://download.proxmox.com/images/system/ubuntu-20.04-standard_20.04-1_amd64.tar.gz"
}
}
```

View File

@ -110,6 +110,9 @@ func TestAccResourceContainer(t *testing.T) {
"device_passthrough.0.mode": "0660",
"initialization.0.dns.#": "0",
}),
ResourceAttributesSet(accTestContainerName, []string{
"ipv4.vmbr0",
}),
func(*terraform.State) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

View File

@ -102,6 +102,78 @@ func (c *Client) GetContainerStatus(ctx context.Context) (*GetStatusResponseData
return resBody.Data, nil
}
// GetContainerNetworkInterfaces retrieves details about the container network interfaces.
func (c *Client) GetContainerNetworkInterfaces(ctx context.Context) ([]GetNetworkInterfacesData, error) {
resBody := &GetNetworkInterfaceResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath("interfaces"), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving container network interfaces: %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,
timeout time.Duration,
) ([]GetNetworkInterfacesData, error) {
errNoIPsYet := errors.New("no ips yet")
ctxWithTimeout, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
ifaces, err := retry.DoWithData(
func() ([]GetNetworkInterfacesData, error) {
ifaces, err := c.GetContainerNetworkInterfaces(ctx)
if err != nil {
return nil, err
}
for _, iface := range ifaces {
if iface.Name != "lo" && iface.IPAddresses != nil && len(*iface.IPAddresses) > 0 {
// we have at least one non-loopback interface with an IP address
return ifaces, nil
}
}
return nil, errNoIPsYet
},
retry.Context(ctxWithTimeout),
retry.RetryIf(func(err error) bool {
var target *api.HTTPError
if errors.As(err, &target) {
if target.Code == http.StatusBadRequest {
// this is a special case to account for eventual consistency
// when creating a task -- the task may not be available via status API
// immediately after creation
return true
}
}
return errors.Is(err, api.ErrNoDataObjectInResponse) || errors.Is(err, errNoIPsYet)
}),
retry.LastErrorOnly(true),
retry.UntilSucceeded(),
retry.DelayType(retry.FixedDelay),
retry.Delay(time.Second),
)
if errors.Is(err, context.DeadlineExceeded) {
return nil, errors.New("timeout while waiting for container IP addresses")
}
if err != nil {
return nil, fmt.Errorf("error while waiting for container IP addresses: %w", err)
}
return ifaces, nil
}
// RebootContainer reboots a container.
func (c *Client) RebootContainer(ctx context.Context, d *RebootRequestBody) error {
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("status/reboot"), d, nil)

View File

@ -228,6 +228,22 @@ type GetStatusResponseData struct {
VMID *types.CustomInt `json:"vmid,omitempty"`
}
// GetNetworkInterfaceResponseBody contains the body from a container get network interface response.
type GetNetworkInterfaceResponseBody struct {
Data []GetNetworkInterfacesData `json:"data,omitempty"`
}
// GetNetworkInterfacesData contains the data from a container get network interfaces response.
type GetNetworkInterfacesData struct {
MACAddress string `json:"hardware-address"`
Name string `json:"name"`
IPAddresses *[]struct {
Address string `json:"ip-address"`
Prefix types.CustomInt `json:"prefix"`
Type string `json:"ip-address-type"`
} `json:"ip-addresses,omitempty"`
}
// StartResponseBody contains the body from a container start response.
type StartResponseBody struct {
Data *string `json:"data,omitempty"`

View File

@ -16,6 +16,7 @@ import (
"strings"
"time"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@ -180,6 +181,9 @@ const (
mkTimeoutDelete = "timeout_delete"
mkUnprivileged = "unprivileged"
mkVMID = "vm_id"
mkIPv4 = "ipv4"
mkIPv6 = "ipv6"
)
// Container returns a resource that manages a container.
@ -565,6 +569,27 @@ func Container() *schema.Resource {
MaxItems: 1,
MinItems: 0,
},
mkIPv4: {
Type: schema.TypeMap,
Description: "The container's IPv4 addresses per network device",
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
// this does not work with map datatype in SDK :(
// Elem: &schema.Schema{
// Type: schema.TypeList,
// Elem: &schema.Schema{Type: schema.TypeString},
// },
},
mkIPv6: {
Type: schema.TypeMap,
Description: "The container's IPv6 addresses per network device",
Computed: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
mkMemory: {
Type: schema.TypeList,
Description: "The memory allocation",
@ -2094,6 +2119,7 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d
return diag.FromErr(e)
}
template := d.Get(mkTemplate).(bool)
nodeName := d.Get(mkNodeName).(string)
vmID, e := strconv.Atoi(d.Id())
@ -2733,9 +2759,7 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d
diags = append(diags, diag.FromErr(e)...)
}
currentTemplate := d.Get(mkTemplate).(bool)
if len(clone) == 0 || currentTemplate {
if len(clone) == 0 || template {
if containerConfig.Template != nil {
e = d.Set(
mkTemplate,
@ -2754,7 +2778,47 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d
return diag.FromErr(e)
}
e = d.Set(mkStarted, status.Status == "running")
started := status.Status == "running"
if started && len(networkInterfaces) > 0 {
ifaces, err := containerAPI.WaitForContainerNetworkInterfaces(ctx, 10*time.Second)
if err == nil {
ipv4Map := make(map[string]interface{})
ipv6Map := make(map[string]interface{})
for _, iface := range ifaces {
if iface.IPAddresses != nil && iface.Name != "lo" {
for _, ip := range *iface.IPAddresses {
switch ip.Type {
case "inet":
// store only the first IPv4 address per interface
if _, exists := ipv4Map[iface.Name]; !exists {
ipv4Map[iface.Name] = ip.Address
}
case "inet6":
// store only the first IPV6 address per interface
if _, exists := ipv6Map[iface.Name]; !exists {
ipv6Map[iface.Name] = ip.Address
}
default:
return diag.FromErr(fmt.Errorf("unexpected IP address type %q for interface %q", ip.Type, iface.Name))
}
}
}
}
e = d.Set(mkIPv4, ipv4Map)
diags = append(diags, diag.FromErr(e)...)
e = d.Set(mkIPv6, ipv6Map)
diags = append(diags, diag.FromErr(e)...)
} else {
tflog.Warn(ctx, "error waiting for container network interfaces", map[string]interface{}{
"error": err.Error(),
})
}
}
e = d.Set(mkStarted, started)
diags = append(diags, diag.FromErr(e)...)
return diags