Container/image/service name, Go module path, CLI binary name, and DOCKER_MIGRATE_* env vars still used the old working name; the project is branded DockMV everywhere else (README, logo, Gitea repo). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
161 lines
4.9 KiB
Go
161 lines
4.9 KiB
Go
package dkr
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/arescom/dockmv/internal/spec"
|
|
dockertypes "github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/api/types/image"
|
|
)
|
|
|
|
// CopyOut streams the contents of a path inside a container as an uncompressed
|
|
// tar archive.
|
|
//
|
|
// This is the single mechanism used for every kind of data location. It works
|
|
// for named volumes, anonymous volumes and bind mounts alike, because the
|
|
// daemon resolves the mount and produces the tar itself: no helper image is
|
|
// needed, the container's own image needs no tar binary, and the container does
|
|
// not have to be running.
|
|
//
|
|
// The archive entries are rooted at the last path segment, matching `docker cp`
|
|
// semantics. Restoring therefore targets the parent directory; use RestorePath.
|
|
func (c *Client) CopyOut(ctx context.Context, containerID, srcPath string) (io.ReadCloser, error) {
|
|
rc, _, err := c.api.CopyFromContainer(ctx, containerID, srcPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read %s from %s: %w", srcPath, short(containerID), err)
|
|
}
|
|
return rc, nil
|
|
}
|
|
|
|
// CopyIn writes an uncompressed tar archive into a path inside a container.
|
|
func (c *Client) CopyIn(ctx context.Context, containerID, dstPath string, r io.Reader) error {
|
|
err := c.api.CopyToContainer(ctx, containerID, dstPath, r, container.CopyToContainerOptions{
|
|
AllowOverwriteDirWithFile: false,
|
|
CopyUIDGID: true,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("write %s into %s: %w", dstPath, short(containerID), err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RestorePath is the directory an archive produced by CopyOut must be extracted
|
|
// into so that the contents land back at the original destination.
|
|
func RestorePath(destination string) string {
|
|
d := path.Dir(strings.TrimSuffix(destination, "/"))
|
|
if d == "" || d == "." {
|
|
return "/"
|
|
}
|
|
return d
|
|
}
|
|
|
|
// SaveImage streams `docker save` output for one or more image references.
|
|
func (c *Client) SaveImage(ctx context.Context, refs ...string) (io.ReadCloser, error) {
|
|
rc, err := c.api.ImageSave(ctx, refs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("save image %s: %w", strings.Join(refs, ","), err)
|
|
}
|
|
return rc, nil
|
|
}
|
|
|
|
// ImageSizeBytes returns the on-disk size of an image, used to estimate how
|
|
// long a streamed transfer will take.
|
|
func (c *Client) ImageSizeBytes(ctx context.Context, ref string) int64 {
|
|
insp, err := c.api.ImageInspect(ctx, ref)
|
|
if err != nil {
|
|
return -1
|
|
}
|
|
return insp.Size
|
|
}
|
|
|
|
// State returns the current status of a container, e.g. "running".
|
|
func (c *Client) State(ctx context.Context, id string) (string, error) {
|
|
j, err := c.api.ContainerInspect(ctx, id)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if j.State == nil {
|
|
return "", errors.New("no state in inspect payload")
|
|
}
|
|
return j.State.Status, nil
|
|
}
|
|
|
|
// Stop stops a container and waits for it to settle. A container that is
|
|
// already stopped is left alone.
|
|
func (c *Client) Stop(ctx context.Context, id string, timeout time.Duration) error {
|
|
st, err := c.State(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if st != "running" && st != "restarting" && st != "paused" {
|
|
return nil
|
|
}
|
|
secs := int(timeout.Seconds())
|
|
if err := c.api.ContainerStop(ctx, id, container.StopOptions{Timeout: &secs}); err != nil {
|
|
return fmt.Errorf("stop %s: %w", short(id), err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Start starts a container.
|
|
func (c *Client) Start(ctx context.Context, id string) error {
|
|
if err := c.api.ContainerStart(ctx, id, container.StartOptions{}); err != nil {
|
|
return fmt.Errorf("start %s: %w", short(id), err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// VolumeSizes measures every local volume in one daemon round trip. It can be
|
|
// slow on hosts with a lot of data, so the UI asks for it explicitly rather
|
|
// than including it in the inventory.
|
|
func (c *Client) VolumeSizes(ctx context.Context) (map[string]int64, error) {
|
|
du, err := c.api.DiskUsage(ctx, dockertypes.DiskUsageOptions{
|
|
Types: []dockertypes.DiskUsageObject{dockertypes.VolumeObject},
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("compute disk usage: %w", err)
|
|
}
|
|
out := map[string]int64{}
|
|
for _, v := range du.Volumes {
|
|
if v == nil || v.UsageData == nil {
|
|
continue
|
|
}
|
|
out[v.Name] = v.UsageData.Size
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// MeasureMounts fills in the SizeBytes of every mount it can determine.
|
|
// Volume sizes come from the daemon; bind mount sizes are measured by walking
|
|
// the path from inside the container, which works even when the daemon is
|
|
// remote.
|
|
func (c *Client) MeasureMounts(ctx context.Context, containers []spec.Container) error {
|
|
sizes, err := c.VolumeSizes(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range containers {
|
|
for j := range containers[i].Mounts {
|
|
m := &containers[i].Mounts[j]
|
|
switch m.Kind {
|
|
case spec.MountVolume, spec.MountAnonymous:
|
|
if s, ok := sizes[m.Name]; ok {
|
|
m.SizeBytes = s
|
|
}
|
|
case spec.MountTmpfs:
|
|
m.SizeBytes = 0
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var _ = image.InspectResponse{}
|