Initial push
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
// Package migrate turns a plan into work: either commands executed on a target
|
||||
// host over SSH, or a self-contained package that can be carried to the target
|
||||
// on a disk.
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/arescom/docker-migrate/internal/spec"
|
||||
)
|
||||
|
||||
// Prepared is one container resolved against the user's selection: the spec as
|
||||
// it will exist on the target, plus the list of data locations to transfer.
|
||||
type Prepared struct {
|
||||
// Source is the container as read from the source host.
|
||||
Source *spec.Container
|
||||
// Target is the same container rewritten for the target: renamed mounts,
|
||||
// relocated binds, dropped mounts and an optional new container name.
|
||||
Target *spec.Container
|
||||
// Selection is the user's answer for this container.
|
||||
Selection spec.ItemSelection
|
||||
// Transfers are the mounts whose contents must be copied, in target terms.
|
||||
Transfers []Transfer
|
||||
// Volumes are the named volumes to create on the target.
|
||||
Volumes []spec.Volume
|
||||
// Networks are the user-defined networks to create on the target.
|
||||
Networks []spec.Network
|
||||
// Render carries the flags that shape the generated docker create command.
|
||||
Render spec.RenderOptions
|
||||
// Notes are advisories to show next to this container.
|
||||
Notes []string
|
||||
}
|
||||
|
||||
// Transfer is one data location to copy from source to target.
|
||||
type Transfer struct {
|
||||
// SourcePath is the path inside the source container to read from.
|
||||
SourcePath string
|
||||
// Destination is the path inside the target container the data belongs at.
|
||||
Destination string
|
||||
// RestoreInto is the directory the tar archive is extracted into, which is
|
||||
// the parent of Destination.
|
||||
RestoreInto string
|
||||
// Kind describes what is behind the destination on the target.
|
||||
Kind spec.MountKind
|
||||
// ReadOnly means the target container mounts this read-only, so the copy
|
||||
// has to go through a staging container.
|
||||
ReadOnly bool
|
||||
// VolumeName is the named volume behind the destination, when known.
|
||||
VolumeName string
|
||||
// BindSource is the host path behind the destination, for bind mounts.
|
||||
BindSource string
|
||||
// SizeBytes is the best-effort size, or -1.
|
||||
SizeBytes int64
|
||||
// Label is a human description used in the progress UI.
|
||||
Label string
|
||||
}
|
||||
|
||||
// ContainerName returns the name the container will have on the target.
|
||||
func (p *Prepared) ContainerName() string {
|
||||
if p.Render.NameOverride != "" {
|
||||
return p.Render.NameOverride
|
||||
}
|
||||
return p.Source.Name
|
||||
}
|
||||
|
||||
// Prepare resolves a plan item against the source inventory.
|
||||
func Prepare(
|
||||
src *spec.Container,
|
||||
sel spec.ItemSelection,
|
||||
allVolumes []spec.Volume,
|
||||
allNetworks []spec.Network,
|
||||
) (*Prepared, error) {
|
||||
if src == nil {
|
||||
return nil, fmt.Errorf("container not found in source inventory")
|
||||
}
|
||||
|
||||
p := &Prepared{Source: src, Selection: sel}
|
||||
target := *src // shallow copy; mounts are rebuilt below
|
||||
|
||||
p.Render = spec.RenderOptions{
|
||||
NameOverride: sel.NameOverride,
|
||||
KeepStaticIPs: sel.MigrateNetworks && sel.KeepStaticIPs,
|
||||
SkipNetworks: !sel.MigrateNetworks,
|
||||
SkipPorts: !sel.MigratePorts,
|
||||
DropMounts: map[string]bool{},
|
||||
}
|
||||
|
||||
volByName := map[string]spec.Volume{}
|
||||
for _, v := range allVolumes {
|
||||
volByName[v.Name] = v
|
||||
}
|
||||
netByName := map[string]spec.Network{}
|
||||
for _, n := range allNetworks {
|
||||
netByName[n.Name] = n
|
||||
}
|
||||
|
||||
var mounts []spec.Mount
|
||||
seenVolume := map[string]bool{}
|
||||
|
||||
for _, m := range src.Mounts {
|
||||
ms, ok := sel.Mounts[m.Destination]
|
||||
if !ok {
|
||||
// A mount the UI never asked about defaults to being copied, so
|
||||
// data is never silently left behind.
|
||||
ms = spec.MountSelection{Action: spec.MountActionCopy}
|
||||
if m.Kind == spec.MountTmpfs {
|
||||
ms.Action = spec.MountActionStructure
|
||||
}
|
||||
}
|
||||
if ms.Action == spec.MountActionSkip {
|
||||
p.Render.DropMounts[m.Destination] = true
|
||||
p.Notes = append(p.Notes, "mount "+m.Destination+" is not migrated")
|
||||
continue
|
||||
}
|
||||
|
||||
tm := m
|
||||
switch m.Kind {
|
||||
case spec.MountVolume:
|
||||
if ms.TargetName != "" {
|
||||
tm.Name = ms.TargetName
|
||||
}
|
||||
if v, ok := volByName[m.Name]; ok && !seenVolume[tm.Name] {
|
||||
v.Name = tm.Name
|
||||
p.Volumes = append(p.Volumes, v)
|
||||
seenVolume[tm.Name] = true
|
||||
}
|
||||
case spec.MountAnonymous:
|
||||
// Anonymous volumes are recreated as fresh anonymous volumes on
|
||||
// the target; their generated name carries no meaning and the data
|
||||
// is restored through the container path, not the volume name.
|
||||
p.Render.AnonymousVolumesAsAnonymous = true
|
||||
case spec.MountBind:
|
||||
if ms.TargetSource != "" {
|
||||
tm.Source = ms.TargetSource
|
||||
}
|
||||
}
|
||||
mounts = append(mounts, tm)
|
||||
|
||||
if ms.Action != spec.MountActionCopy || !m.HasData() {
|
||||
continue
|
||||
}
|
||||
if isRootPath(m.Destination) {
|
||||
p.Notes = append(p.Notes, "refusing to copy mount at "+m.Destination+": copying a container root is not supported")
|
||||
continue
|
||||
}
|
||||
t := Transfer{
|
||||
SourcePath: m.Destination,
|
||||
Destination: tm.Destination,
|
||||
RestoreInto: parentDir(tm.Destination),
|
||||
Kind: tm.Kind,
|
||||
ReadOnly: tm.ReadOnly,
|
||||
VolumeName: tm.Name,
|
||||
BindSource: tm.Source,
|
||||
SizeBytes: m.SizeBytes,
|
||||
}
|
||||
switch tm.Kind {
|
||||
case spec.MountVolume:
|
||||
t.Label = "volume " + tm.Name + " -> " + tm.Destination
|
||||
case spec.MountAnonymous:
|
||||
t.Label = "anonymous volume -> " + tm.Destination
|
||||
case spec.MountBind:
|
||||
t.Label = "bind " + tm.Source + " -> " + tm.Destination
|
||||
default:
|
||||
t.Label = string(tm.Kind) + " -> " + tm.Destination
|
||||
}
|
||||
p.Transfers = append(p.Transfers, t)
|
||||
}
|
||||
target.Mounts = mounts
|
||||
|
||||
if sel.MigrateNetworks {
|
||||
for _, ep := range src.Endpoints {
|
||||
if n, ok := netByName[ep.Network]; ok {
|
||||
p.Networks = append(p.Networks, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !sel.MigrateImage {
|
||||
p.Notes = append(p.Notes, "image is assumed to already exist on the target")
|
||||
}
|
||||
if sel.MigrateNetworks && sel.KeepStaticIPs {
|
||||
p.Notes = append(p.Notes, "static IP addresses are reapplied; they must fit the target subnets")
|
||||
}
|
||||
p.Target = &target
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// StagingMountPath is where a read-only destination is mounted inside the
|
||||
// temporary staging container used to seed it.
|
||||
const stagingRoot = "/__docker_migrate"
|
||||
|
||||
// StagingPaths returns the mount point and the extraction directory used when
|
||||
// seeding a read-only mount through a staging container. The volume is mounted
|
||||
// under a directory named after the destination's last segment so the archive,
|
||||
// whose entries are rooted at that same segment, lands exactly on top of it.
|
||||
func StagingPaths(destination string) (mountAt string, extractInto string) {
|
||||
return path.Join(stagingRoot, path.Base(strings.TrimSuffix(destination, "/"))), stagingRoot
|
||||
}
|
||||
|
||||
// StagingName is the throwaway container name used to seed one read-only mount.
|
||||
func StagingName(container string, index int) string {
|
||||
return fmt.Sprintf("dm-stage-%s-%d", sanitize(container), index)
|
||||
}
|
||||
|
||||
func parentDir(p string) string {
|
||||
d := path.Dir(strings.TrimSuffix(p, "/"))
|
||||
if d == "" || d == "." {
|
||||
return "/"
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func isRootPath(p string) bool {
|
||||
p = strings.TrimSuffix(p, "/")
|
||||
return p == "" || p == "/"
|
||||
}
|
||||
|
||||
func sanitize(s string) string {
|
||||
var b strings.Builder
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '_', r == '.', r == '-':
|
||||
b.WriteRune(r)
|
||||
default:
|
||||
b.WriteByte('_')
|
||||
}
|
||||
}
|
||||
out := b.String()
|
||||
if len(out) > 40 {
|
||||
out = out[:40]
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user