0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-07-01 02:52:58 +00:00
terraform-provider-proxmox/proxmox/nodes/containers/containers.go
Pavel Boldyrev 03f2079902
fix(lxc): prevent spurious dns config change when updating initialization block (#1859)
* fix(lxc): prevent spurious `dns` config change when updating `initialization` block

Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
2025-03-28 22:39:57 -04:00

287 lines
7.3 KiB
Go

/*
* 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 containers
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/avast/retry-go/v4"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
)
// CloneContainer clones a container.
func (c *Client) CloneContainer(ctx context.Context, d *CloneRequestBody) error {
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("/clone"), d, nil)
if err != nil {
return fmt.Errorf("error cloning container: %w", err)
}
return nil
}
// CreateContainer creates a container.
func (c *Client) CreateContainer(ctx context.Context, d *CreateRequestBody) error {
taskID, err := c.CreateContainerAsync(ctx, d)
if err != nil {
return err
}
err = c.Tasks().WaitForTask(ctx, *taskID)
if err != nil {
return fmt.Errorf("error waiting for container created: %w", err)
}
return nil
}
// CreateContainerAsync creates a container asynchronously.
func (c *Client) CreateContainerAsync(ctx context.Context, d *CreateRequestBody) (*string, error) {
resBody := &CreateResponseBody{}
err := c.DoRequest(ctx, http.MethodPost, c.basePath(), d, resBody)
if err != nil {
return nil, fmt.Errorf("error creating container: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// DeleteContainer deletes a container.
func (c *Client) DeleteContainer(ctx context.Context) error {
err := c.DoRequest(ctx, http.MethodDelete, c.ExpandPath(""), nil, nil)
if err != nil {
return fmt.Errorf("error deleting container: %w", err)
}
return nil
}
// GetContainer retrieves a container.
func (c *Client) GetContainer(ctx context.Context) (*GetResponseData, error) {
resBody := &GetResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath("config"), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving container: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// GetContainerStatus retrieves the status for a container.
func (c *Client) GetContainerStatus(ctx context.Context) (*GetStatusResponseData, error) {
resBody := &GetStatusResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath("status/current"), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error retrieving container status: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, 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)
if err != nil {
return fmt.Errorf("error rebooting container: %w", err)
}
return nil
}
// ShutdownContainer shuts down a container.
func (c *Client) ShutdownContainer(ctx context.Context, d *ShutdownRequestBody) error {
taskID, err := c.ShutdownContainerAsync(ctx, d)
if err != nil {
return err
}
err = c.Tasks().WaitForTask(ctx, *taskID)
if err != nil {
return fmt.Errorf("error waiting for container shut down: %w", err)
}
return nil
}
// ShutdownContainerAsync shuts down a container asynchronously.
func (c *Client) ShutdownContainerAsync(ctx context.Context, d *ShutdownRequestBody) (*string, error) {
resBody := &ShutdownResponseBody{}
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("status/shutdown"), d, resBody)
if err != nil {
return nil, fmt.Errorf("error shutting down container: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// StartContainer starts a container if is not already running.
func (c *Client) StartContainer(ctx context.Context) error {
status, err := c.GetContainerStatus(ctx)
if err != nil {
return fmt.Errorf("error retrieving container status: %w", err)
}
if status.Status == "running" {
return nil
}
taskID, err := c.StartContainerAsync(ctx)
if err != nil {
return fmt.Errorf("error starting container: %w", err)
}
err = c.Tasks().WaitForTask(ctx, *taskID)
if err != nil {
return fmt.Errorf("error waiting for container start: %w", err)
}
// the timeout here should probably be configurable
err = c.WaitForContainerStatus(ctx, "running")
if err != nil {
return fmt.Errorf("error waiting for container start: %w", err)
}
return nil
}
// StartContainerAsync starts a container asynchronously.
func (c *Client) StartContainerAsync(ctx context.Context) (*string, error) {
resBody := &StartResponseBody{}
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("status/start"), nil, resBody)
if err != nil {
return nil, fmt.Errorf("error starting container: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// StopContainer stops a container immediately.
func (c *Client) StopContainer(ctx context.Context) error {
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("status/stop"), nil, nil)
if err != nil {
return fmt.Errorf("error stopping container: %w", err)
}
return nil
}
// UpdateContainer updates a container.
func (c *Client) UpdateContainer(ctx context.Context, d *UpdateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPut, c.ExpandPath("config"), d, nil)
if err != nil {
return fmt.Errorf("error updating container: %w", err)
}
return nil
}
// WaitForContainerStatus waits for a container to reach a specific state.
func (c *Client) WaitForContainerStatus(ctx context.Context, status string) error {
status = strings.ToLower(status)
unexpectedStatus := fmt.Errorf("unexpected status %q", status)
err := retry.Do(
func() error {
data, err := c.GetContainerStatus(ctx)
if err != nil {
return err
}
if data.Status != status {
return unexpectedStatus
}
return nil
},
retry.Context(ctx),
retry.RetryIf(func(err error) bool {
return errors.Is(err, unexpectedStatus)
}),
retry.UntilSucceeded(),
retry.Delay(1*time.Second),
retry.LastErrorOnly(true),
)
if errors.Is(err, context.DeadlineExceeded) {
return fmt.Errorf("timeout while waiting for container %d to enter the status %q", c.VMID, status)
}
if err != nil {
return fmt.Errorf("error waiting for container %d to enter the status %q: %w", c.VMID, status, err)
}
return nil
}
// WaitForContainerConfigUnlock waits for a container lock to be released.
func (c *Client) WaitForContainerConfigUnlock(ctx context.Context, ignoreErrorResponse bool) error {
stillLocked := errors.New("still locked")
err := retry.Do(
func() error {
data, err := c.GetContainerStatus(ctx)
if err != nil {
return err
}
if data.Lock != nil && *data.Lock != "" {
return stillLocked
}
return nil
},
retry.Context(ctx),
retry.RetryIf(func(err error) bool {
return errors.Is(err, stillLocked) || ignoreErrorResponse
}),
retry.UntilSucceeded(),
retry.Delay(1*time.Second),
retry.LastErrorOnly(true),
)
if errors.Is(err, context.DeadlineExceeded) {
return fmt.Errorf("timeout while waiting for container %d configuration to become unlocked", c.VMID)
}
if err != nil && !ignoreErrorResponse {
return fmt.Errorf("error waiting for container %d configuration to become unlocked: %w", c.VMID, err)
}
return nil
}