Files
remnawave-minishop/scripts/migrate_to_minishop.sh
T

319 lines
10 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
ROOT=""
OLD_PREFIX="remnawave-tg-shop"
NEW_PREFIX="remnawave-minishop"
OLD_DB_VOLUME="${OLD_PREFIX}-db-data"
NEW_DB_VOLUME="${NEW_PREFIX}-db-data"
OLD_CADDY_VOLUMES=("${OLD_PREFIX}-caddy-data" "${OLD_PREFIX}-caddy-config")
NEW_CADDY_VOLUMES=("${NEW_PREFIX}-caddy-data" "${NEW_PREFIX}-caddy-config")
# Container names span three eras: original ``remnawave-tg-shop*`` (≤ v2.7.0),
# the renamed but still single-container ``remnawave-minishop*`` (v3.1.0
# v3.3.x), and the split-arch stack introduced in v3.4.0 (backend / worker /
# frontend / migrate / postgres / redis, plus optional caddy). The list is
# used only to stop existing containers before migration, so it is safe —
# and idempotent — to include every known name from every era.
KNOWN_CONTAINERS=(
# v2.7.0 (upstream remnawave-tg-shop):
"${OLD_PREFIX}"
"${OLD_PREFIX}-db"
"${OLD_PREFIX}-caddy"
# v3.1.0 v3.2.x (renamed but still one container):
"${NEW_PREFIX}"
"${NEW_PREFIX}-db"
"${NEW_PREFIX}-caddy"
# v3.4.0+ (split architecture):
"${NEW_PREFIX}-backend"
"${NEW_PREFIX}-worker"
"${NEW_PREFIX}-frontend"
"${NEW_PREFIX}-migrate"
"${NEW_PREFIX}-postgres"
"${NEW_PREFIX}-redis"
)
log() {
printf '%s\n' "$*"
}
die() {
printf 'Ошибка: %s\n' "$*" >&2
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "Не найдено обязательное средство \`$1\` в PATH."
}
resolve_root() {
if [[ -n "${PROJECT_ROOT:-}" ]]; then
[[ -d "$PROJECT_ROOT" ]] || die "PROJECT_ROOT не существует: $PROJECT_ROOT"
(cd -- "$PROJECT_ROOT" >/dev/null && pwd -P)
return
fi
local git_root
if git_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then
printf '%s\n' "$git_root"
return
fi
pwd -P
}
run() {
log "+ $*"
"$@"
}
container_exists() {
docker inspect "$1" >/dev/null 2>&1
}
container_running() {
[[ "$(docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null || true)" == "true" ]]
}
stop_container() {
local name="$1"
if ! container_exists "$name"; then
return 1
fi
if container_running "$name"; then
docker stop "$name" >/dev/null
fi
docker rm "$name" >/dev/null
}
volume_exists() {
docker volume inspect "$1" >/dev/null 2>&1
}
volume_is_empty() {
docker run --rm -v "$1:/data" alpine sh -c 'test -z "$(find /data -mindepth 1 -print -quit)"' >/dev/null 2>&1
}
copy_volume() {
local source="$1"
local target="$2"
if ! volume_exists "$source"; then
log " - Пропускаю том \`$source\`: исходный том не найден."
return 1
fi
if volume_exists "$target" && ! volume_is_empty "$target"; then
log " - Пропускаю том \`$target\`: он уже не пустой."
return 1
fi
if ! volume_exists "$target"; then
die "Целевой том \`$target\` не создан Compose. Сначала нужно подготовить новый стек в режиме \`--no-start\`."
fi
docker run --rm -v "$source:/from:ro" -v "$target:/to" alpine sh -c 'cd /from && cp -a . /to/'
}
is_old_postgres_host() {
grep -Eq "^[[:space:]]*POSTGRES_HOST[[:space:]]*=[[:space:]]*${OLD_PREFIX}-db[[:space:]]*(#.*)?$" "$ROOT/.env"
}
is_new_postgres_host() {
grep -Eq "^[[:space:]]*POSTGRES_HOST[[:space:]]*=[[:space:]]*${NEW_PREFIX}-db[[:space:]]*(#.*)?$" "$ROOT/.env"
}
update_postgres_host() {
if is_old_postgres_host; then
sed -i.bak -E "s|^([[:space:]]*POSTGRES_HOST[[:space:]]*=[[:space:]]*)${OLD_PREFIX}-db([[:space:]]*(#.*)?)$|\\1${NEW_PREFIX}-db\\2|" "$ROOT/.env"
log " - \`.env\` обновлён, резервная копия сохранена в \`.env.bak\`."
elif is_new_postgres_host; then
log " - \`POSTGRES_HOST\` уже указывает на новый контейнер, ничего менять не нужно."
else
log " - \`POSTGRES_HOST\` не похож на старую схему, пропускаю изменение."
fi
}
main() {
require_cmd git
require_cmd docker
docker info >/dev/null
ROOT="$(resolve_root)"
local compose_file="${COMPOSE_FILE:-docker-compose.yml}"
local target_branch="${TARGET_BRANCH:-main}"
local git_remote="${GIT_REMOTE:-origin}"
local new_origin_url="${NEW_ORIGIN_URL:-}"
local assume_yes="${ASSUME_YES:-0}"
local current_origin
local current_branch
local remote_ref
local head_commit
local compose_has_build=0
local compose_has_caddy=0
local -a compose_cmd
local -a running_containers=()
local -a summary=()
local -a up_args
local name
local source
local target
local answer
if [[ $compose_file != /* ]]; then
compose_file="$ROOT/$compose_file"
fi
[[ -f "$compose_file" ]] || die "Compose-файл не найден: $compose_file"
[[ -e "$ROOT/.git" ]] || die "Скрипт нужно запускать из корня git-репозитория."
[[ -f "$ROOT/.env" ]] || die "Не найден \`.env\` в корне репозитория."
if docker compose version >/dev/null 2>&1; then
compose_cmd=(docker compose)
elif command -v docker-compose >/dev/null 2>&1; then
compose_cmd=(docker-compose)
else
die "Не найден ни \`docker compose\`, ни \`docker-compose\`."
fi
if [[ -n "$(git -C "$ROOT" status --porcelain=v1)" ]]; then
die "В рабочем дереве есть незакоммиченные изменения. Сначала сохраните их, чтобы миграция не затёрла чужие правки."
fi
if grep -Eq '^[[:space:]]*build:[[:space:]]*' "$compose_file"; then
compose_has_build=1
fi
if grep -Eq '^[[:space:]]*caddy:[[:space:]]*$' "$compose_file"; then
compose_has_caddy=1
fi
for name in "${KNOWN_CONTAINERS[@]}"; do
if container_exists "$name"; then
running_containers+=("$name")
fi
done
current_origin="$(git -C "$ROOT" remote get-url "$git_remote")"
if [[ -n "$new_origin_url" && "$current_origin" != "$new_origin_url" ]]; then
summary+=( "обновить $git_remote с \`$current_origin\` на \`$new_origin_url\`" )
fi
summary+=( "скачать ветку \`$target_branch\` из \`$git_remote\`" )
if volume_exists "$OLD_DB_VOLUME"; then
summary+=( "проверить том БД \`$OLD_DB_VOLUME\` и перенести в \`$NEW_DB_VOLUME\` при необходимости" )
fi
if ((compose_has_caddy)); then
for i in 0 1; do
source="${OLD_CADDY_VOLUMES[$i]}"
target="${NEW_CADDY_VOLUMES[$i]}"
if volume_exists "$source"; then
summary+=( "проверить том \`$source\` и перенести в \`$target\` при необходимости" )
fi
done
fi
if is_old_postgres_host; then
summary+=( "обновить \`POSTGRES_HOST\` в \`.env\`" )
fi
summary+=( "подготовить новый стек через Compose в режиме \`--no-start\`" )
summary+=( "запустить compose-файл \`$(basename "$compose_file")\`" )
if [[ "$assume_yes" != "1" ]]; then
if [[ ! -t 0 ]]; then
die "Скрипт ожидает интерактивное подтверждение. Запустите с \`ASSUME_YES=1\` для неинтерактивного режима."
fi
log "План миграции:"
for name in "${summary[@]}"; do
log " - $name"
done
read -r -p "Продолжить? [y/N]: " answer
case "$answer" in
y|Y|yes|YES|Yes)
;;
*)
die "Миграция отменена пользователем."
;;
esac
fi
log "1. Останавливаю старый стек"
if ((${#running_containers[@]})); then
for name in "${running_containers[@]}"; do
stop_container "$name"
log " - контейнер \`$name\` остановлен/удалён"
done
else
log " - запущенных контейнеров старой схемы не найдено"
fi
if [[ -n "$new_origin_url" && "$current_origin" != "$new_origin_url" ]]; then
log "2. Обновляю origin"
run git -C "$ROOT" remote set-url "$git_remote" "$new_origin_url"
else
log "2. Origin уже актуален, пропускаю"
fi
log "3. Обновляю git до ветки \`$target_branch\`"
run git -C "$ROOT" fetch "$git_remote" "$target_branch"
current_branch="$(git -C "$ROOT" branch --show-current || true)"
if [[ -z "$current_branch" ]]; then
current_branch="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD)"
fi
if [[ "$current_branch" != "$target_branch" ]]; then
if git -C "$ROOT" show-ref --verify --quiet "refs/heads/$target_branch"; then
run git -C "$ROOT" switch "$target_branch"
else
run git -C "$ROOT" switch -c "$target_branch" --track "$git_remote/$target_branch"
fi
else
log " - уже на ветке \`$target_branch\`"
fi
remote_ref="$(git -C "$ROOT" rev-parse "$git_remote/$target_branch")"
head_commit="$(git -C "$ROOT" rev-parse HEAD)"
if [[ "$head_commit" != "$remote_ref" ]]; then
run git -C "$ROOT" pull --ff-only "$git_remote" "$target_branch"
else
log " - локальная ветка уже совпадает с удалённой, \`git pull\` не нужен"
fi
log "4. Обновляю \`.env\`"
update_postgres_host
log "5. Подготавливаю новый стек через Compose"
if ((compose_has_build)); then
run "${compose_cmd[@]}" -f "$compose_file" up --no-start --build
else
run "${compose_cmd[@]}" -f "$compose_file" up --no-start
fi
log "6. Переношу тома"
if copy_volume "$OLD_DB_VOLUME" "$NEW_DB_VOLUME"; then
log " - БД перенесена в \`$NEW_DB_VOLUME\`"
fi
if ((compose_has_caddy)); then
for i in 0 1; do
source="${OLD_CADDY_VOLUMES[$i]}"
target="${NEW_CADDY_VOLUMES[$i]}"
if copy_volume "$source" "$target"; then
log " - \`$source\` перенесён в \`$target\`"
fi
done
fi
log "7. Запускаю новый стек"
if ((compose_has_build)); then
up_args=(up -d --build --remove-orphans)
else
up_args=(up -d --remove-orphans)
fi
run "${compose_cmd[@]}" -f "$compose_file" "${up_args[@]}"
run "${compose_cmd[@]}" -f "$compose_file" ps
log "Готово."
}
main "$@"