581 lines
19 KiB
Go
581 lines
19 KiB
Go
package migrate
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/arescom/docker-migrate/internal/spec"
|
|
)
|
|
|
|
// renderInstaller generates the shell script shipped inside a migration
|
|
// package. The script is self-contained: it never parses the manifest and
|
|
// depends on nothing but bash, gzip and the docker CLI, so it can be read and
|
|
// audited by whoever runs it on the target host.
|
|
func renderInstaller(prepared []*Prepared, man *spec.Manifest, savedImages map[string]string, pkgName string) string {
|
|
payload := map[string]spec.Payload{}
|
|
for _, p := range man.Payloads {
|
|
if p.Kind == "mount" {
|
|
payload[p.Container+"\x00"+p.Destination] = p
|
|
}
|
|
}
|
|
imagePayload := map[string]spec.Payload{}
|
|
for _, p := range man.Payloads {
|
|
if p.Kind == "image" {
|
|
imagePayload[p.Image] = p
|
|
}
|
|
}
|
|
|
|
var b strings.Builder
|
|
// w formats a line. Literal blocks must be passed as an argument, never as
|
|
// the format itself: shell text is full of % and would be mangled.
|
|
w := func(format string, args ...any) {
|
|
if len(args) == 0 {
|
|
b.WriteString(format)
|
|
b.WriteByte('\n')
|
|
return
|
|
}
|
|
fmt.Fprintf(&b, format+"\n", args...)
|
|
}
|
|
|
|
w("#!/usr/bin/env bash")
|
|
w("#")
|
|
w("# Migration package: %s", pkgName)
|
|
w("# Created: %s", man.CreatedAt.Format("2006-01-02 15:04:05 MST"))
|
|
w("# Source host: %s (docker %s)", orDash(man.SourceHost), orDash(man.DockerVersion))
|
|
w("# Containers: %d", len(prepared))
|
|
w("#")
|
|
w("# Run this on the TARGET host. It needs bash, gzip and a working docker CLI.")
|
|
w("# Nothing is written outside docker's own storage and the bind mount paths")
|
|
w("# listed below.")
|
|
w("#")
|
|
w("# Usage: ./install.sh [options]")
|
|
w("# --dry-run print every command without changing anything")
|
|
w("# --yes do not ask for confirmation")
|
|
w("# --no-start create the containers but leave them stopped")
|
|
w("# --conflict MODE fail (default) | skip | replace | rename")
|
|
w("# --rename-suffix S suffix used by --conflict rename (default -migrated)")
|
|
w("# --skip-verify do not checksum the payloads")
|
|
w("# --only NAME[,NAME...] restore only these containers")
|
|
w("# --docker CMD docker command to use (default: docker)")
|
|
w("# --sudo prefix docker with sudo -n")
|
|
w("")
|
|
w("set -euo pipefail")
|
|
w("")
|
|
w(`PKGDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"`)
|
|
w("DRY_RUN=0")
|
|
w("ASSUME_YES=0")
|
|
w("NO_START=0")
|
|
w("VERIFY=1")
|
|
w("ONLY=\"\"")
|
|
w("DOCKER_BIN=docker")
|
|
w("USE_SUDO=0")
|
|
w("CONFLICT=%s", spec.ShellQuote(string(defaultConflict(man.Options.Conflict))))
|
|
w("RENAME_SUFFIX=%s", spec.ShellQuote(defaultSuffix(man.Options.RenameSuffix)))
|
|
w("")
|
|
w(`while [ $# -gt 0 ]; do`)
|
|
w(` case "$1" in`)
|
|
w(` --dry-run) DRY_RUN=1 ;;`)
|
|
w(` --yes|-y) ASSUME_YES=1 ;;`)
|
|
w(` --no-start) NO_START=1 ;;`)
|
|
w(` --skip-verify) VERIFY=0 ;;`)
|
|
w(` --conflict) shift; CONFLICT="${1:-}" ;;`)
|
|
w(` --rename-suffix) shift; RENAME_SUFFIX="${1:-}" ;;`)
|
|
w(` --only) shift; ONLY="${1:-}" ;;`)
|
|
w(` --docker) shift; DOCKER_BIN="${1:-docker}" ;;`)
|
|
w(` --sudo) USE_SUDO=1 ;;`)
|
|
w(` -h|--help) sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;`)
|
|
w(` *) echo "unknown option: $1" >&2; exit 2 ;;`)
|
|
w(` esac`)
|
|
w(` shift`)
|
|
w(`done`)
|
|
w("")
|
|
w(`case "$CONFLICT" in fail|skip|replace|rename) ;; *) echo "invalid --conflict: $CONFLICT" >&2; exit 2 ;; esac`)
|
|
w("")
|
|
w(`if [ "$USE_SUDO" = 1 ]; then DOCKER="sudo -n $DOCKER_BIN"; else DOCKER="$DOCKER_BIN"; fi`)
|
|
w("")
|
|
w(installerHelpers)
|
|
w("")
|
|
|
|
// Preflight.
|
|
w(`log "migration package: %s"`, escapeDoubleQuoted(pkgName))
|
|
w(`preflight`)
|
|
w("")
|
|
|
|
// Bind mount summary, so the operator sees what will touch the host
|
|
// filesystem before answering the prompt.
|
|
binds := collectBinds(prepared)
|
|
if len(binds) > 0 {
|
|
w(`echo "This package writes into the following host paths:"`)
|
|
for _, p := range binds {
|
|
w(`echo " %s"`, escapeDoubleQuoted(p))
|
|
}
|
|
w("")
|
|
}
|
|
w(`confirm`)
|
|
w("")
|
|
|
|
// Networks, created once.
|
|
nets := map[string]bool{}
|
|
var netBlock strings.Builder
|
|
for _, p := range prepared {
|
|
for _, n := range p.Networks {
|
|
if nets[n.Name] || isBuiltin(n.Name) {
|
|
continue
|
|
}
|
|
nets[n.Name] = true
|
|
fmt.Fprintf(&netBlock, "ensure_network %s %s\n",
|
|
spec.ShellQuote(n.Name), spec.ShellQuoteAll(n.CreateArgs()))
|
|
}
|
|
}
|
|
if netBlock.Len() > 0 {
|
|
w(`step "networks"`)
|
|
w("%s", strings.TrimRight(netBlock.String(), "\n"))
|
|
w("")
|
|
}
|
|
|
|
// One function per container keeps the flow readable and lets --only skip
|
|
// whole containers cleanly.
|
|
for i, p := range prepared {
|
|
w("%s", renderContainerFunc(i, p, payload, imagePayload, savedImages))
|
|
}
|
|
|
|
w(`FAILED=0`)
|
|
for i, p := range prepared {
|
|
name := p.ContainerName()
|
|
w(`if selected %s; then`, spec.ShellQuote(name))
|
|
w(` if ! migrate_%d; then err "container %s failed"; FAILED=$((FAILED+1)); fi`, i, escapeDoubleQuoted(name))
|
|
w(`else`)
|
|
w(` log "skipping %s (not in --only)"`, escapeDoubleQuoted(name))
|
|
w(`fi`)
|
|
}
|
|
w("")
|
|
w(`if [ "$FAILED" -gt 0 ]; then`)
|
|
w(` err "$FAILED container(s) failed"`)
|
|
w(` exit 1`)
|
|
w(`fi`)
|
|
w(`ok "migration complete"`)
|
|
w(`if [ "$DRY_RUN" = 1 ]; then log "this was a dry run; nothing was changed"; fi`)
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func renderContainerFunc(idx int, p *Prepared, payload, imagePayload map[string]spec.Payload, savedImages map[string]string) string {
|
|
var b strings.Builder
|
|
w := func(format string, args ...any) {
|
|
if len(args) == 0 {
|
|
b.WriteString(format)
|
|
b.WriteByte('\n')
|
|
return
|
|
}
|
|
fmt.Fprintf(&b, format+"\n", args...)
|
|
}
|
|
|
|
name := p.ContainerName()
|
|
sel := p.Selection
|
|
|
|
// Every command below is checked explicitly with "|| return 1". This
|
|
// function is invoked from an `if !` test, which switches `set -e` off for
|
|
// its whole body, so an unchecked failure would otherwise be swallowed and
|
|
// the container reported as migrated when it was not.
|
|
w("migrate_%d() {", idx)
|
|
w(` local base=%s`, spec.ShellQuote(name))
|
|
w(` CNAME="$base"`)
|
|
w(` step "container $base"`)
|
|
for _, note := range p.Notes {
|
|
w(` warn %s`, spec.ShellQuote(note))
|
|
}
|
|
for _, note := range p.Source.Warnings {
|
|
w(` warn %s`, spec.ShellQuote(note))
|
|
}
|
|
|
|
// Conflict handling.
|
|
w(` if object_exists container "$CNAME"; then`)
|
|
w(` case "$CONFLICT" in`)
|
|
w(` skip) warn "container $CNAME already exists; skipping"; return 0 ;;`)
|
|
w(` replace) warn "removing existing container $CNAME"; run $DOCKER rm -f "$CNAME" || return 1 ;;`)
|
|
w(` rename) CNAME="$(free_name "$base")"; warn "creating $CNAME instead" ;;`)
|
|
w(` *) err "container $CNAME already exists; rerun with --conflict replace|rename|skip"; return 1 ;;`)
|
|
w(` esac`)
|
|
w(` fi`)
|
|
|
|
// Image.
|
|
image := p.Target.Image
|
|
switch {
|
|
case !sel.MigrateImage || sel.ImageMode == spec.ImageSkip:
|
|
w(` if ! object_exists image %s; then`, spec.ShellQuote(image))
|
|
w(` err "image %s is not present and this package does not carry it"; return 1`, escapeDoubleQuoted(image))
|
|
w(` fi`)
|
|
case sel.ImageMode == spec.ImagePull:
|
|
w(` ensure_image_pull %s || return 1`, spec.ShellQuote(image))
|
|
default:
|
|
if rel, ok := savedImages[image]; ok {
|
|
ip := imagePayload[image]
|
|
w(` ensure_image_load %s %s %s %s || return 1`,
|
|
spec.ShellQuote(image), spec.ShellQuote(rel),
|
|
spec.ShellQuote(ip.SHA256), boolArg(ip.Compressed))
|
|
} else {
|
|
w(` ensure_image_pull %s || return 1`, spec.ShellQuote(image))
|
|
}
|
|
}
|
|
|
|
// Named volumes.
|
|
for _, v := range p.Volumes {
|
|
w(` ensure_volume %s %s || return 1`, spec.ShellQuote(v.Name), spec.ShellQuoteAll(v.CreateArgs()))
|
|
}
|
|
|
|
// Bind mount directories, created before the container so docker does not
|
|
// invent them with unexpected ownership halfway through.
|
|
for _, m := range p.Target.Mounts {
|
|
if m.Kind == spec.MountBind && !p.Render.DropMounts[m.Destination] && !isSpecialBind(m.Source) {
|
|
w(` ensure_dir %s || return 1`, spec.ShellQuote(m.Source))
|
|
}
|
|
}
|
|
|
|
// Create. The name is substituted at run time so --conflict rename works.
|
|
createArgs := p.Target.CreateArgs(p.Render)
|
|
rest := createArgs
|
|
if len(rest) >= 3 && rest[0] == "create" && rest[1] == "--name" {
|
|
rest = rest[3:]
|
|
}
|
|
w(` log "creating container $CNAME"`)
|
|
w(` run $DOCKER create --name "$CNAME" %s || { err "could not create $CNAME"; return 1; }`, spec.ShellQuoteAll(rest))
|
|
|
|
for _, args := range p.Target.NetworkConnectArgs(p.Render) {
|
|
// The rendered args end with (network, containerName); the name is
|
|
// replaced so a renamed container still gets attached.
|
|
if len(args) < 2 {
|
|
continue
|
|
}
|
|
head := args[:len(args)-1]
|
|
w(` run $DOCKER %s "$CNAME" || { err "could not attach $CNAME to a network"; return 1; }`, spec.ShellQuoteAll(head))
|
|
}
|
|
|
|
// Data.
|
|
for _, t := range p.Transfers {
|
|
pl, ok := payload[name+"\x00"+t.Destination]
|
|
if !ok {
|
|
w(` warn "no data archive for %s in this package; leaving it empty"`, escapeDoubleQuoted(t.Destination))
|
|
continue
|
|
}
|
|
w(` verify_payload %s %s || return 1`, spec.ShellQuote(pl.Path), spec.ShellQuote(pl.SHA256))
|
|
if t.ReadOnly {
|
|
w(` log "restoring %s (read-only mount, via staging container)"`, escapeDoubleQuoted(t.Label))
|
|
w(` seed_readonly "$CNAME" %s %s %s %s || return 1`,
|
|
spec.ShellQuote(p.Target.Image), spec.ShellQuote(t.Destination),
|
|
spec.ShellQuote(pl.Path), boolArg(pl.Compressed))
|
|
} else {
|
|
w(` log "restoring %s"`, escapeDoubleQuoted(t.Label))
|
|
w(` feed_archive %s %s "$CNAME" %s || { err "could not restore %s"; return 1; }`,
|
|
spec.ShellQuote(pl.Path), boolArg(pl.Compressed), spec.ShellQuote(t.RestoreInto),
|
|
escapeDoubleQuoted(t.Label))
|
|
}
|
|
}
|
|
|
|
if sel.StartAfter {
|
|
w(` if [ "$NO_START" = 1 ]; then`)
|
|
w(` log "leaving $CNAME stopped (--no-start)"`)
|
|
w(` else`)
|
|
w(` log "starting $CNAME"`)
|
|
w(` run $DOCKER start "$CNAME" || { err "could not start $CNAME"; return 1; }`)
|
|
w(` check_running "$CNAME" || return 1`)
|
|
w(` fi`)
|
|
} else {
|
|
w(` log "$CNAME created but not started (it was not running on the source)"`)
|
|
}
|
|
w(` ok "$CNAME done"`)
|
|
w(` return 0`)
|
|
w("}")
|
|
w("")
|
|
return b.String()
|
|
}
|
|
|
|
// installerHelpers is the fixed shell prelude shared by every generated
|
|
// installer.
|
|
const installerHelpers = `
|
|
if [ -t 1 ]; then C_R=$'\033[31m'; C_G=$'\033[32m'; C_Y=$'\033[33m'; C_B=$'\033[1m'; C_0=$'\033[0m'
|
|
else C_R=""; C_G=""; C_Y=""; C_B=""; C_0=""; fi
|
|
|
|
log() { printf '%s\n' " $*"; }
|
|
step() { printf '\n%s\n' "${C_B}==> $*${C_0}"; }
|
|
ok() { printf '%s\n' " ${C_G}ok${C_0} $*"; }
|
|
warn() { printf '%s\n' " ${C_Y}warning${C_0} $*" >&2; }
|
|
err() { printf '%s\n' " ${C_R}error${C_0} $*" >&2; }
|
|
die() { err "$*"; exit 1; }
|
|
|
|
# run echoes a command and executes it, unless this is a dry run.
|
|
#
|
|
# It discards the command's own stdout itself. Callers must not add their own
|
|
# >/dev/null: that would also hide the "would run" line, leaving a dry run
|
|
# showing none of the commands it was about to execute.
|
|
run() {
|
|
if [ "$DRY_RUN" = 1 ]; then
|
|
printf ' would run:'; printf ' %q' "$@"; printf '\n'
|
|
return 0
|
|
fi
|
|
"$@" >/dev/null
|
|
}
|
|
|
|
preflight() {
|
|
command -v "$DOCKER_BIN" >/dev/null 2>&1 || die "$DOCKER_BIN is not on PATH"
|
|
if ! $DOCKER version >/dev/null 2>&1; then
|
|
die "cannot talk to the docker daemon (try --sudo, or add your user to the docker group)"
|
|
fi
|
|
command -v gzip >/dev/null 2>&1 || warn "gzip is missing; compressed payloads cannot be restored"
|
|
local srv
|
|
srv="$($DOCKER version --format '{{.Server.Version}}' 2>/dev/null || echo unknown)"
|
|
log "docker server $srv on $(uname -s) $(uname -m)"
|
|
}
|
|
|
|
confirm() {
|
|
[ "$ASSUME_YES" = 1 ] && return 0
|
|
[ "$DRY_RUN" = 1 ] && return 0
|
|
printf '%s' "Proceed? [y/N] "
|
|
local ans; read -r ans </dev/tty || ans=""
|
|
case "$ans" in y|Y|yes|YES) return 0 ;; *) echo "aborted"; exit 1 ;; esac
|
|
}
|
|
|
|
selected() {
|
|
[ -z "$ONLY" ] && return 0
|
|
local want
|
|
IFS=, read -ra want <<< "$ONLY"
|
|
local n
|
|
for n in "${want[@]}"; do [ "$n" = "$1" ] && return 0; done
|
|
return 1
|
|
}
|
|
|
|
object_exists() { # kind name
|
|
$DOCKER "$1" inspect "$2" >/dev/null 2>&1
|
|
}
|
|
|
|
free_name() { # base -> an unused container name
|
|
local base="$1" candidate="$1$RENAME_SUFFIX" i=2
|
|
while object_exists container "$candidate"; do
|
|
candidate="$base$RENAME_SUFFIX-$i"; i=$((i+1))
|
|
[ "$i" -gt 50 ] && die "no free name based on $base"
|
|
done
|
|
printf '%s' "$candidate"
|
|
}
|
|
|
|
ensure_network() { # name, then the full docker network create argv
|
|
local name="$1"; shift
|
|
if object_exists network "$name"; then
|
|
log "network $name already exists; reusing it"
|
|
return 0
|
|
fi
|
|
log "creating network $name"
|
|
run $DOCKER "$@" || { err "could not create network $name"; return 1; }
|
|
}
|
|
|
|
ensure_volume() { # name, then the full docker volume create argv
|
|
local name="$1"; shift
|
|
if object_exists volume "$name"; then
|
|
warn "volume $name already exists; restored data will be merged into it"
|
|
return 0
|
|
fi
|
|
log "creating volume $name"
|
|
run $DOCKER "$@" || { err "could not create volume $name"; return 1; }
|
|
}
|
|
|
|
ensure_dir() { # host path for a bind mount
|
|
if [ -e "$1" ]; then return 0; fi
|
|
log "creating host directory $1"
|
|
if [ "$DRY_RUN" = 1 ]; then printf ' would run: mkdir -p %q\n' "$1"; return 0; fi
|
|
mkdir -p "$1" 2>/dev/null || sudo mkdir -p "$1" || { err "cannot create $1"; return 1; }
|
|
}
|
|
|
|
ensure_image_pull() { # ref
|
|
if object_exists image "$1"; then log "image $1 already present"; return 0; fi
|
|
log "pulling image $1"
|
|
run $DOCKER pull "$1" || { err "could not pull $1"; return 1; }
|
|
}
|
|
|
|
ensure_image_load() { # ref relpath sha256 compressed
|
|
if object_exists image "$1"; then log "image $1 already present"; return 0; fi
|
|
verify_payload "$2" "$3" || return 1
|
|
log "loading image $1 from $2"
|
|
if [ "$DRY_RUN" = 1 ]; then printf ' would run: docker load < %q\n' "$2"; return 0; fi
|
|
if [ "$4" = 1 ]; then gzip -dc -- "$PKGDIR/$2" | $DOCKER load >/dev/null
|
|
else $DOCKER load >/dev/null < "$PKGDIR/$2"; fi
|
|
}
|
|
|
|
verify_payload() { # relpath sha256
|
|
[ -f "$PKGDIR/$1" ] || { err "payload missing from package: $1"; return 1; }
|
|
[ "$VERIFY" = 1 ] || return 0
|
|
if ! command -v sha256sum >/dev/null 2>&1; then
|
|
warn "sha256sum not available; skipping checksum verification"
|
|
VERIFY=0
|
|
return 0
|
|
fi
|
|
local got
|
|
got="$(sha256sum "$PKGDIR/$1" | cut -d' ' -f1)"
|
|
if [ "$got" != "$2" ]; then
|
|
err "checksum mismatch for $1 (package is corrupt or truncated)"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
feed_archive() { # relpath compressed container extract_into
|
|
if [ "$DRY_RUN" = 1 ]; then
|
|
printf ' would restore %q into %s:%s\n' "$1" "$3" "$4"
|
|
return 0
|
|
fi
|
|
if [ "$2" = 1 ]; then
|
|
gzip -dc -- "$PKGDIR/$1" | $DOCKER cp -a - "$3:$4"
|
|
else
|
|
$DOCKER cp -a - "$3:$4" < "$PKGDIR/$1"
|
|
fi
|
|
}
|
|
|
|
# resolve_mount prints the volume name, or the host path, backing a mount
|
|
# destination in a container that already exists.
|
|
resolve_mount() { # container destination
|
|
local d n s
|
|
while IFS='|' read -r d n s; do
|
|
if [ "$d" = "$2" ]; then
|
|
if [ -n "$n" ]; then printf '%s' "$n"; else printf '%s' "$s"; fi
|
|
return 0
|
|
fi
|
|
done < <($DOCKER inspect --format '{{range .Mounts}}{{.Destination}}|{{.Name}}|{{.Source}}{{"\n"}}{{end}}' "$1")
|
|
return 1
|
|
}
|
|
|
|
# seed_readonly fills a mount the container declares read-only. The same volume
|
|
# or host path is attached writable to a throwaway container, which is created
|
|
# but never started, and removed straight after.
|
|
seed_readonly() { # container image destination relpath compressed
|
|
if [ "$DRY_RUN" = 1 ]; then
|
|
printf ' would seed read-only mount %s from %q via a staging container\n' "$3" "$4"
|
|
return 0
|
|
fi
|
|
local store base stage mountat
|
|
store="$(resolve_mount "$1" "$3")" || { err "cannot resolve storage behind $3"; return 1; }
|
|
base="${3##*/}"
|
|
stage="dm-stage-$$-${RANDOM}"
|
|
mountat="/__docker_migrate/$base"
|
|
$DOCKER create --name "$stage" --volume "$store:$mountat" "$2" >/dev/null \
|
|
|| { err "could not create staging container for $3"; return 1; }
|
|
local rc=0
|
|
if [ "$5" = 1 ]; then
|
|
gzip -dc -- "$PKGDIR/$4" | $DOCKER cp -a - "$stage:/__docker_migrate" || rc=$?
|
|
else
|
|
$DOCKER cp -a - "$stage:/__docker_migrate" < "$PKGDIR/$4" || rc=$?
|
|
fi
|
|
$DOCKER rm -f "$stage" >/dev/null 2>&1 || true
|
|
if [ "$rc" != 0 ]; then
|
|
err "failed to seed read-only mount $3"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
check_running() { # container
|
|
[ "$DRY_RUN" = 1 ] && return 0
|
|
sleep 2
|
|
local st
|
|
st="$($DOCKER inspect --format '{{.State.Status}}' "$1" 2>/dev/null || echo missing)"
|
|
if [ "$st" != "running" ]; then
|
|
err "$1 is not running (status: $st); last log lines:"
|
|
$DOCKER logs --tail 20 "$1" 2>&1 | sed 's/^/ /' || true
|
|
return 1
|
|
fi
|
|
}
|
|
`
|
|
|
|
func renderReadme(name string, prepared []*Prepared, opts spec.Options) string {
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "Docker migration package: %s\n", name)
|
|
fmt.Fprintf(&b, "%s\n\n", strings.Repeat("=", 27+len(name)))
|
|
b.WriteString("How to use this package\n")
|
|
b.WriteString("-----------------------\n")
|
|
b.WriteString("1. Copy this whole directory (or tar file) to the target host.\n")
|
|
b.WriteString("2. On the target host, unpack it if needed and run:\n\n")
|
|
b.WriteString(" ./install.sh --dry-run # review every command first\n")
|
|
b.WriteString(" ./install.sh # actually restore\n\n")
|
|
b.WriteString("The target host needs: bash, gzip, and a working docker CLI.\n")
|
|
b.WriteString("Nothing else is installed and no network access is required unless a\n")
|
|
b.WriteString("container's image is set to be pulled instead of carried.\n\n")
|
|
|
|
b.WriteString("Contents\n")
|
|
b.WriteString("--------\n")
|
|
b.WriteString(" install.sh self-contained restore script (read it, it is plain bash)\n")
|
|
b.WriteString(" manifest.json machine-readable description of everything in here\n")
|
|
b.WriteString(" images/ docker image archives\n")
|
|
b.WriteString(" data/ volume and bind mount contents, one tar per mount\n\n")
|
|
|
|
b.WriteString("Containers in this package\n")
|
|
b.WriteString("--------------------------\n")
|
|
for _, p := range prepared {
|
|
fmt.Fprintf(&b, " %s (image %s)\n", p.ContainerName(), p.Target.Image)
|
|
for _, t := range p.Transfers {
|
|
fmt.Fprintf(&b, " data: %s\n", t.Label)
|
|
}
|
|
for _, m := range p.Target.Mounts {
|
|
if m.Kind == spec.MountBind {
|
|
fmt.Fprintf(&b, " writes host path: %s\n", m.Source)
|
|
}
|
|
}
|
|
}
|
|
if opts.DryRun {
|
|
b.WriteString("\nNOTE: this package was built in dry-run mode and contains no data archives.\n")
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func collectBinds(prepared []*Prepared) []string {
|
|
seen := map[string]bool{}
|
|
var out []string
|
|
for _, p := range prepared {
|
|
for _, m := range p.Target.Mounts {
|
|
if m.Kind == spec.MountBind && !p.Render.DropMounts[m.Destination] && !seen[m.Source] {
|
|
seen[m.Source] = true
|
|
out = append(out, m.Source)
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// isSpecialBind reports paths that must never be created by the installer,
|
|
// because they are kernel or daemon sockets rather than data directories.
|
|
func isSpecialBind(p string) bool {
|
|
switch p {
|
|
case "/var/run/docker.sock", "/run/docker.sock", "/proc", "/sys", "/dev", "/":
|
|
return true
|
|
}
|
|
return strings.HasPrefix(p, "/proc/") || strings.HasPrefix(p, "/sys/") || strings.HasPrefix(p, "/dev/")
|
|
}
|
|
|
|
func boolArg(b bool) string {
|
|
if b {
|
|
return "1"
|
|
}
|
|
return "0"
|
|
}
|
|
|
|
func defaultConflict(c spec.ConflictPolicy) spec.ConflictPolicy {
|
|
if c == "" {
|
|
return spec.ConflictFail
|
|
}
|
|
return c
|
|
}
|
|
|
|
func defaultSuffix(s string) string {
|
|
if s == "" {
|
|
return "-migrated"
|
|
}
|
|
return s
|
|
}
|
|
|
|
func orDash(s string) string {
|
|
if s == "" {
|
|
return "-"
|
|
}
|
|
return s
|
|
}
|
|
|
|
// escapeDoubleQuoted makes a value safe to interpolate inside a double-quoted
|
|
// shell string in the generated script.
|
|
func escapeDoubleQuoted(s string) string {
|
|
r := strings.NewReplacer(`\`, `\\`, `"`, `\"`, "`", "\\`", `$`, `\$`)
|
|
return r.Replace(s)
|
|
}
|