diff --git a/docs/getting-started/deployment.md b/docs/getting-started/deployment.md index ae3f09d..844cdda 100644 --- a/docs/getting-started/deployment.md +++ b/docs/getting-started/deployment.md @@ -31,7 +31,8 @@ Wizard работает через меню с цифрами и подтвер - сохранить backup существующих файлов перед перезаписью; - запустить `docker compose pull && docker compose up -d`; - проверить текущий стек через `docker compose ps` и логи `migrate`; -- запустить миграцию из поддерживаемых legacy-ботов, сейчас - из Remnashop. +- запустить миграцию из поддерживаемых legacy-ботов: Remnashop и старый + `remnawave-tg-shop`; Для тестирования другой ветки или форка задайте источник перед запуском: @@ -41,8 +42,11 @@ MINISHOP_INSTALL_REF=main \ sh install.sh ``` -Миграция в wizard сначала запускает `dry-run`, показывает JSON-сводку и только -после отдельного подтверждения применяет изменения в целевую БД. +Миграция Remnashop в wizard сначала запускает `dry-run`, показывает JSON-сводку +и только после отдельного подтверждения применяет изменения в целевую БД. +Миграция со старого `remnawave-tg-shop` работает как upgrade совместимой БД: +либо копирует старый Docker volume, либо делает `pg_dump` по source DSN, +восстанавливает дамп в целевую compose-БД и запускает сервис `migrate`. Обычный `docker compose up -d --build` поднимает: diff --git a/docs/migrations/remnawave-tg-shop.md b/docs/migrations/remnawave-tg-shop.md index b7e16b0..9ef4b84 100644 --- a/docs/migrations/remnawave-tg-shop.md +++ b/docs/migrations/remnawave-tg-shop.md @@ -4,6 +4,33 @@ Автоматический скрипт ниже рассчитан именно на родственный стек `remnawave-tg-shop`, где структура БД и Docker volumes известны заранее. Для других ботов нужен отдельный адаптер экспорта/импорта. +## Новый install wizard + +Для нового сервера или переноса без клонирования репозитория используйте общий +`sh` wizard: + +```bash +curl -fsSL https://raw.githubusercontent.com/3252a8/remnawave-minishop/main/scripts/install.sh -o install.sh +sh install.sh +``` + +В меню выберите `Install new stack and run legacy migration` или +`Run legacy migration only`, затем источник `Legacy remnawave-tg-shop`. +Wizard поддерживает два режима: + +- `Copy old Docker volumes` - тот же безопасный сценарий, что и старый + `migrate_to_minishop.sh`: старый volume `remnawave-tg-shop-db-data` + копируется в `remnawave-minishop-db-data`, затем новый stack запускает + сервис `migrate` и накатывает все схемные миграции; +- `Dump from a source PostgreSQL DSN` - новый режим для случаев, когда старая + БД доступна как внешний PostgreSQL DSN. Wizard поднимает целевой `postgres`, + сбрасывает целевую БД, делает `pg_dump` из старой БД, восстанавливает дамп в + compose-БД и затем запускает `migrate`. + +Старый helper `scripts/migrate_to_minishop.sh` ниже всё ещё полезен для +in-place обновления уже клонированного репозитория: он переключает git-ветку, +переносит volumes и стартует новый stack. Прямой source DSN он не поддерживал. + ## Короткий путь без смены ветки и сборки Если вы используете только готовые Docker-образы и не собираете проект diff --git a/scripts/install.sh b/scripts/install.sh index d9d93dc..f365310 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -11,6 +11,13 @@ INSTALL_STATE_DIR=".installer" IMPORTER_CACHE_PATH="$INSTALL_STATE_DIR/import_legacy.py" APP_UID=10001 APP_GID=10001 +OLD_TGSHOP_DB_VOLUME="remnawave-tg-shop-db-data" +NEW_MINISHOP_DB_VOLUME="remnawave-minishop-db-data" +OLD_TGSHOP_CADDY_DATA_VOLUME="remnawave-tg-shop-caddy-data" +OLD_TGSHOP_CADDY_CONFIG_VOLUME="remnawave-tg-shop-caddy-config" +NEW_MINISHOP_CADDY_DATA_VOLUME="remnawave-minishop-caddy-data" +NEW_MINISHOP_CADDY_CONFIG_VOLUME="remnawave-minishop-caddy-config" +KNOWN_LEGACY_CONTAINERS="remnawave-tg-shop remnawave-tg-shop-db remnawave-tg-shop-caddy remnawave-minishop remnawave-minishop-db remnawave-minishop-caddy remnawave-minishop-backend remnawave-minishop-worker remnawave-minishop-frontend remnawave-minishop-migrate remnawave-minishop-postgres remnawave-minishop-redis" if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then RESET="$(printf '\033[0m')" @@ -40,6 +47,7 @@ ENV_PATH="" COMPOSE_STYLE="" PROMPT_VALUE="" CHOICE_VALUE="" +LEGACY_SOURCE="" COMPOSE_PROJECT_NAME_VALUE="" IMAGE_TAG_VALUE="" @@ -121,9 +129,11 @@ Environment overrides: MINISHOP_INSTALL_REF default ref ($DEFAULT_REF) MINISHOP_IMAGE_TAG default image tag ($DEFAULT_IMAGE_TAG) REMNASHOP_SOURCE_DSN default source DSN for migration + LEGACY_TGSHOP_SOURCE_DSN default remnawave-tg-shop source DSN for dump/restore The wizard is interactive by design. It never overwrites files without -confirmation and always runs legacy imports as dry-run first. +confirmation. Remnashop imports always run dry-run first; legacy +remnawave-tg-shop can be migrated from Docker volumes or a PostgreSQL DSN. EOF } @@ -677,9 +687,12 @@ run_compose() { } start_stack() { + pull="${1:-1}" section "Start Docker stack" require_docker || return 1 - (cd "$TARGET_DIR" && run_compose pull) || return 1 + if [ "$pull" = "1" ]; then + (cd "$TARGET_DIR" && run_compose pull) || return 1 + fi (cd "$TARGET_DIR" && run_compose up -d) || return 1 (cd "$TARGET_DIR" && run_compose ps) || true ok "Stack command completed." @@ -693,6 +706,95 @@ validate_stack() { ok "Validation commands completed." } +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_if_safe() { + source_volume="$1" + target_volume="$2" + required="${3:-0}" + + if ! volume_exists "$source_volume"; then + if [ "$required" = "1" ]; then + fail "Source Docker volume not found: $source_volume" + return 1 + fi + warn "Skipping $source_volume: source volume was not found." + return 0 + fi + + if ! volume_exists "$target_volume"; then + if [ "$required" = "1" ]; then + fail "Target Docker volume not found: $target_volume" + return 1 + fi + warn "Skipping $target_volume: target volume was not created by this profile." + return 0 + fi + + if ! volume_is_empty "$target_volume"; then + if [ "$required" = "1" ]; then + warn "Target volume $target_volume is already not empty." + warn "It may already be migrated, or the target stack may have been started with an empty database." + if confirm "Continue without copying the legacy database volume?" 0; then + return 0 + fi + return 1 + fi + warn "Skipping $target_volume: target volume is already not empty." + return 0 + fi + + run_label="docker run --rm -v $source_volume:/from:ro -v $target_volume:/to alpine sh -c 'cd /from && cp -a . /to/'" + color "+ $run_label" "$DIM" + printf '\n' + docker run --rm \ + -v "$source_volume:/from:ro" \ + -v "$target_volume:/to" \ + alpine sh -c 'cd /from && cp -a . /to/' || return 1 + ok "Copied $source_volume -> $target_volume" +} + +stop_known_legacy_containers() { + section "Stop legacy containers" + stopped=0 + for container in $KNOWN_LEGACY_CONTAINERS; do + if docker inspect "$container" >/dev/null 2>&1; then + if docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null | grep -q '^true$'; then + docker stop "$container" >/dev/null || true + fi + docker rm "$container" >/dev/null || true + info "Stopped/removed $container" + stopped=1 + fi + done + if [ "$stopped" = "0" ]; then + info "No known legacy containers found." + fi +} + +wait_target_postgres() { + section "Wait for target PostgreSQL" + attempt=1 + while [ "$attempt" -le 30 ]; do + if (cd "$TARGET_DIR" && compose exec -T postgres sh -c \ + 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"' >/dev/null 2>&1); then + ok "PostgreSQL is ready." + return 0 + fi + sleep 2 + attempt=$((attempt + 1)) + done + fail "Target PostgreSQL did not become ready." + return 1 +} + download_importer() { importer="$TARGET_DIR/$IMPORTER_CACHE_PATH" mkdir -p "$(dirname "$importer")" @@ -742,23 +844,38 @@ run_import_command() { (cd "$TARGET_DIR" && run_compose "$@") } -run_legacy_migration() { - section "Legacy migration" +choose_legacy_source() { + choose "Source bot" "1" "1|2|3" \ + "1. Remnashop - import users, subscriptions, payments, referrals and promo codes." \ + "2. Legacy remnawave-tg-shop - upgrade an old compatible database/volume." \ + "3. Skip migration" + case "$CHOICE_VALUE" in + 1) LEGACY_SOURCE="remnashop" ;; + 2) LEGACY_SOURCE="remnawave-tg-shop" ;; + 3) LEGACY_SOURCE="skip" ;; + esac +} + +ensure_github_source_for_importer() { + if [ -n "$SOURCE_REPO" ] && [ -n "$SOURCE_REF" ]; then + return 0 + fi + github_source +} + +run_remnashop_migration() { + section "Remnashop migration" ENV_PATH="$TARGET_DIR/.env" if [ ! -f "$ENV_PATH" ]; then fail ".env not found. Install or generate configuration first." return 1 fi + ensure_github_source_for_importer || return 1 require_docker || return 1 POSTGRES_USER_VALUE="$(env_get POSTGRES_USER '')" POSTGRES_PASSWORD_VALUE="$(env_get POSTGRES_PASSWORD '')" POSTGRES_DB_VALUE="$(env_get POSTGRES_DB '')" - choose "Source bot" "1" "1|2" \ - "1. Remnashop" \ - "2. Skip migration" - [ "$CHOICE_VALUE" = "2" ] && return 0 - prompt_value "Source Remnashop PostgreSQL DSN" "${REMNASHOP_SOURCE_DSN:-}" 1 0 "" SOURCE_DSN="$PROMPT_VALUE" prompt_value "Source schema" "public" 1 0 "" @@ -810,6 +927,118 @@ run_legacy_migration() { ok "Legacy migration completed." } +run_target_schema_migrations() { + section "Apply target schema migrations" + require_docker || return 1 + (cd "$TARGET_DIR" && run_compose run --rm migrate) || return 1 + ok "Schema migrations completed." +} + +prepare_compose_without_starting_apps() { + section "Prepare target Compose stack" + require_docker || return 1 + (cd "$TARGET_DIR" && run_compose up --no-start) || return 1 +} + +run_tgshop_volume_migration() { + section "Legacy remnawave-tg-shop volume migration" + warn "This path copies the old PostgreSQL Docker volume into the new Minishop volume." + warn "Old volumes are not deleted; keep them until you verify the new stack." + + if confirm "Stop known old/current containers before copying volumes?" 1; then + stop_known_legacy_containers || return 1 + (cd "$TARGET_DIR" && run_compose down) || true + fi + + prepare_compose_without_starting_apps || return 1 + copy_volume_if_safe "$OLD_TGSHOP_DB_VOLUME" "$NEW_MINISHOP_DB_VOLUME" 1 || return 1 + copy_volume_if_safe "$OLD_TGSHOP_CADDY_DATA_VOLUME" "$NEW_MINISHOP_CADDY_DATA_VOLUME" 0 || return 1 + copy_volume_if_safe "$OLD_TGSHOP_CADDY_CONFIG_VOLUME" "$NEW_MINISHOP_CADDY_CONFIG_VOLUME" 0 || return 1 + + if confirm "Start the new stack and let migrate apply schema changes now?" 1; then + start_stack 0 || return 1 + (cd "$TARGET_DIR" && run_compose logs --tail 120 migrate) || true + else + warn "Stack was prepared but not started. Run docker compose up -d later." + fi +} + +run_tgshop_dsn_migration() { + section "Legacy remnawave-tg-shop DSN migration" + warn "The old standalone helper did not support direct DSN import." + warn "This wizard path dumps the old PostgreSQL database, restores it into target Compose PostgreSQL, then runs Minishop schema migrations." + warn "The target database will be dropped and recreated before restore." + + if ! confirm "Replace target database with the legacy dump?" 0; then + warn "Migration not applied." + return 0 + fi + + prompt_value "Source remnawave-tg-shop PostgreSQL DSN" "${LEGACY_TGSHOP_SOURCE_DSN:-}" 1 0 "" + SOURCE_DSN="$PROMPT_VALUE" + + require_docker || return 1 + POSTGRES_USER_VALUE="$(env_get POSTGRES_USER '')" + POSTGRES_PASSWORD_VALUE="$(env_get POSTGRES_PASSWORD '')" + POSTGRES_DB_VALUE="$(env_get POSTGRES_DB '')" + TARGET_DSN="$(local_target_dsn)" + + section "Start target PostgreSQL" + (cd "$TARGET_DIR" && run_compose stop backend worker frontend migrate) || true + (cd "$TARGET_DIR" && run_compose up -d postgres redis) || return 1 + wait_target_postgres || return 1 + + section "Reset target database" + (cd "$TARGET_DIR" && run_compose exec -T postgres sh -c \ + 'dropdb -U "$POSTGRES_USER" --if-exists "$POSTGRES_DB" && createdb -U "$POSTGRES_USER" "$POSTGRES_DB"') || return 1 + + section "Dump and restore legacy database" + (cd "$TARGET_DIR" && run_compose run --rm --no-deps \ + -e "SOURCE_DSN=$SOURCE_DSN" \ + -e "TARGET_DSN=$TARGET_DSN" \ + backend sh -lc \ + 'pg_dump --clean --if-exists --no-owner --no-privileges "$SOURCE_DSN" | psql "$TARGET_DSN"') || return 1 + + run_target_schema_migrations || return 1 + if confirm "Start the full stack now?" 1; then + start_stack 0 || return 1 + fi +} + +run_remnawave_tg_shop_migration() { + section "Legacy remnawave-tg-shop migration" + ENV_PATH="$TARGET_DIR/.env" + if [ ! -f "$ENV_PATH" ]; then + fail ".env not found. Install or generate configuration first." + return 1 + fi + require_docker || return 1 + + choose "Migration method" "1" "1|2|3" \ + "1. Copy old Docker volumes on this host (recommended for old compose installs)." \ + "2. Dump from a source PostgreSQL DSN and restore into this compose stack." \ + "3. Skip migration" + case "$CHOICE_VALUE" in + 1) run_tgshop_volume_migration ;; + 2) run_tgshop_dsn_migration ;; + 3) return 0 ;; + esac +} + +run_selected_legacy_migration() { + case "$LEGACY_SOURCE" in + remnashop) + run_remnashop_migration + ;; + remnawave-tg-shop) + run_remnawave_tg_shop_migration + ;; + skip|"") + return 0 + ;; + esac +} + installation_directory() { prompt_value "Install directory" "${MINISHOP_INSTALL_DIR:-$(pwd)}" 1 0 "" mkdir -p "$(dirname "$PROMPT_VALUE")" @@ -826,6 +1055,7 @@ github_source() { install_flow() { with_migration="$1" + LEGACY_SOURCE="" installation_directory || return 1 github_source || return 1 choose_profile @@ -838,20 +1068,43 @@ install_flow() { write_env_file || return 1 mkdir -p "$TARGET_DIR/$INSTALL_STATE_DIR" prepare_data_directory || return 1 - if confirm "Start Docker Compose stack now?" 1; then - start_stack || return 1 - fi if [ "$with_migration" = "1" ]; then - run_legacy_migration + choose_legacy_source elif confirm "Run a legacy bot migration now?" 0; then - run_legacy_migration + choose_legacy_source fi + + case "$LEGACY_SOURCE" in + remnawave-tg-shop) + run_selected_legacy_migration + ;; + remnashop) + if confirm "Start Docker Compose stack before Remnashop import?" 1; then + start_stack || return 1 + else + warn "Remnashop import needs the target stack database. Skipping import." + return 0 + fi + run_selected_legacy_migration + ;; + *) + if confirm "Start Docker Compose stack now?" 1; then + start_stack || return 1 + fi + ;; + esac } migration_only_flow() { + LEGACY_SOURCE="" installation_directory || return 1 - github_source || return 1 - run_legacy_migration + choose_legacy_source + case "$LEGACY_SOURCE" in + remnashop) + github_source || return 1 + ;; + esac + run_selected_legacy_migration } download_only_flow() { diff --git a/tests/test_install_script.py b/tests/test_install_script.py index b185524..cec0b1e 100644 --- a/tests/test_install_script.py +++ b/tests/test_install_script.py @@ -21,6 +21,7 @@ def test_shell_installer_help_does_not_require_python(): assert "MINISHOP_INSTALL_REPO" in result.stdout assert "dry-run" in result.stdout + assert "LEGACY_TGSHOP_SOURCE_DSN" in result.stdout def test_shell_installer_is_the_only_install_entrypoint(): @@ -37,3 +38,13 @@ def test_shell_installer_downloads_raw_files_and_runs_import_in_container(): assert "backend python backend/scripts/import_legacy.py" in script assert "--dry-run" in script assert "Install new stack and run legacy migration" in script + + +def test_shell_installer_supports_legacy_tgshop_volume_and_dsn_paths(): + script = INSTALL_SCRIPT.read_text(encoding="utf-8") + + assert "Legacy remnawave-tg-shop" in script + assert "remnawave-tg-shop-db-data" in script + assert "remnawave-minishop-db-data" in script + assert "pg_dump --clean --if-exists" in script + assert "run_compose run --rm migrate" in script