Files
kawaandClaude Sonnet 5 34e18987a0 Rename product to dockmv (container, image, module, env vars)
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>
2026-08-11 13:56:26 +02:00

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{}