Initial push
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
// Package dkr wraps the Docker Engine API with the operations the migration
|
||||
// tool needs: reading a full container inventory, streaming data out of a
|
||||
// container's mounts, and streaming image layers.
|
||||
package dkr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
// Client is a connection to one Docker daemon.
|
||||
type Client struct {
|
||||
api *client.Client
|
||||
// Endpoint is the daemon address, shown in the UI.
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
// New connects to the daemon described by the standard DOCKER_* environment
|
||||
// variables, or to host when it is non-empty (e.g. unix:///var/run/docker.sock
|
||||
// or tcp://10.0.0.5:2375).
|
||||
func New(host string) (*Client, error) {
|
||||
opts := []client.Opt{client.FromEnv, client.WithAPIVersionNegotiation()}
|
||||
if host != "" {
|
||||
opts = append(opts, client.WithHost(host))
|
||||
}
|
||||
api, err := client.NewClientWithOpts(opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create docker client: %w", err)
|
||||
}
|
||||
return &Client{api: api, Endpoint: api.DaemonHost()}, nil
|
||||
}
|
||||
|
||||
// API exposes the underlying SDK client for callers that need an operation
|
||||
// this package does not wrap.
|
||||
func (c *Client) API() *client.Client { return c.api }
|
||||
|
||||
// Close releases the daemon connection.
|
||||
func (c *Client) Close() error { return c.api.Close() }
|
||||
|
||||
// Info returns daemon information, and doubles as a connectivity check.
|
||||
func (c *Client) Info(ctx context.Context) (system.Info, error) {
|
||||
return c.api.Info(ctx)
|
||||
}
|
||||
|
||||
// Ping verifies the daemon is reachable and returns its version string.
|
||||
func (c *Client) Ping(ctx context.Context) (string, error) {
|
||||
v, err := c.api.ServerVersion(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return v.Version, nil
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package dkr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/arescom/docker-migrate/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{}
|
||||
@@ -0,0 +1,570 @@
|
||||
package dkr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/arescom/docker-migrate/internal/spec"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
imagetypes "github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
networktypes "github.com/docker/docker/api/types/network"
|
||||
volumetypes "github.com/docker/docker/api/types/volume"
|
||||
)
|
||||
|
||||
// Inventory is everything the UI needs to render the source host.
|
||||
type Inventory struct {
|
||||
Host string `json:"host"`
|
||||
DockerVersion string `json:"dockerVersion"`
|
||||
Containers []spec.Container `json:"containers"`
|
||||
Volumes []spec.Volume `json:"volumes"`
|
||||
Networks []spec.Network `json:"networks"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
}
|
||||
|
||||
// anonymousVolume matches the 64-hex names Docker generates for volumes that
|
||||
// were never explicitly named.
|
||||
var anonymousVolume = regexp.MustCompile(`^[0-9a-f]{64}$`)
|
||||
|
||||
// Inventory reads every container on the daemon, plus the volumes and networks
|
||||
// they reference, and normalizes them into the transport spec.
|
||||
func (c *Client) Inventory(ctx context.Context) (*Inventory, error) {
|
||||
version, err := c.Ping(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect to docker: %w", err)
|
||||
}
|
||||
info, err := c.api.Info(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read docker info: %w", err)
|
||||
}
|
||||
|
||||
summaries, err := c.api.ContainerList(ctx, container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list containers: %w", err)
|
||||
}
|
||||
|
||||
inv := &Inventory{Host: info.Name, DockerVersion: version}
|
||||
imgCache := map[string]*imagetypes.InspectResponse{}
|
||||
volNames := map[string]bool{}
|
||||
netNames := map[string]bool{}
|
||||
|
||||
for _, s := range summaries {
|
||||
cs, err := c.inspectContainer(ctx, s.ID, imgCache)
|
||||
if err != nil {
|
||||
inv.Warnings = append(inv.Warnings, fmt.Sprintf("skipped container %s: %v", short(s.ID), err))
|
||||
continue
|
||||
}
|
||||
for _, m := range cs.Mounts {
|
||||
if m.Kind == spec.MountVolume && m.Name != "" {
|
||||
volNames[m.Name] = true
|
||||
}
|
||||
}
|
||||
for _, e := range cs.Endpoints {
|
||||
netNames[e.Network] = true
|
||||
}
|
||||
inv.Containers = append(inv.Containers, *cs)
|
||||
}
|
||||
|
||||
sort.Slice(inv.Containers, func(i, j int) bool {
|
||||
a, b := inv.Containers[i], inv.Containers[j]
|
||||
if a.ComposeProject != b.ComposeProject {
|
||||
return a.ComposeProject < b.ComposeProject
|
||||
}
|
||||
return a.Name < b.Name
|
||||
})
|
||||
|
||||
for name := range volNames {
|
||||
v, err := c.api.VolumeInspect(ctx, name)
|
||||
if err != nil {
|
||||
inv.Warnings = append(inv.Warnings, fmt.Sprintf("volume %s: %v", name, err))
|
||||
continue
|
||||
}
|
||||
inv.Volumes = append(inv.Volumes, convertVolume(v))
|
||||
}
|
||||
sort.Slice(inv.Volumes, func(i, j int) bool { return inv.Volumes[i].Name < inv.Volumes[j].Name })
|
||||
|
||||
for name := range netNames {
|
||||
if isBuiltinNetwork(name) {
|
||||
continue
|
||||
}
|
||||
n, err := c.api.NetworkInspect(ctx, name, networktypes.InspectOptions{})
|
||||
if err != nil {
|
||||
inv.Warnings = append(inv.Warnings, fmt.Sprintf("network %s: %v", name, err))
|
||||
continue
|
||||
}
|
||||
inv.Networks = append(inv.Networks, convertNetwork(n))
|
||||
}
|
||||
sort.Slice(inv.Networks, func(i, j int) bool { return inv.Networks[i].Name < inv.Networks[j].Name })
|
||||
|
||||
return inv, nil
|
||||
}
|
||||
|
||||
// InspectContainer normalizes one container by id or name.
|
||||
func (c *Client) InspectContainer(ctx context.Context, id string) (*spec.Container, error) {
|
||||
return c.inspectContainer(ctx, id, map[string]*imagetypes.InspectResponse{})
|
||||
}
|
||||
|
||||
func (c *Client) inspectContainer(ctx context.Context, id string, imgCache map[string]*imagetypes.InspectResponse) (*spec.Container, error) {
|
||||
j, err := c.api.ContainerInspect(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if j.Config == nil || j.HostConfig == nil {
|
||||
return nil, fmt.Errorf("incomplete inspect payload")
|
||||
}
|
||||
|
||||
out := &spec.Container{
|
||||
ID: j.ID,
|
||||
Name: strings.TrimPrefix(j.Name, "/"),
|
||||
Image: j.Config.Image,
|
||||
ImageID: j.Image,
|
||||
}
|
||||
if out.Image == "" {
|
||||
out.Image = j.Image
|
||||
}
|
||||
if j.State != nil {
|
||||
out.State = j.State.Status
|
||||
}
|
||||
|
||||
// Image config is used to strip everything the image already provides, so
|
||||
// the recreated container carries only genuine run-time overrides.
|
||||
img := c.imageConfig(ctx, j.Image, imgCache)
|
||||
if img == nil {
|
||||
out.Warnings = append(out.Warnings,
|
||||
"image config unavailable; env, command and labels are reproduced in full")
|
||||
} else if len(img.RepoDigests) > 0 {
|
||||
out.ImageDigest = img.RepoDigests[0]
|
||||
}
|
||||
|
||||
cfg := j.Config
|
||||
out.Hostname = dropGeneratedHostname(cfg.Hostname, j.ID)
|
||||
out.Domainname = cfg.Domainname
|
||||
out.User = cfg.User
|
||||
out.WorkingDir = cfg.WorkingDir
|
||||
out.Tty = cfg.Tty
|
||||
out.OpenStdin = cfg.OpenStdin
|
||||
out.StopSignal = cfg.StopSignal
|
||||
out.StopTimeout = cfg.StopTimeout
|
||||
|
||||
var imgEnv, imgCmd, imgEntry []string
|
||||
var imgLabels map[string]string
|
||||
if img != nil && img.Config != nil {
|
||||
imgEnv, imgLabels = img.Config.Env, img.Config.Labels
|
||||
imgCmd, imgEntry = img.Config.Cmd, img.Config.Entrypoint
|
||||
if img.Config.User == cfg.User {
|
||||
out.User = ""
|
||||
}
|
||||
if img.Config.WorkingDir == cfg.WorkingDir {
|
||||
out.WorkingDir = ""
|
||||
}
|
||||
}
|
||||
out.Env = subtractStrings(cfg.Env, imgEnv)
|
||||
out.Labels = subtractLabels(cfg.Labels, imgLabels)
|
||||
out.Cmd = cfg.Cmd
|
||||
out.CmdSet = !equalStrings(cfg.Cmd, imgCmd)
|
||||
out.Entrypoint = cfg.Entrypoint
|
||||
out.EntrypointSet = !equalStrings(cfg.Entrypoint, imgEntry)
|
||||
|
||||
if p := cfg.Labels["com.docker.compose.project"]; p != "" {
|
||||
out.ComposeProject = p
|
||||
out.ComposeService = cfg.Labels["com.docker.compose.service"]
|
||||
}
|
||||
|
||||
if cfg.Healthcheck != nil {
|
||||
var imgHC *container.HealthConfig
|
||||
if img != nil && img.Config != nil {
|
||||
imgHC = img.Config.Healthcheck
|
||||
}
|
||||
if !sameHealthcheck(cfg.Healthcheck, imgHC) {
|
||||
out.Healthcheck = &spec.Healthcheck{
|
||||
Test: cfg.Healthcheck.Test,
|
||||
Interval: int64(cfg.Healthcheck.Interval),
|
||||
Timeout: int64(cfg.Healthcheck.Timeout),
|
||||
StartPeriod: int64(cfg.Healthcheck.StartPeriod),
|
||||
Retries: cfg.Healthcheck.Retries,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hc := j.HostConfig
|
||||
out.RestartPolicy = string(hc.RestartPolicy.Name)
|
||||
out.RestartMaxRetries = hc.RestartPolicy.MaximumRetryCount
|
||||
out.AutoRemove = hc.AutoRemove
|
||||
out.Privileged = hc.Privileged
|
||||
out.ReadonlyRootfs = hc.ReadonlyRootfs
|
||||
out.CapAdd = hc.CapAdd
|
||||
out.CapDrop = hc.CapDrop
|
||||
out.SecurityOpt = dropDefaultSecurityOpt(hc.SecurityOpt)
|
||||
out.GroupAdd = hc.GroupAdd
|
||||
out.Sysctls = hc.Sysctls
|
||||
out.Runtime = hc.Runtime
|
||||
out.PidMode = string(hc.PidMode)
|
||||
out.IpcMode = string(hc.IpcMode)
|
||||
out.UtsMode = string(hc.UTSMode)
|
||||
out.UsernsMode = string(hc.UsernsMode)
|
||||
out.CgroupnsMode = string(hc.CgroupnsMode)
|
||||
out.DNS = hc.DNS
|
||||
out.DNSSearch = hc.DNSSearch
|
||||
out.DNSOptions = hc.DNSOptions
|
||||
out.ExtraHosts = hc.ExtraHosts
|
||||
out.NetworkMode = string(hc.NetworkMode)
|
||||
out.PublishAll = hc.PublishAllPorts
|
||||
out.Init = hc.Init
|
||||
out.LogDriver = hc.LogConfig.Type
|
||||
out.LogOptions = hc.LogConfig.Config
|
||||
|
||||
for _, d := range hc.Devices {
|
||||
out.Devices = append(out.Devices, spec.Device{
|
||||
PathOnHost: d.PathOnHost,
|
||||
PathInContainer: d.PathInContainer,
|
||||
CgroupPermissions: d.CgroupPermissions,
|
||||
})
|
||||
}
|
||||
for _, u := range hc.Ulimits {
|
||||
if u == nil {
|
||||
continue
|
||||
}
|
||||
out.Ulimits = append(out.Ulimits, spec.Ulimit{Name: u.Name, Soft: u.Soft, Hard: u.Hard})
|
||||
}
|
||||
|
||||
out.Resources = spec.Resources{
|
||||
Memory: hc.Memory,
|
||||
MemoryReservation: hc.MemoryReservation,
|
||||
MemorySwap: hc.MemorySwap,
|
||||
MemorySwappiness: hc.MemorySwappiness,
|
||||
NanoCPUs: hc.NanoCPUs,
|
||||
CPUShares: hc.CPUShares,
|
||||
CPUPeriod: hc.CPUPeriod,
|
||||
CPUQuota: hc.CPUQuota,
|
||||
CpusetCpus: hc.CpusetCpus,
|
||||
CpusetMems: hc.CpusetMems,
|
||||
PidsLimit: hc.PidsLimit,
|
||||
OomKillDisable: hc.OomKillDisable,
|
||||
OomScoreAdj: hc.OomScoreAdj,
|
||||
ShmSize: hc.ShmSize,
|
||||
}
|
||||
|
||||
for portProto, bindings := range hc.PortBindings {
|
||||
for _, b := range bindings {
|
||||
out.Ports = append(out.Ports, spec.PortBinding{
|
||||
ContainerPort: string(portProto),
|
||||
HostIP: b.HostIP,
|
||||
HostPort: b.HostPort,
|
||||
})
|
||||
}
|
||||
}
|
||||
sort.Slice(out.Ports, func(i, j int) bool {
|
||||
if out.Ports[i].ContainerPort != out.Ports[j].ContainerPort {
|
||||
return out.Ports[i].ContainerPort < out.Ports[j].ContainerPort
|
||||
}
|
||||
return out.Ports[i].HostPort < out.Ports[j].HostPort
|
||||
})
|
||||
for p := range cfg.ExposedPorts {
|
||||
out.ExposedPorts = append(out.ExposedPorts, string(p))
|
||||
}
|
||||
sort.Strings(out.ExposedPorts)
|
||||
|
||||
out.Mounts = convertMounts(j.Mounts, hc.Tmpfs)
|
||||
out.Endpoints = convertEndpoints(j.NetworkSettings, out.NetworkMode, j.ID)
|
||||
|
||||
if hc.AutoRemove {
|
||||
out.Warnings = append(out.Warnings,
|
||||
"source runs with --rm; the migrated container is created without it so it survives inspection")
|
||||
}
|
||||
if strings.HasPrefix(out.NetworkMode, "container:") {
|
||||
out.Warnings = append(out.Warnings,
|
||||
"shares another container's network namespace; migrate that container too")
|
||||
}
|
||||
for _, m := range out.Mounts {
|
||||
if m.Kind == spec.MountBind && isSensitiveBind(m.Source) {
|
||||
out.Warnings = append(out.Warnings,
|
||||
"binds host path "+m.Source+"; copying it is usually wrong, review before migrating")
|
||||
}
|
||||
}
|
||||
if len(hc.VolumesFrom) > 0 {
|
||||
out.Warnings = append(out.Warnings,
|
||||
"uses --volumes-from ("+strings.Join(hc.VolumesFrom, ", ")+"), which is not reproduced")
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *Client) imageConfig(ctx context.Context, id string, cache map[string]*imagetypes.InspectResponse) *imagetypes.InspectResponse {
|
||||
if v, ok := cache[id]; ok {
|
||||
return v
|
||||
}
|
||||
insp, err := c.api.ImageInspect(ctx, id)
|
||||
if err != nil {
|
||||
cache[id] = nil
|
||||
return nil
|
||||
}
|
||||
cache[id] = &insp
|
||||
return &insp
|
||||
}
|
||||
|
||||
func convertMounts(mounts []container.MountPoint, tmpfs map[string]string) []spec.Mount {
|
||||
out := make([]spec.Mount, 0, len(mounts)+len(tmpfs))
|
||||
for _, m := range mounts {
|
||||
sm := spec.Mount{
|
||||
Destination: m.Destination,
|
||||
ReadOnly: !m.RW,
|
||||
Propagation: string(m.Propagation),
|
||||
SizeBytes: -1,
|
||||
}
|
||||
switch m.Type {
|
||||
case "volume":
|
||||
sm.Name = m.Name
|
||||
if anonymousVolume.MatchString(m.Name) {
|
||||
sm.Kind = spec.MountAnonymous
|
||||
} else {
|
||||
sm.Kind = spec.MountVolume
|
||||
}
|
||||
case "bind":
|
||||
sm.Kind = spec.MountBind
|
||||
sm.Source = m.Source
|
||||
case "tmpfs":
|
||||
sm.Kind = spec.MountTmpfs
|
||||
default:
|
||||
// npipe and unknown driver types carry no portable data.
|
||||
sm.Kind = spec.MountKind(m.Type)
|
||||
sm.Source = m.Source
|
||||
}
|
||||
out = append(out, sm)
|
||||
}
|
||||
for dest, opts := range tmpfs {
|
||||
if hasDestination(out, dest) {
|
||||
continue
|
||||
}
|
||||
out = append(out, spec.Mount{Kind: spec.MountTmpfs, Destination: dest, TmpfsOpts: opts, SizeBytes: 0})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Destination < out[j].Destination })
|
||||
return out
|
||||
}
|
||||
|
||||
func convertEndpoints(ns *container.NetworkSettings, networkMode, containerID string) []spec.Endpoint {
|
||||
if ns == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]spec.Endpoint, 0, len(ns.Networks))
|
||||
for name, ep := range ns.Networks {
|
||||
if ep == nil {
|
||||
continue
|
||||
}
|
||||
e := spec.Endpoint{
|
||||
Network: name,
|
||||
Aliases: dropGeneratedAliases(ep.Aliases, containerID),
|
||||
Links: ep.Links,
|
||||
DriverOpts: ep.DriverOpts,
|
||||
}
|
||||
// The MAC address is normally derived by the daemon. Carrying it over
|
||||
// only makes sense alongside the static addressing it belongs to;
|
||||
// otherwise it risks colliding with an address on the target network.
|
||||
if ep.IPAMConfig != nil && (ep.IPAMConfig.IPv4Address != "" || ep.IPAMConfig.IPv6Address != "") {
|
||||
e.MacAddress = ep.MacAddress
|
||||
}
|
||||
if ep.IPAMConfig != nil {
|
||||
e.IPv4Address = ep.IPAMConfig.IPv4Address
|
||||
e.IPv6Address = ep.IPAMConfig.IPv6Address
|
||||
}
|
||||
out = append(out, e)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
// The network named by NetworkMode has to come first: it is the one
|
||||
// `docker create --network` can express.
|
||||
if out[i].Network == networkMode {
|
||||
return true
|
||||
}
|
||||
if out[j].Network == networkMode {
|
||||
return false
|
||||
}
|
||||
return out[i].Network < out[j].Network
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func convertVolume(v volumetypes.Volume) spec.Volume {
|
||||
return spec.Volume{
|
||||
Name: v.Name,
|
||||
Driver: v.Driver,
|
||||
DriverOpts: v.Options,
|
||||
Labels: v.Labels,
|
||||
}
|
||||
}
|
||||
|
||||
func convertNetwork(n network.Inspect) spec.Network {
|
||||
out := spec.Network{
|
||||
Name: n.Name,
|
||||
Driver: n.Driver,
|
||||
Scope: n.Scope,
|
||||
EnableIPv6: n.EnableIPv6,
|
||||
Internal: n.Internal,
|
||||
Attachable: n.Attachable,
|
||||
Ingress: n.Ingress,
|
||||
IPAMDriver: n.IPAM.Driver,
|
||||
Options: n.Options,
|
||||
Labels: n.Labels,
|
||||
}
|
||||
for _, p := range n.IPAM.Config {
|
||||
out.IPAMPools = append(out.IPAMPools, spec.IPAMPool{
|
||||
Subnet: p.Subnet,
|
||||
IPRange: p.IPRange,
|
||||
Gateway: p.Gateway,
|
||||
AuxAddress: p.AuxAddress,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ImageInspectExists reports whether the daemon holds the given image.
|
||||
func (c *Client) ImageExists(ctx context.Context, ref string) bool {
|
||||
_, err := c.api.ImageInspect(ctx, ref)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// ImageRepoDigests returns the registry digests of an image, used to decide
|
||||
// whether the target can simply pull it.
|
||||
func (c *Client) ImageRepoDigests(ctx context.Context, ref string) []string {
|
||||
insp, err := c.api.ImageInspect(ctx, ref)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return insp.RepoDigests
|
||||
}
|
||||
|
||||
var _ = image.InspectResponse{}
|
||||
|
||||
func isBuiltinNetwork(name string) bool {
|
||||
switch name {
|
||||
case "bridge", "host", "none":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isSensitiveBind flags host paths that almost never should be copied wholesale
|
||||
// to another machine.
|
||||
func isSensitiveBind(p string) bool {
|
||||
p = strings.TrimSuffix(strings.ReplaceAll(p, `\`, "/"), "/")
|
||||
switch p {
|
||||
case "/var/run/docker.sock", "/run/docker.sock", "/proc", "/sys", "/dev", "/", "/etc", "/var/run", "/run":
|
||||
return true
|
||||
}
|
||||
return strings.HasPrefix(p, "/sys/") || strings.HasPrefix(p, "/proc/") || strings.HasPrefix(p, "/dev/")
|
||||
}
|
||||
|
||||
// dropGeneratedHostname removes the hostname Docker derives from the container
|
||||
// id, which must not be pinned on the target.
|
||||
func dropGeneratedHostname(hostname, id string) string {
|
||||
if hostname == "" || strings.HasPrefix(id, hostname) {
|
||||
return ""
|
||||
}
|
||||
return hostname
|
||||
}
|
||||
|
||||
// dropGeneratedAliases removes the short-container-id alias Docker attaches to
|
||||
// every endpoint by itself. Re-applying it would pin the target container to
|
||||
// the source container's id.
|
||||
func dropGeneratedAliases(aliases []string, containerID string) []string {
|
||||
out := make([]string, 0, len(aliases))
|
||||
for _, a := range aliases {
|
||||
if len(a) == 12 && strings.HasPrefix(containerID, a) {
|
||||
continue
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
sort.Strings(out)
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// dropDefaultSecurityOpt removes the label=disable style entries Docker reports
|
||||
// on hosts without SELinux, which would fail to apply elsewhere.
|
||||
func dropDefaultSecurityOpt(opts []string) []string {
|
||||
out := make([]string, 0, len(opts))
|
||||
for _, o := range opts {
|
||||
if strings.HasPrefix(o, "name=") {
|
||||
continue
|
||||
}
|
||||
out = append(out, o)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func hasDestination(ms []spec.Mount, dest string) bool {
|
||||
for _, m := range ms {
|
||||
if m.Destination == dest {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func subtractStrings(all, base []string) []string {
|
||||
if len(base) == 0 {
|
||||
return all
|
||||
}
|
||||
seen := make(map[string]bool, len(base))
|
||||
for _, b := range base {
|
||||
seen[b] = true
|
||||
}
|
||||
out := make([]string, 0, len(all))
|
||||
for _, v := range all {
|
||||
if !seen[v] {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func subtractLabels(all, base map[string]string) map[string]string {
|
||||
out := map[string]string{}
|
||||
for k, v := range all {
|
||||
if bv, ok := base[k]; ok && bv == v {
|
||||
continue
|
||||
}
|
||||
out[k] = v
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func equalStrings(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func sameHealthcheck(a, b *container.HealthConfig) bool {
|
||||
if a == nil || b == nil {
|
||||
return a == b
|
||||
}
|
||||
return equalStrings(a.Test, b.Test) && a.Interval == b.Interval &&
|
||||
a.Timeout == b.Timeout && a.StartPeriod == b.StartPeriod && a.Retries == b.Retries
|
||||
}
|
||||
|
||||
func short(id string) string {
|
||||
if len(id) > 12 {
|
||||
return id[:12]
|
||||
}
|
||||
return id
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package dkr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/arescom/docker-migrate/internal/spec"
|
||||
)
|
||||
|
||||
// TestLiveInventory is a smoke test against whatever daemon the environment
|
||||
// points at. It is skipped unless DOCKER_MIGRATE_LIVE_TEST is set, because it
|
||||
// needs a real Docker host.
|
||||
func TestLiveInventory(t *testing.T) {
|
||||
if os.Getenv("DOCKER_MIGRATE_LIVE_TEST") == "" {
|
||||
t.Skip("set DOCKER_MIGRATE_LIVE_TEST=1 to run against the local daemon")
|
||||
}
|
||||
c, err := New("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
inv, err := c.Inventory(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("host=%s docker=%s containers=%d volumes=%d networks=%d warnings=%v",
|
||||
inv.Host, inv.DockerVersion, len(inv.Containers), len(inv.Volumes), len(inv.Networks), inv.Warnings)
|
||||
|
||||
for i := range inv.Containers {
|
||||
ct := &inv.Containers[i]
|
||||
args := ct.CreateArgs(spec.RenderOptions{})
|
||||
t.Logf("%s [%s] -> docker %s", ct.Name, ct.State, spec.ShellQuoteAll(args))
|
||||
for _, m := range ct.DataMounts() {
|
||||
t.Logf(" mount %-8s %-40s restore-into %s", m.Kind, m.Destination, RestorePath(m.Destination))
|
||||
}
|
||||
for _, w := range ct.Warnings {
|
||||
t.Logf(" warn: %s", w)
|
||||
}
|
||||
}
|
||||
b, _ := json.MarshalIndent(inv, "", " ")
|
||||
t.Logf("inventory bytes: %d", len(b))
|
||||
}
|
||||
Reference in New Issue
Block a user