304 lines
9.4 KiB
Bash
304 lines
9.4 KiB
Bash
#!/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")
|
||
KNOWN_CONTAINERS=(
|
||
"${OLD_PREFIX}"
|
||
"${OLD_PREFIX}-db"
|
||
"${OLD_PREFIX}-caddy"
|
||
"${NEW_PREFIX}"
|
||
"${NEW_PREFIX}-db"
|
||
"${NEW_PREFIX}-caddy"
|
||
)
|
||
|
||
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 "$@"
|