diff --git a/docs/resources/virtual_environment_container.md b/docs/resources/virtual_environment_container.md index f2e07811..70c7c879 100644 --- a/docs/resources/virtual_environment_container.md +++ b/docs/resources/virtual_environment_container.md @@ -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 diff --git a/docs/resources/virtual_environment_file.md b/docs/resources/virtual_environment_file.md index 4c3dfaf6..5bd8f13c 100644 --- a/docs/resources/virtual_environment_file.md +++ b/docs/resources/virtual_environment_file.md @@ -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" } } ``` diff --git a/fwprovider/test/resource_container_test.go b/fwprovider/test/resource_container_test.go index 9cc2c4af..208f5422 100644 --- a/fwprovider/test/resource_container_test.go +++ b/fwprovider/test/resource_container_test.go @@ -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() diff --git a/proxmox/nodes/containers/containers.go b/proxmox/nodes/containers/containers.go index 06e57b0e..0dbfc1a2 100644 --- a/proxmox/nodes/containers/containers.go +++ b/proxmox/nodes/containers/containers.go @@ -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) diff --git a/proxmox/nodes/containers/containers_types.go b/proxmox/nodes/containers/containers_types.go index e4b37710..d798cdf9 100644 --- a/proxmox/nodes/containers/containers_types.go +++ b/proxmox/nodes/containers/containers_types.go @@ -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"` diff --git a/proxmoxtf/resource/container/container.go b/proxmoxtf/resource/container/container.go index 0ebcc5a6..2ebc6282 100644 --- a/proxmoxtf/resource/container/container.go +++ b/proxmoxtf/resource/container/container.go @@ -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