chore: initial commit of docker-tools
This commit is contained in:
758
bash/docker-migrate.sh
Executable file
758
bash/docker-migrate.sh
Executable file
@@ -0,0 +1,758 @@
|
||||
#!/bin/bash
|
||||
# Docker Service Migration Script
|
||||
# Enhanced version matching Go docwell functionality
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Version
|
||||
VERSION="2.6.2"
|
||||
|
||||
# Resolve script directory and source shared library
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=lib/common.sh
|
||||
source "$SCRIPT_DIR/lib/common.sh"
|
||||
|
||||
# Enable debug trace after sourcing common.sh (so DEBUG variable is available)
|
||||
enable_debug_trace
|
||||
|
||||
# Default configuration
|
||||
LOG_FILE="${LOG_FILE:-/tmp/docwell/docker-migrate.log}"
|
||||
HOSTNAME=$(hostname 2>/dev/null | tr -cd 'a-zA-Z0-9._-' || echo "unknown")
|
||||
|
||||
# Source/destination configuration
|
||||
OLD_HOST="${OLD_HOST:-local}"
|
||||
OLD_PORT="${OLD_PORT:-22}"
|
||||
OLD_USER="${OLD_USER:-$(whoami)}"
|
||||
NEW_HOST="${NEW_HOST:-}"
|
||||
NEW_PORT="${NEW_PORT:-22}"
|
||||
NEW_USER="${NEW_USER:-$(whoami)}"
|
||||
|
||||
# Transfer options
|
||||
TRANSFER_METHOD="rsync"
|
||||
TRANSFER_MODE="clone" # clone (non-disruptive) or transfer (stop source)
|
||||
COMPRESSION="zstd"
|
||||
BANDWIDTH_MB="${BANDWIDTH_MB:-0}"
|
||||
TRANSFER_RETRIES="${TRANSFER_RETRIES:-3}"
|
||||
SERVICE_NAME=""
|
||||
|
||||
# Load config file overrides
|
||||
load_config
|
||||
|
||||
parse_args() {
|
||||
# Support legacy positional argument: first arg as service name
|
||||
if [[ $# -gt 0 ]] && [[ ! "$1" =~ ^-- ]]; then
|
||||
SERVICE_NAME="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--version)
|
||||
echo "docker-migrate.sh v$VERSION"
|
||||
exit 0
|
||||
;;
|
||||
--quiet|-q)
|
||||
QUIET=true
|
||||
shift
|
||||
;;
|
||||
--yes|-y)
|
||||
YES=true
|
||||
shift
|
||||
;;
|
||||
--service)
|
||||
require_arg "$1" "${2:-}" || exit 1
|
||||
SERVICE_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--source)
|
||||
require_arg "$1" "${2:-}" || exit 1
|
||||
OLD_HOST="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dest)
|
||||
require_arg "$1" "${2:-}" || exit 1
|
||||
NEW_HOST="$2"
|
||||
shift 2
|
||||
;;
|
||||
--method)
|
||||
TRANSFER_METHOD="$2"
|
||||
shift 2
|
||||
;;
|
||||
--transfer)
|
||||
TRANSFER_MODE="transfer"
|
||||
shift
|
||||
;;
|
||||
--compression)
|
||||
COMPRESSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--bwlimit)
|
||||
BANDWIDTH_MB="$2"
|
||||
shift 2
|
||||
;;
|
||||
--retries)
|
||||
TRANSFER_RETRIES="$2"
|
||||
shift 2
|
||||
;;
|
||||
--stacks-dir)
|
||||
STACKS_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--install-deps)
|
||||
AUTO_INSTALL=true
|
||||
shift
|
||||
;;
|
||||
--log)
|
||||
LOG_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
--debug|-d)
|
||||
DEBUG=true
|
||||
shift
|
||||
;;
|
||||
--verbose|-v)
|
||||
VERBOSE=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unknown option: $1${NC}" >&2
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
show_help() {
|
||||
cat << EOF
|
||||
Docker Service Migration Script v$VERSION
|
||||
|
||||
Usage: $0 [SERVICE] [OPTIONS]
|
||||
|
||||
Options:
|
||||
--service SERVICE Service name to migrate
|
||||
--source HOST Source host (default: local)
|
||||
--dest HOST Destination host/user@host
|
||||
--method METHOD Transfer method: rsync, tar, rclone (default: rsync)
|
||||
--transfer Transfer mode (stop source), default is clone mode
|
||||
--compression METHOD Compression: zstd or gzip (default: zstd)
|
||||
--bwlimit MB Bandwidth limit in MB/s for rsync (0 = unlimited)
|
||||
--retries N Number of retries for failed transfers (default: 3)
|
||||
--stacks-dir DIR Stacks directory (default: /opt/stacks)
|
||||
--quiet, -q Suppress non-error output
|
||||
--yes, -y Auto-confirm all prompts
|
||||
--dry-run Show what would be done without actually doing it
|
||||
--debug, -d Enable debug mode (verbose output + xtrace)
|
||||
--verbose, -v Enable verbose logging
|
||||
--install-deps Auto-install missing dependencies (requires root)
|
||||
--log FILE Log file path
|
||||
--version Show version information
|
||||
--help, -h Show this help message
|
||||
|
||||
Examples:
|
||||
$0 myapp --dest user@remote-host
|
||||
$0 --service myapp --source local --dest user@remote --method rsync
|
||||
$0 --service myapp --source old-server --dest new-server --transfer
|
||||
$0 --service myapp --dest user@remote --bwlimit 50 --retries 5
|
||||
$0 --service myapp --dest user@remote --dry-run
|
||||
EOF
|
||||
}
|
||||
|
||||
# ─── Dependency checks ───────────────────────────────────────────────────────
|
||||
|
||||
check_dependencies() {
|
||||
local required_cmds=("docker" "ssh")
|
||||
local missing=()
|
||||
|
||||
case "$TRANSFER_METHOD" in
|
||||
rsync) required_cmds+=("rsync") ;;
|
||||
rclone) required_cmds+=("rclone") ;;
|
||||
esac
|
||||
|
||||
for cmd in "${required_cmds[@]}"; do
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
missing+=("$cmd")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||||
log_error "Missing dependencies: ${missing[*]}"
|
||||
if [[ "$AUTO_INSTALL" == "true" ]]; then
|
||||
install_dependencies "${missing[@]}"
|
||||
return $?
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ─── Parse user@host:port ─────────────────────────────────────────────────────
|
||||
|
||||
parse_host_string() {
|
||||
local input="$1"
|
||||
local -n _host="$2"
|
||||
local -n _user="$3"
|
||||
local -n _port="$4"
|
||||
|
||||
if [[ "$input" == "local" ]]; then
|
||||
_host="local"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Parse user@host:port
|
||||
if [[ "$input" =~ ^(.+)@(.+):([0-9]+)$ ]]; then
|
||||
_user="${BASH_REMATCH[1]}"
|
||||
_host="${BASH_REMATCH[2]}"
|
||||
_port="${BASH_REMATCH[3]}"
|
||||
elif [[ "$input" =~ ^(.+)@(.+)$ ]]; then
|
||||
_user="${BASH_REMATCH[1]}"
|
||||
_host="${BASH_REMATCH[2]}"
|
||||
elif [[ "$input" =~ ^(.+):([0-9]+)$ ]]; then
|
||||
_host="${BASH_REMATCH[1]}"
|
||||
_port="${BASH_REMATCH[2]}"
|
||||
else
|
||||
_host="$input"
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Pre-flight checks ───────────────────────────────────────────────────────
|
||||
|
||||
preflight_checks() {
|
||||
local service="$1"
|
||||
|
||||
log_info "Running pre-flight checks..."
|
||||
|
||||
# Validate service name
|
||||
if ! validate_stack_name "$service"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check source
|
||||
if [[ "$OLD_HOST" == "local" ]]; then
|
||||
if [[ ! -d "$STACKS_DIR/$service" ]]; then
|
||||
log_error "Service not found locally: $STACKS_DIR/$service"
|
||||
return 1
|
||||
fi
|
||||
log_success "Source: local ($STACKS_DIR/$service)"
|
||||
else
|
||||
log_info "Source: $OLD_USER@$OLD_HOST:$OLD_PORT"
|
||||
if ! test_ssh "$OLD_HOST" "$OLD_PORT" "$OLD_USER"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verify service exists on source
|
||||
if ! ssh_cmd "$OLD_HOST" "$OLD_PORT" "$OLD_USER" "test -d '$STACKS_DIR/$service'" 2>/dev/null; then
|
||||
log_error "Service not found on source: $STACKS_DIR/$service"
|
||||
return 1
|
||||
fi
|
||||
log_success "Service found on source"
|
||||
fi
|
||||
|
||||
# Check destination
|
||||
if [[ -z "$NEW_HOST" ]]; then
|
||||
log_error "Destination host not specified"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Destination: $NEW_USER@$NEW_HOST:$NEW_PORT"
|
||||
if ! test_ssh "$NEW_HOST" "$NEW_PORT" "$NEW_USER"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Service conflict check: verify service doesn't already exist on destination
|
||||
if ssh_cmd "$NEW_HOST" "$NEW_PORT" "$NEW_USER" "test -d '$STACKS_DIR/$service'" 2>/dev/null; then
|
||||
log_warn "Service already exists on destination: $STACKS_DIR/$service"
|
||||
if ! confirm "Overwrite existing service on destination?"; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
log_success "Pre-flight checks passed"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ─── Transfer functions ──────────────────────────────────────────────────────
|
||||
|
||||
rsync_transfer() {
|
||||
local src="$1" dst_host="$2" dst_port="$3" dst_user="$4" dst_path="$5"
|
||||
local attempt=0
|
||||
local max_retries="$TRANSFER_RETRIES"
|
||||
|
||||
local rsync_opts=(-avz --progress --delete)
|
||||
|
||||
# Bandwidth limiting
|
||||
if [[ "$BANDWIDTH_MB" -gt 0 ]]; then
|
||||
local bw_kbps=$((BANDWIDTH_MB * 1024))
|
||||
rsync_opts+=(--bwlimit="$bw_kbps")
|
||||
log_info "Bandwidth limited to ${BANDWIDTH_MB}MB/s"
|
||||
fi
|
||||
|
||||
# Compression
|
||||
local comp_info
|
||||
comp_info=$(get_compressor "$COMPRESSION")
|
||||
local compressor="${comp_info%%:*}"
|
||||
if [[ "$compressor" == "zstd" ]]; then
|
||||
rsync_opts+=(--compress-choice=zstd)
|
||||
fi
|
||||
|
||||
rsync_opts+=(-e "ssh -p $dst_port -o StrictHostKeyChecking=no -o ConnectTimeout=10")
|
||||
|
||||
while [[ $attempt -lt $max_retries ]]; do
|
||||
((attempt++))
|
||||
if [[ $attempt -gt 1 ]]; then
|
||||
local backoff=$((attempt * 5))
|
||||
log_warn "Retry $attempt/$max_retries in ${backoff}s..."
|
||||
sleep "$backoff"
|
||||
fi
|
||||
|
||||
if run_or_dry "rsync $src to $dst_user@$dst_host:$dst_path" \
|
||||
rsync "${rsync_opts[@]}" "$src" "$dst_user@$dst_host:$dst_path"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_warn "Transfer attempt $attempt failed"
|
||||
done
|
||||
|
||||
log_error "Transfer failed after $max_retries attempts"
|
||||
return 1
|
||||
}
|
||||
|
||||
tar_transfer() {
|
||||
local src="$1" dst_host="$2" dst_port="$3" dst_user="$4" dst_path="$5"
|
||||
|
||||
local comp_info
|
||||
comp_info=$(get_compressor "$COMPRESSION")
|
||||
local compressor="${comp_info%%:*}"
|
||||
|
||||
log_info "Transferring via tar+$compressor..."
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "[DRY-RUN] Would tar $src | $compressor | ssh -> $dst_path"
|
||||
return 0
|
||||
fi
|
||||
|
||||
ssh_cmd "$dst_host" "$dst_port" "$dst_user" "mkdir -p '$dst_path'" || return 1
|
||||
|
||||
tar -cf - -C "$(dirname "$src")" "$(basename "$src")" | \
|
||||
$compressor | \
|
||||
ssh -p "$dst_port" -o StrictHostKeyChecking=no "$dst_user@$dst_host" \
|
||||
"cd '$dst_path' && $compressor -d | tar -xf -"
|
||||
}
|
||||
|
||||
rclone_transfer() {
|
||||
local src="$1" dst_host="$2" dst_port="$3" dst_user="$4" dst_path="$5"
|
||||
|
||||
if ! command -v rclone &>/dev/null; then
|
||||
log_error "rclone not installed, falling back to rsync"
|
||||
rsync_transfer "$src" "$dst_host" "$dst_port" "$dst_user" "$dst_path"
|
||||
return $?
|
||||
fi
|
||||
|
||||
log_info "Transferring via rclone SFTP..."
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "[DRY-RUN] Would rclone sync $src to :sftp:$dst_host:$dst_path"
|
||||
return 0
|
||||
fi
|
||||
|
||||
rclone sync "$src" ":sftp,host=$dst_host,user=$dst_user,port=$dst_port:$dst_path" \
|
||||
--transfers "$((MAX_PARALLEL > 16 ? 16 : MAX_PARALLEL))" \
|
||||
--progress \
|
||||
--sftp-ask-password \
|
||||
2>&1 || {
|
||||
log_error "rclone transfer failed"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
file_transfer() {
|
||||
local method="$1" src="$2" dst_host="$3" dst_port="$4" dst_user="$5" dst_path="$6"
|
||||
|
||||
case "$method" in
|
||||
rsync) rsync_transfer "$src" "$dst_host" "$dst_port" "$dst_user" "$dst_path" ;;
|
||||
tar) tar_transfer "$src" "$dst_host" "$dst_port" "$dst_user" "$dst_path" ;;
|
||||
rclone) rclone_transfer "$src" "$dst_host" "$dst_port" "$dst_user" "$dst_path" ;;
|
||||
*)
|
||||
log_error "Unknown transfer method: $method"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ─── Volume operations ────────────────────────────────────────────────────────
|
||||
|
||||
backup_volume() {
|
||||
local vol="$1"
|
||||
local backup_dir="$2"
|
||||
|
||||
local comp_info
|
||||
comp_info=$(get_compressor "$COMPRESSION")
|
||||
local compressor="${comp_info%%:*}"
|
||||
local ext="${comp_info#*:}"
|
||||
|
||||
local archive="$backup_dir/${vol}${ext}"
|
||||
|
||||
log_info "Backing up volume: $vol"
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "[DRY-RUN] Would backup volume $vol to $archive"
|
||||
return 0
|
||||
fi
|
||||
|
||||
docker run --rm \
|
||||
-v "${vol}:/data:ro" \
|
||||
-v "$backup_dir:/backup" \
|
||||
alpine \
|
||||
sh -c "tar -cf - -C /data . | $compressor > /backup/${vol}${ext}" 2>/dev/null || {
|
||||
log_error "Failed to backup volume: $vol"
|
||||
return 1
|
||||
}
|
||||
log_success "Volume $vol backed up"
|
||||
}
|
||||
|
||||
restore_volume() {
|
||||
local vol="$1"
|
||||
local backup_dir="$2"
|
||||
local host="$3" port="$4" user="$5"
|
||||
|
||||
# Detect archive format
|
||||
local archive=""
|
||||
local decompressor=""
|
||||
if [[ -f "$backup_dir/${vol}.tar.zst" ]]; then
|
||||
archive="$backup_dir/${vol}.tar.zst"
|
||||
decompressor="zstd -d"
|
||||
elif [[ -f "$backup_dir/${vol}.tar.gz" ]]; then
|
||||
archive="$backup_dir/${vol}.tar.gz"
|
||||
decompressor="gzip -d"
|
||||
else
|
||||
log_error "No backup found for volume: $vol"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Restoring volume: $vol"
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "[DRY-RUN] Would restore volume $vol from $archive"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$host" == "local" ]]; then
|
||||
docker run --rm \
|
||||
-v "${vol}:/data" \
|
||||
-v "$backup_dir:/backup:ro" \
|
||||
alpine \
|
||||
sh -c "cd /data && cat /backup/$(basename "$archive") | $decompressor | tar -xf -" 2>/dev/null || {
|
||||
log_error "Failed to restore volume: $vol"
|
||||
return 1
|
||||
}
|
||||
else
|
||||
ssh_cmd "$host" "$port" "$user" \
|
||||
"docker run --rm -v '${vol}:/data' -v '$backup_dir:/backup:ro' alpine sh -c 'cd /data && cat /backup/$(basename "$archive") | $decompressor | tar -xf -'" 2>/dev/null || {
|
||||
log_error "Failed to restore volume on remote: $vol"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
|
||||
log_success "Volume $vol restored"
|
||||
}
|
||||
|
||||
# ─── Post-migration verification ─────────────────────────────────────────────
|
||||
|
||||
verify_migration() {
|
||||
local service="$1"
|
||||
local host="$2" port="$3" user="$4"
|
||||
|
||||
log_info "Verifying migration..."
|
||||
|
||||
# Check service directory exists on destination
|
||||
if ! ssh_cmd "$host" "$port" "$user" "test -d '$STACKS_DIR/$service'" 2>/dev/null; then
|
||||
log_error "Service directory not found on destination"
|
||||
return 1
|
||||
fi
|
||||
log_success "Service directory present"
|
||||
|
||||
# Check compose file exists
|
||||
local compose_found=false
|
||||
for f in compose.yaml compose.yml docker-compose.yaml docker-compose.yml; do
|
||||
if ssh_cmd "$host" "$port" "$user" "test -f '$STACKS_DIR/$service/$f'" 2>/dev/null; then
|
||||
compose_found=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$compose_found" == "false" ]]; then
|
||||
log_error "No compose file found on destination"
|
||||
return 1
|
||||
fi
|
||||
log_success "Compose file present"
|
||||
|
||||
# Wait and check if service started
|
||||
log_info "Waiting for service health check (5s)..."
|
||||
sleep 5
|
||||
|
||||
if ssh_cmd "$host" "$port" "$user" \
|
||||
"cd '$STACKS_DIR/$service' && docker compose ps -q 2>/dev/null | head -1" 2>/dev/null | grep -q .; then
|
||||
log_success "Service is running on destination"
|
||||
else
|
||||
log_warn "Service may not be running on destination yet"
|
||||
fi
|
||||
|
||||
log_success "Migration verification passed"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ─── Main migration logic ────────────────────────────────────────────────────
|
||||
|
||||
migrate_service() {
|
||||
local service="$1"
|
||||
local start_time=$SECONDS
|
||||
|
||||
log_header "Migrating Service: $service"
|
||||
log_info "Mode: $TRANSFER_MODE | Method: $TRANSFER_METHOD | Compression: $COMPRESSION"
|
||||
write_log "Starting migration of $service from $OLD_HOST to $NEW_HOST"
|
||||
|
||||
# Create temp dir for volume backups
|
||||
local vol_backup_dir
|
||||
vol_backup_dir=$(mktemp -d /tmp/docwell_migrate.XXXXXX)
|
||||
register_cleanup "rm -rf '$vol_backup_dir'"
|
||||
|
||||
# Step 1: Transfer configuration
|
||||
log_info "[Step 1/5] Transferring configuration..."
|
||||
local stack_path="$STACKS_DIR/$service"
|
||||
|
||||
if [[ "$OLD_HOST" == "local" ]]; then
|
||||
file_transfer "$TRANSFER_METHOD" "$stack_path/" "$NEW_HOST" "$NEW_PORT" "$NEW_USER" "$STACKS_DIR/$service/"
|
||||
else
|
||||
# Remote to remote: pull from source then push to dest
|
||||
local local_temp
|
||||
local_temp=$(mktemp -d /tmp/docwell_config.XXXXXX)
|
||||
register_cleanup "rm -rf '$local_temp'"
|
||||
|
||||
rsync -avz -e "ssh -p $OLD_PORT -o StrictHostKeyChecking=no" \
|
||||
"$OLD_USER@$OLD_HOST:$stack_path/" "$local_temp/$service/" >/dev/null 2>&1 || {
|
||||
log_error "Failed to pull config from source"
|
||||
return 1
|
||||
}
|
||||
file_transfer "$TRANSFER_METHOD" "$local_temp/$service/" "$NEW_HOST" "$NEW_PORT" "$NEW_USER" "$STACKS_DIR/$service/"
|
||||
fi
|
||||
log_success "Configuration transferred"
|
||||
|
||||
# Step 2: Get volumes
|
||||
log_info "[Step 2/5] Discovering volumes..."
|
||||
local volumes
|
||||
mapfile -t volumes < <(get_service_volumes "$service")
|
||||
|
||||
if [[ ${#volumes[@]} -eq 0 ]] || [[ -z "${volumes[0]}" ]]; then
|
||||
log_info "No volumes to migrate"
|
||||
else
|
||||
log_info "Found ${#volumes[@]} volume(s)"
|
||||
|
||||
# Step 3: Backup volumes
|
||||
log_info "[Step 3/5] Backing up volumes..."
|
||||
local vol_idx=0
|
||||
for vol in "${volumes[@]}"; do
|
||||
[[ -z "$vol" ]] && continue
|
||||
((vol_idx++))
|
||||
echo -e "${CYAN}[$vol_idx/${#volumes[@]}]${NC} Volume: $vol"
|
||||
backup_volume "$vol" "$vol_backup_dir" || {
|
||||
log_error "Failed to backup volume: $vol"
|
||||
return 1
|
||||
}
|
||||
done
|
||||
|
||||
# Step 4: Transfer and restore volumes
|
||||
log_info "[Step 4/5] Transferring volumes..."
|
||||
file_transfer "$TRANSFER_METHOD" "$vol_backup_dir/" "$NEW_HOST" "$NEW_PORT" "$NEW_USER" "$vol_backup_dir/"
|
||||
|
||||
log_info "Restoring volumes on destination..."
|
||||
vol_idx=0
|
||||
for vol in "${volumes[@]}"; do
|
||||
[[ -z "$vol" ]] && continue
|
||||
((vol_idx++))
|
||||
|
||||
# Create volume on destination
|
||||
ssh_cmd "$NEW_HOST" "$NEW_PORT" "$NEW_USER" "docker volume create '$vol'" >/dev/null 2>&1 || true
|
||||
|
||||
echo -e "${CYAN}[$vol_idx/${#volumes[@]}]${NC} Restoring: $vol"
|
||||
restore_volume "$vol" "$vol_backup_dir" "$NEW_HOST" "$NEW_PORT" "$NEW_USER" || {
|
||||
log_warn "Failed to restore volume: $vol"
|
||||
}
|
||||
done
|
||||
fi
|
||||
|
||||
# Step 5: Start on destination and optionally stop source
|
||||
log_info "[Step 5/5] Starting service on destination..."
|
||||
local remote_compose
|
||||
for f in compose.yaml compose.yml docker-compose.yaml docker-compose.yml; do
|
||||
if ssh_cmd "$NEW_HOST" "$NEW_PORT" "$NEW_USER" "test -f '$STACKS_DIR/$service/$f'" 2>/dev/null; then
|
||||
remote_compose="$f"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -n "${remote_compose:-}" ]]; then
|
||||
if run_or_dry "start service on destination" \
|
||||
ssh_cmd "$NEW_HOST" "$NEW_PORT" "$NEW_USER" \
|
||||
"cd '$STACKS_DIR/$service' && docker compose -f '$remote_compose' up -d" >/dev/null 2>&1; then
|
||||
log_success "Service started on destination"
|
||||
else
|
||||
log_error "Failed to start service on destination"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Transfer mode: stop source
|
||||
if [[ "$TRANSFER_MODE" == "transfer" ]]; then
|
||||
log_info "Transfer mode: stopping source service..."
|
||||
if [[ "$OLD_HOST" == "local" ]]; then
|
||||
local compose_file
|
||||
compose_file=$(get_compose_file "$stack_path") && \
|
||||
run_or_dry "stop local source" docker compose -f "$stack_path/$compose_file" down >/dev/null 2>&1
|
||||
else
|
||||
run_or_dry "stop remote source" ssh_cmd "$OLD_HOST" "$OLD_PORT" "$OLD_USER" \
|
||||
"cd '$STACKS_DIR/$service' && docker compose down" >/dev/null 2>&1 || {
|
||||
log_warn "Could not stop source service"
|
||||
}
|
||||
fi
|
||||
log_success "Source service stopped"
|
||||
fi
|
||||
|
||||
# Post-migration verification
|
||||
verify_migration "$service" "$NEW_HOST" "$NEW_PORT" "$NEW_USER" || {
|
||||
log_warn "Post-migration verification had warnings"
|
||||
}
|
||||
|
||||
local elapsed=$(( SECONDS - start_time ))
|
||||
log_success "Migration completed in $(format_elapsed "$elapsed")"
|
||||
}
|
||||
|
||||
# ─── Interactive mode ─────────────────────────────────────────────────────────
|
||||
|
||||
interactive_migrate() {
|
||||
clear_screen
|
||||
log_header "Docker Service Migration"
|
||||
echo -e "${GRAY}DocWell Migration v$VERSION${NC}"
|
||||
echo
|
||||
|
||||
# Service selection
|
||||
show_spinner "Loading stacks..."
|
||||
local stacks
|
||||
mapfile -t stacks < <(get_stacks)
|
||||
hide_spinner
|
||||
|
||||
if [[ ${#stacks[@]} -eq 0 ]]; then
|
||||
log_error "No stacks found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Available services:"
|
||||
for i in "${!stacks[@]}"; do
|
||||
local status="${GRAY}●${NC}"
|
||||
is_stack_running "${stacks[$i]}" && status="${GREEN}●${NC}"
|
||||
echo -e " ${BOLD}$((i+1))${NC}) $status ${stacks[$i]}"
|
||||
done
|
||||
echo
|
||||
|
||||
read -rp "Select service to migrate: " choice
|
||||
if [[ ! "$choice" =~ ^[0-9]+$ ]] || [[ "$choice" -lt 1 ]] || [[ "$choice" -gt "${#stacks[@]}" ]]; then
|
||||
log_error "Invalid selection"
|
||||
exit 1
|
||||
fi
|
||||
SERVICE_NAME="${stacks[$((choice-1))]}"
|
||||
|
||||
# Source
|
||||
read -rp "Source host [${OLD_HOST}]: " input_host
|
||||
[[ -n "$input_host" ]] && OLD_HOST="$input_host"
|
||||
|
||||
# Destination
|
||||
read -rp "Destination host: " NEW_HOST
|
||||
if [[ -z "$NEW_HOST" ]]; then
|
||||
log_error "Destination required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse user@host for destination
|
||||
parse_host_string "$NEW_HOST" NEW_HOST NEW_USER NEW_PORT
|
||||
|
||||
# Transfer method
|
||||
echo
|
||||
echo "Transfer methods:"
|
||||
echo " 1) rsync (recommended)"
|
||||
echo " 2) tar over SSH"
|
||||
echo " 3) rclone"
|
||||
read -rp "Select method [1]: " method_choice
|
||||
case "${method_choice:-1}" in
|
||||
2) TRANSFER_METHOD="tar" ;;
|
||||
3) TRANSFER_METHOD="rclone" ;;
|
||||
*) TRANSFER_METHOD="rsync" ;;
|
||||
esac
|
||||
|
||||
# Transfer mode
|
||||
echo
|
||||
echo "Transfer modes:"
|
||||
echo " 1) Clone (keep source running)"
|
||||
echo " 2) Transfer (stop source after)"
|
||||
read -rp "Select mode [1]: " mode_choice
|
||||
case "${mode_choice:-1}" in
|
||||
2) TRANSFER_MODE="transfer" ;;
|
||||
*) TRANSFER_MODE="clone" ;;
|
||||
esac
|
||||
|
||||
echo
|
||||
log_separator
|
||||
echo -e " Service: ${BOLD}$SERVICE_NAME${NC}"
|
||||
echo -e " Source: $OLD_USER@$OLD_HOST:$OLD_PORT"
|
||||
echo -e " Destination: $NEW_USER@$NEW_HOST:$NEW_PORT"
|
||||
echo -e " Method: $TRANSFER_METHOD"
|
||||
echo -e " Mode: $TRANSFER_MODE"
|
||||
log_separator
|
||||
echo
|
||||
|
||||
if ! confirm "Proceed with migration?"; then
|
||||
log_info "Migration cancelled"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! preflight_checks "$SERVICE_NAME"; then
|
||||
log_error "Pre-flight checks failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
migrate_service "$SERVICE_NAME"
|
||||
}
|
||||
|
||||
# ─── Main ────────────────────────────────────────────────────────────────────
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
if ! check_dependencies; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse destination user@host if provided
|
||||
if [[ -n "$NEW_HOST" ]]; then
|
||||
parse_host_string "$NEW_HOST" NEW_HOST NEW_USER NEW_PORT
|
||||
fi
|
||||
|
||||
# CLI mode
|
||||
if [[ -n "$SERVICE_NAME" ]] && [[ -n "$NEW_HOST" ]]; then
|
||||
if ! preflight_checks "$SERVICE_NAME"; then
|
||||
exit 1
|
||||
fi
|
||||
migrate_service "$SERVICE_NAME"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# Interactive mode
|
||||
interactive_migrate
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user