From 2ccead9b49b40aa236f19dc7d0088a5844cf9949 Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Tue, 2 Jun 2026 10:57:28 +0300 Subject: [PATCH] refactor: consolidate migration installer --- README.md | 4 +- docs/migrations/remnashop.md | 8 +- docs/migrations/remnawave-tg-shop.md | 447 ++++++--------------------- scripts/install.sh | 9 +- scripts/migrate_to_minishop.sh | 318 ------------------- tests/test_install_script.py | 3 +- tests/test_migration_doc_accuracy.py | 187 ++++------- 7 files changed, 162 insertions(+), 814 deletions(-) delete mode 100644 scripts/migrate_to_minishop.sh diff --git a/README.md b/README.md index d1c581f..96c9ad9 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,8 @@ Remnawave Minishop - Telegram-бот и Web App (Mini App) для продажи - [Telegram-авторизация](docs/features/telegram-auth.md) и [вход по email](docs/features/email-login.md) - настройка BotFather/OAuth и SMTP-логина. - [Поддержка пользователей / тикеты](docs/features/support.md) - тикеты в Mini App, входящий список админки, уведомления, лимиты и внешняя ссылка поддержки. - [Темы Web App](docs/features/webapp-themes.md) - кастомные темы, настройка внешнего вида, логотипы, CSS/ассеты и пайплайн создания новой темы. -- [Миграции](docs/migrations/index.md) - готовые сценарии переноса с других ботов; сейчас описан `remnawave-tg-shop`. -- [Миграция с remnawave-tg-shop](docs/migrations/remnawave-tg-shop.md) - готовый сценарий для legacy-стека. +- [Миграции](docs/migrations/index.md) - готовые сценарии переноса с `remnawave-tg-shop` и Remnashop. +- [Миграция с remnawave-tg-shop](docs/migrations/remnawave-tg-shop.md) и [Remnashop](docs/migrations/remnashop.md) - сценарии через общий install wizard. ## Совместимость diff --git a/docs/migrations/remnashop.md b/docs/migrations/remnashop.md index 2387bb9..11e662c 100644 --- a/docs/migrations/remnashop.md +++ b/docs/migrations/remnashop.md @@ -1,6 +1,6 @@ # Миграция из Remnashop -Remnashop импортируется через общий legacy-importer `backend/scripts/import_legacy.py`. +Remnashop импортируется через общий скрипт импорта `backend/scripts/import_legacy.py`. Самый удобный путь - интерактивный install wizard: ```bash @@ -8,8 +8,8 @@ curl -fsSL https://raw.githubusercontent.com/3252a8/remnawave-minishop/main/scri sh install.sh ``` -В меню выберите `Install new stack and run legacy migration` для нового сервера -или `Run legacy migration only`, если compose-папка и `.env` уже готовы. +В меню выберите `Install new stack and run migration` для нового сервера +или `Run migration only`, если compose-папка и `.env` уже готовы. ## Что переносится @@ -21,7 +21,7 @@ sh install.sh - служебные mappings, чтобы повторный запуск мог работать в режиме `merge`; - настройки совместимости Remnashop в админке: старые ref-ссылки и promo codes. -Данные, которые не имеют прямого аналога, сохраняются в legacy mappings или +Данные, которые не имеют прямого аналога, сохраняются в служебных таблицах миграции или message logs как заметки, чтобы администратор мог проверить их после переноса. ## Flow wizard diff --git a/docs/migrations/remnawave-tg-shop.md b/docs/migrations/remnawave-tg-shop.md index 789fa71..640336f 100644 --- a/docs/migrations/remnawave-tg-shop.md +++ b/docs/migrations/remnawave-tg-shop.md @@ -1,117 +1,60 @@ -# Миграция с `remnawave-tg-shop` (≤ v2.7.0) на `remnawave-minishop` (v3.4+) +# Миграция с `remnawave-tg-shop` на `remnawave-minishop` -Эта страница - готовый сценарий для legacy-стека `remnawave-tg-shop`. Это единственная миграция с другого бота, которая сейчас описана в документации. Для других Telegram-ботов, самописных панелей и ручных таблиц готового сценария пока нет: их нельзя переносить по этой инструкции без отдельного анализа схемы БД, тарифов, платежей и связи с Remnawave Panel. - -Автоматический скрипт ниже рассчитан именно на родственный стек `remnawave-tg-shop`, где структура БД и Docker volumes известны заранее. Для других ботов нужен отдельный адаптер экспорта/импорта. - -## Новый install wizard - -Для нового сервера или переноса без клонирования репозитория используйте общий -`sh` wizard: +Для переноса со старого родственного стека используйте общий install 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`, затем источник `Old remnawave-tg-shop`. -Wizard поддерживает два режима: +В меню выберите `Install new stack and run migration` для нового +сервера или `Run migration only`, если compose-папка уже готова. Затем +выберите источник `Old remnawave-tg-shop`. -- `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`. +Wizard поддерживает два способа переноса: -Старый helper `scripts/migrate_to_minishop.sh` ниже всё ещё полезен для -in-place обновления уже клонированного репозитория: он переключает git-ветку, -переносит volumes и стартует новый stack. Прямой source DSN он не поддерживал. +- `Copy old Docker volumes` - для старого compose-стека на том же Docker host. + Скрипт подготавливает новый stack, копирует + `remnawave-tg-shop-db-data` в `remnawave-minishop-db-data`, опционально + переносит Caddy volumes и запускает новый stack. +- `Dump from a source PostgreSQL DSN` - для старой БД, доступной по DSN. + Скрипт поднимает целевой `postgres`, сбрасывает целевую БД, делает + `pg_dump` из старой БД, восстанавливает дамп в compose-БД и запускает + сервис `migrate`. -## Короткий путь без смены ветки и сборки +В обоих режимах старые volumes и старая БД не удаляются автоматически. -Если вы используете только готовые Docker-образы и не собираете проект -локально, git-команды из ручного способа не нужны. Достаточно обновить -compose-файл до одного из готовых примеров в `deploy/examples` и -перенести/обновить БД. Самый прямой вариант без встроенного обратного прокси - -`deploy/examples/no-proxy/docker-compose.yml`; для Caddy, Nginx и Newt есть -такие же самостоятельные папки. +## Как работает перенос -Минимальная последовательность: +`remnawave-tg-shop` и `remnawave-minishop` имеют совместимую историю схемы. +После переноса старой PostgreSQL-БД сервис `migrate` накатывает недостающие +миграции из `backend/db/migrator.py`: сначала применяются `Base.metadata`, +затем последовательные записи `schema_migrations`. Это one-shot сервис: он +должен завершиться с кодом `0`, после чего стартуют `backend` и `worker`. -```bash -docker compose down +При volume-миграции wizard: -# Скопируйте старый .env в выбранную папку примера и обновите значения там. -cp .env deploy/examples/no-proxy/.env -nano deploy/examples/no-proxy/.env +1. Останавливает известные контейнеры старого и переходного стеков, если вы + подтверждаете этот шаг. +2. Запускает `docker compose up --no-start`, чтобы Docker Compose создал новые + volumes. +3. Копирует старый volume БД: -# Подготовьте стек из готовых образов. -IMAGE_TAG=3.4.0 docker compose \ - --env-file deploy/examples/no-proxy/.env \ - -f deploy/examples/no-proxy/docker-compose.yml \ - up --no-start + ```bash + docker run --rm \ + -v remnawave-tg-shop-db-data:/from:ro \ + -v remnawave-minishop-db-data:/to \ + alpine sh -c "cd /from && cp -a . /to" + ``` -# Нужно только при переходе со старого имени volume remnawave-tg-shop-db-data. -# Если у вас уже есть remnawave-minishop-db-data, этот шаг пропустите. -docker run --rm \ - -v remnawave-tg-shop-db-data:/from:ro \ - -v remnawave-minishop-db-data:/to \ - alpine sh -c "cd /from && cp -a . /to" +4. Если старые Caddy volumes существуют, переносит + `remnawave-tg-shop-caddy-data` -> `remnawave-minishop-caddy-data` и + `remnawave-tg-shop-caddy-config` -> `remnawave-minishop-caddy-config`. +5. Запускает новый stack через Docker Compose. -IMAGE_TAG=3.4.0 docker compose \ - --env-file deploy/examples/no-proxy/.env \ - -f deploy/examples/no-proxy/docker-compose.yml \ - up -d -docker compose \ - --env-file deploy/examples/no-proxy/.env \ - -f deploy/examples/no-proxy/docker-compose.yml \ - logs migrate -``` - -Сервис `migrate` сам применит недостающие схемные миграции к перенесённому -тому PostgreSQL. Новые тома `remnawave-minishop-redis-data` и -`remnawave-minishop-shop-data` переносить не нужно: они создаются пустыми. - -Этот документ описывает обновление стека, поднятого по `remnawave-tg-shop` -(включая последний релиз `v2.7.0` форка `kavore/remnawave-tg-shop`), до -текущей версии `remnawave-minishop` (v3.4+). Между этими версиями произошли -две независимые перетряски, и скрипт пытается отработать обе одной командой: - -1. **Переименование стека** (v3.1.0): контейнеры и тома `remnawave-tg-shop-*` - стали `remnawave-minishop-*`. Простой `docker compose up -d` после - `git pull` создаёт пустую БД — без переноса тома данные теряются. -2. **Разделение бота на сервисы** (v3.4.0): из одного контейнера выделены - `backend`, `worker`, `frontend`, `migrate` + новые `postgres`, `redis`. - Появились новые volumes `redis-data` и `shop-data`, новые обязательные - переменные окружения, а схема БД обновляется автоматически one-shot - сервисом `migrate`. - -После миграции `docker compose ps` должен показать как минимум: `backend`, -`worker`, `frontend`, `postgres`, `redis` (running) и `migrate` (exited 0). -Логи: `docker compose logs -f backend worker frontend`. - -Доступные пути: - -- [Автоматический](#автоматический-способ-через-скрипт) — скрипт-обёртка - останавливает старый стек, накатывает свежий код, переносит том БД, - поднимает новые сервисы. Идемпотентный. -- [Ручной](#ручной-способ) — те же шаги командами, для тех, кому нужно - понимать каждое действие или выполнить выборочно. - -В обоих случаях: - -- старые тома **не удаляются** автоматически — это безопасный бэкап на случай - отката; -- сертификаты Caddy (если используется `deploy/examples/caddy/docker-compose.yml`) - тоже переносятся, чтобы Let's Encrypt не выписывал их заново и не упереться - в rate limit; -- схема БД обновляется автоматически: при первом `docker compose up -d` сервис - `migrate` накатывает на перенесённый том все недостающие миграции (от - alembic-схемы v2.7.0 до текущей). +Если целевой DB volume уже непустой, wizard не перетирает его молча: он +останавливается и просит отдельное подтверждение на продолжение без копирования +старой БД. ## Что меняется в архитектуре @@ -120,256 +63,55 @@ docker compose \ | Версия | Сервисы | | --- | --- | | `v2.7.0` | `remnawave-tg-shop`, `remnawave-tg-shop-db` | -| `v3.1.x–v3.3.x` | `remnawave-minishop`, `remnawave-minishop-db` | -| `v3.4+` (текущая) | `remnawave-minishop-backend`, `remnawave-minishop-worker`, `remnawave-minishop-frontend`, `remnawave-minishop-migrate`, `remnawave-minishop-postgres`, `remnawave-minishop-redis` | - -Внутри Docker-сети сервисы доступны по коротким DNS-именам (`backend`, `worker`, -`frontend`, `postgres`, `redis`), а не по полному `container_name`. Это важно -для внешнего reverse-proxy — см. раздел [Внешний reverse-proxy](#внешний-reverse-proxy) ниже. +| `v3.1.x-v3.3.x` | `remnawave-minishop`, `remnawave-minishop-db` | +| `v3.4+` | `remnawave-minishop-backend`, `remnawave-minishop-worker`, `remnawave-minishop-frontend`, `remnawave-minishop-migrate`, `remnawave-minishop-postgres`, `remnawave-minishop-redis` | **Volumes**: -| Volume | v2.7.0 | v3.4+ | Что внутри | -| --- | --- | --- | --- | -| `remnawave-minishop-db-data` | переименовать из `remnawave-tg-shop-db-data` | переносится скриптом | PostgreSQL | -| `remnawave-minishop-redis-data` | — | создаётся пустым | Redis (FSM, rate-limit, cache, очередь вебхуков, distributed locks) | -| `remnawave-minishop-shop-data` | — | создаётся пустым | `/app/data`: `tariffs.json`, темы Web App, кэш логотипа/emoji | -| `remnawave-minishop-caddy-data` / `remnawave-minishop-caddy-config` | переименовать из `remnawave-tg-shop-caddy-*` | переносится скриптом | только при Caddy-варианте | +| Volume | Что происходит | +| --- | --- | +| `remnawave-minishop-db-data` | переносится из `remnawave-tg-shop-db-data` или восстанавливается из source DSN | +| `remnawave-minishop-redis-data` | создается пустым | +| `remnawave-minishop-shop-data` | создается пустым; runtime-файлы в `/app/data` дальше настраиваются через админку или вручную | +| `remnawave-minishop-caddy-data` / `remnawave-minishop-caddy-config` | переносятся из `remnawave-tg-shop-caddy-*`, если старый стек использовал Caddy | -`redis-data` и `shop-data` стартуют пустыми — это нормально. Redis ничего -долгоживущего не хранит (всё либо FSM, либо кеш с TTL), а `data/` инициализируется -из образа при первом старте (`tariffs.json` пуст пока вы не сконфигурируете -тарифы через админ-панель). +Доступные compose-профили: `docker-compose.yml`, +`deploy/examples/caddy/docker-compose.yml`, +`deploy/examples/nginx/docker-compose.yml`, +`deploy/examples/newt/docker-compose.yml`, +`deploy/examples/no-proxy/docker-compose.yml`. -## Переменные окружения, которые могли исчезнуть или переехать +## Переменные окружения -Перед запуском нового стека проверьте `.env`. Ниже — только то, что точно -менялось между v2.7.0 и v3.4+: +Перед запуском нового стека проверьте `.env`. Самые важные изменения: -| Было (v2.7.0) | Стало (v3.4+) | Действие | +| Было | Стало | Действие | | --- | --- | --- | -| `TELEGRAM_WEBHOOK_SECRET` | `WEBHOOK_SECRET_TOKEN` | Переименовать. Если пусто — будет сгенерирован при старте, но тогда Telegram переустановит webhook (на это не реагирует существующий запрос). | -| `TELEGRAM_WEBHOOK_PATH` | удалена | Путь вебхука теперь генерируется из `BOT_TOKEN` автоматически. | -| `REQUIRED_CHANNEL_SUBSCRIBE_TO_USE` | удалена | Гейт включается автоматически, как только задан `REQUIRED_CHANNEL_ID`. | -| `STARS_PROVIDER_TOKEN` | удалена | Telegram Stars (XTR) используются напрямую. | -| `REFERRAL_ENABLED` | удалена | Реферальная программа активна по умолчанию. В legacy-режиме без JSON-каталога отключайте платежные бонусы через нули в `REFERRAL_BONUS_DAYS_*` и `REFEREE_BONUS_DAYS_*`; в JSON-тарифах обнуляйте или удаляйте `referral_bonus_days_inviter` и `referral_bonus_days_referee` у period-тарифов. | -| `POSTGRES_HOST=remnawave-tg-shop-db` | в `.env` — `remnawave-minishop-db` или пусто | Под compose значение всё равно переопределяется на сервисное имя `postgres` (см. `environment:` в compose-файлах), поэтому скрипт правит `.env` только для bare-metal сценариев. | -| `WEBHOOK_BASE_URL` | **обязательна** | Polling-режим удалён, без публичного URL бот не стартует. | -| — | `REDIS_URL=redis://redis:6379/0` | Обязательна для воркера, очередей и rate-limit. По умолчанию в compose-файлах уже задана. | -| — | `WEBAPP_SESSION_SECRET`, `WEBAPP_ENABLED`, `WEBAPP_SERVER_PORT`, `WEBAPP_THEMES_DIR`, `TARIFFS_CONFIG_PATH` | Новые настройки Web App / тарифного каталога. Безопасные дефолты есть в `.env.example`. | +| `TELEGRAM_WEBHOOK_SECRET` | `WEBHOOK_SECRET_TOKEN` | Перенести значение или сгенерировать новый stable secret. | +| `TELEGRAM_WEBHOOK_PATH` | удалена | Путь вебхука теперь рассчитывается автоматически. | +| `REQUIRED_CHANNEL_SUBSCRIBE_TO_USE` | удалена | Гейт включается, когда задан `REQUIRED_CHANNEL_ID`. | +| `STARS_PROVIDER_TOKEN` | удалена | Telegram Stars используются напрямую. | +| `POSTGRES_HOST=remnawave-tg-shop-db` | `postgres` внутри Compose | В compose-файлах `POSTGRES_HOST` переопределяется service name `postgres`. | +| `WEBHOOK_BASE_URL` | обязательна | Без публичного URL backend не стартует корректно. | +| - | `REDIS_URL=redis://redis:6379/0` | В compose-профилях задано автоматически. | +| - | `WEBAPP_SESSION_SECRET`, `WEBAPP_ENABLED`, `TARIFFS_CONFIG_PATH` | Новые настройки Web App и каталога тарифов. | -Полный референс — [docs/getting-started/configuration.md](../getting-started/configuration.md). Скрипт миграции -эти переменные **не правит** автоматически (только `POSTGRES_HOST`), потому -что у каждой инсталляции свой шаблон `.env` с кастомными значениями. Лучше -сравнить свой `.env` с `.env.example` глазами один раз, чем получить -несовместимый шаблон автоматом. +Остальные продуктовые настройки удобнее проверить после первого входа в +админку. -## Автоматический способ (через скрипт) +## Reverse Proxy -Если helper ещё не лежит у вас локально, запускайте его прямо из `raw` из -корня старого репозитория: +В старом стеке часто был один upstream `remnawave-tg-shop:8000`. В текущем +split-arch stack маршруты разделены: -```bash -bash <(curl -fsSL https://raw.githubusercontent.com/3252a8/remnawave-minishop/main/scripts/migrate_to_minishop.sh) -``` - -> Команда выше рассчитана на `bash` / Git Bash / WSL. Если вы запускаете из -> PowerShell, удобнее сначала открыть Git Bash. - -Если вы уже подтянули новую версию и файл есть локально, можно запускать так: - -```bash -bash scripts/migrate_to_minishop.sh -``` - -По умолчанию скрипт работает с `docker-compose.yml` и переключается на ветку -`main`. Можно переопределить через переменные окружения: - -| Переменная | Назначение | По умолчанию | -| ----------------- | ----------------------------------------------------------------------- | ---------------------- | -| `PROJECT_ROOT` | Явный путь к корню старого репозитория, если запуск не из него | текущая директория | -| `COMPOSE_FILE` | Какой compose-файл стартовать в конце | `docker-compose.yml` | -| `TARGET_BRANCH` | На какую ветку переключаться и подтягивать обновления | `main` | -| `GIT_REMOTE` | Какой remote использовать для `fetch`/`pull` | `origin` | -| `NEW_ORIGIN_URL` | Если задано и не совпадает с URL выбранного remote — он будет обновлён | (не меняется) | -| `ASSUME_YES` | `1` — не задавать интерактивных вопросов | `0` | - -Примеры: - -```bash -# Caddy-вариант из raw-файла. -# Перед запуском скопируйте старый .env в deploy/examples/caddy/.env -# и заполните WEBHOOK_HOST / MINIAPP_HOST. -COMPOSE_FILE=deploy/examples/caddy/docker-compose.yml \ - bash <(curl -fsSL https://raw.githubusercontent.com/3252a8/remnawave-minishop/main/scripts/migrate_to_minishop.sh) - -# С переключением origin на форк 3252a8 -NEW_ORIGIN_URL=https://github.com/3252a8/remnawave-minishop.git \ - bash <(curl -fsSL https://raw.githubusercontent.com/3252a8/remnawave-minishop/main/scripts/migrate_to_minishop.sh) - -# Без интерактива -ASSUME_YES=1 \ - bash <(curl -fsSL https://raw.githubusercontent.com/3252a8/remnawave-minishop/main/scripts/migrate_to_minishop.sh) -``` - -Что делает скрипт: - -1. **Останавливает текущий стек**: ищет известные контейнеры старой схемы - (`remnawave-tg-shop`, `…-db`, `…-caddy`), переходного периода - (`remnawave-minishop`, `…-db`, `…-caddy`) и новой схемы - (`…-backend`, `…-worker`, `…-frontend`, `…-migrate`, `…-postgres`, `…-redis`) - и останавливает их, если запущены. Безопасно при повторном запуске. -2. **Переключает `origin`**, если задана переменная `NEW_ORIGIN_URL`, иначе - оставляет как есть. -3. **Подтягивает целевую ветку** (`git fetch` + `git switch` + `git pull --ff-only`). - Прерывается, если в рабочем дереве есть незакоммиченные изменения. -4. **Правит `POSTGRES_HOST` в `.env`** (только для bare-metal сценариев — в - compose это значение перебивает `environment:` блок). -5. **Подготавливает новый стек в режиме `--no-start`**, чтобы Compose сам - создал тома `db-data`, `redis-data`, `shop-data` и не ругался на уже - существующий volume. -6. **Переносит том БД** `remnawave-tg-shop-db-data` → `remnawave-minishop-db-data` - (и Caddy-тома, если применимо) через одноразовый `alpine`-контейнер. Если - новый том уже непустой — копирование пропускается. Новые volumes - `redis-data` и `shop-data` остаются пустыми (их и не должно быть в старом - стеке). -7. **Стартует новый стек** (`docker compose up -d --remove-orphans` плюс - `--build` для локальной сборки). `migrate` отработает первым, накатит - на перенесённый том все недостающие миграции (от alembic-схемы v2.7.0 до - текущей) и завершится. Затем стартуют `backend`, `worker`, `frontend`. - -Скрипт идемпотентен: повторный запуск ничего не сломает, просто пропустит уже -выполненные шаги. - -После того как убедитесь, что бот работает и данные на месте, удалите старые -тома: - -```bash -docker volume rm remnawave-tg-shop-db-data -docker volume rm remnawave-tg-shop-caddy-data remnawave-tg-shop-caddy-config 2>/dev/null || true -``` - -## Ручной способ - -1. **Остановите старый стек и обновите код:** - - ```bash - docker compose down - git fetch origin - git checkout main - git pull --ff-only origin main - ``` - -2. **(Только для bare-metal без compose)** обновите `.env`, если в нём ещё - жёстко прописан старый контейнер БД: - - ```bash - sed -i.bak 's/^POSTGRES_HOST=remnawave-tg-shop-db$/POSTGRES_HOST=remnawave-minishop-db/' .env - ``` - - Под `docker compose up` это не нужно: compose сам выставляет - `POSTGRES_HOST: postgres` (имя сервиса) в `environment:` и `.env`-значение - не используется. - -3. **Проверьте `.env`** на наличие переменных, которые исчезли или - переименовались — см. раздел - [Переменные окружения](#переменные-окружения-которые-могли-исчезнуть-или-переехать) - выше. Главное: `WEBHOOK_SECRET_TOKEN` (бывший `TELEGRAM_WEBHOOK_SECRET`), - обязательный `WEBHOOK_BASE_URL` и наличие `REDIS_URL` (по умолчанию задано - в compose). - -4. **Подготовьте новый стек без запуска**, чтобы Compose создал новые volumes - (`db-data`, `redis-data`, `shop-data`) и контейнеры: - - ```bash - # Локальная сборка - docker compose up --no-start --build - - # Или готовый Caddy-вариант из GHCR-образов - cp .env deploy/examples/caddy/.env - nano deploy/examples/caddy/.env - docker compose \ - --env-file deploy/examples/caddy/.env \ - -f deploy/examples/caddy/docker-compose.yml \ - up --no-start - - # Другие готовые варианты: - # deploy/examples/nginx/docker-compose.yml - # deploy/examples/newt/docker-compose.yml - # deploy/examples/no-proxy/docker-compose.yml - ``` - -5. **Перенесите том БД в новое имя:** - - ```bash - docker run --rm \ - -v remnawave-tg-shop-db-data:/from:ro \ - -v remnawave-minishop-db-data:/to \ - alpine sh -c "cd /from && cp -a . /to" - ``` - - `remnawave-minishop-redis-data` и `remnawave-minishop-shop-data` — новые, - переносить нечего. Они инициализируются на лету: Redis пуст, а `data/` - наполняется при первом обращении к настройкам Web App / каталогу тарифов. - -6. **(Только для Caddy)** перенесите тома Caddy с TLS-сертификатами и - состоянием ACME: - - ```bash - for v in caddy-data caddy-config; do - docker run --rm \ - -v "remnawave-tg-shop-$v":/from:ro \ - -v "remnawave-minishop-$v":/to \ - alpine sh -c "cd /from && cp -a . /to" - done - ``` - -7. **Запустите новый стек:** - - ```bash - docker compose up -d - # или - docker compose \ - --env-file deploy/examples/caddy/.env \ - -f deploy/examples/caddy/docker-compose.yml \ - up -d - ``` - - Сервис `migrate` запустится первым, обнаружит перенесённый том, - применит недостающие схемные миграции (`Base.metadata.create_all` + - последовательные миграции `0001..00NN` из `backend/db/migrator.py`) и - выйдет с кодом 0. Только после этого стартуют `backend` и `worker`. - -8. **Проверьте состояние:** - - ```bash - docker compose ps - docker compose logs -f backend worker frontend - docker compose logs migrate # должен закончиться "Migrator: migration 00NN applied successfully" - ``` - -9. **(Опционально) удалите старые тома**, когда убедитесь, что новый стек - стабилен: - - ```bash - docker volume rm remnawave-tg-shop-db-data - docker volume rm remnawave-tg-shop-caddy-data remnawave-tg-shop-caddy-config 2>/dev/null || true - ``` - -## Внешний reverse-proxy - -В v2.7.0 был один upstream — `remnawave-tg-shop:8000`. В v3.4+ функциональность -разнесена по портам и сервисам: - -| Назначение | DNS-имя сервиса | Порт | +| Назначение | Service | Port | | --- | --- | --- | -| Telegram / платежные / вебхуки панели | `backend` | `8080` | -| Health-чек | `backend` | `8080` (`/healthz`) | -| Web App API (`/api/*`, `/auth/*`, ассеты тем и логотипов) | `backend` | `8081` (доступен только из Docker-сети) | -| Статический фронт Web App | `frontend` | `80` (внутри `frontend` уже проксирует `/api/*` и `/auth/*` на `backend:8081`) | +| Telegram, платежные и panel webhooks | `backend` | `8080` | +| Health-check | `backend` | `8080` (`/healthz`) | +| Web App API и auth | `backend` | `8081` внутри Docker-сети | +| Статический Web App frontend | `frontend` | `80` | -Минимальная замена для внешнего Nginx, который раньше слал всё на один -upstream: +Минимальная схема для внешнего Nginx: ```nginx upstream remnawave_backend_webhooks { server backend:8080; } @@ -378,33 +120,32 @@ upstream remnawave_frontend { server frontend:80; } server { server_name app.domain.com; listen 443 ssl; - http2 on; - # ssl_certificate / ssl_certificate_key — без изменений location /webhook/ { proxy_pass http://remnawave_backend_webhooks; } location /healthz { proxy_pass http://remnawave_backend_webhooks; } - location / { proxy_pass http://remnawave_frontend; } + location / { proxy_pass http://remnawave_frontend; } } ``` -Полные примеры (Caddy, Nginx, Newt/Pangolin и запуск без обратного прокси) — в -[docs/getting-started/deployment.md](../getting-started/deployment.md) и [docs/features/web-app.md](../features/web-app.md). Если раньше прокси указывал на -`remnawave-tg-shop:8000` напрямую, после миграции нужно либо переключиться на -`backend:8080` / `frontend:80`, либо использовать готовый Caddy/Nginx/Newt -пример, который уже знает правильную маршрутизацию. +Готовые Caddy, Nginx, Pangolin/Newt и no-proxy профили уже содержат нужную +маршрутизацию. -## Если что-то пошло не так +## Проверка -`migrate` упал → читайте `docker compose logs migrate`. Том БД остался -не тронут, можно откатиться, переключив compose-файл обратно на старый -коммит и подняв старый стек на старом томе `remnawave-tg-shop-db-data` -(пока вы его не удалили). +После переноса: -`backend` не стартует → чаще всего `WEBHOOK_BASE_URL` пуст, либо -`WEBHOOK_SECRET_TOKEN` отличается от того, что Telegram ждёт. Поставьте -свежий секрет в `.env` и перезапустите — Telegram переустановит webhook -автоматически. +```bash +docker compose ps +docker compose logs migrate +docker compose logs -f backend worker frontend +``` -Web App пуст / 502 → проверьте, что `frontend` живёт (`docker compose ps`), -а внешний прокси шлёт на `frontend:80`, а не на старый -`remnawave-tg-shop:8000`. +`migrate` должен завершиться успешно, а `backend`, `worker`, `frontend`, +`postgres` и `redis` должны быть running/healthy. + +Когда убедитесь, что новый stack работает, старые volumes можно удалить вручную: + +```bash +docker volume rm remnawave-tg-shop-db-data +docker volume rm remnawave-tg-shop-caddy-data remnawave-tg-shop-caddy-config 2>/dev/null || true +``` diff --git a/scripts/install.sh b/scripts/install.sh index 26274be..95b7835 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -939,7 +939,7 @@ run_remnashop_migration() { if confirm "Restart backend and worker so setting overrides are reloaded?" 1; then (cd "$TARGET_DIR" && run_compose restart backend worker) || true fi - ok "Legacy migration completed." + ok "Migration completed." } run_target_schema_migrations() { @@ -980,11 +980,10 @@ run_tgshop_volume_migration() { run_tgshop_dsn_migration() { section "Old 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 + if ! confirm "Replace target database with the source dump?" 0; then warn "Migration not applied." return 0 fi @@ -1141,8 +1140,8 @@ main_menu() { banner choose "Main menu" "1" "1|2|3|4|5|6" \ "1. Install new stack" \ - "2. Install new stack and run legacy migration" \ - "3. Run legacy migration only" \ + "2. Install new stack and run migration" \ + "3. Run migration only" \ "4. Download/update deployment files only" \ "5. Validate current stack" \ "6. Exit" diff --git a/scripts/migrate_to_minishop.sh b/scripts/migrate_to_minishop.sh deleted file mode 100644 index b8c8546..0000000 --- a/scripts/migrate_to_minishop.sh +++ /dev/null @@ -1,318 +0,0 @@ -#!/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 "$@" diff --git a/tests/test_install_script.py b/tests/test_install_script.py index 72a3b56..654627e 100644 --- a/tests/test_install_script.py +++ b/tests/test_install_script.py @@ -37,7 +37,8 @@ def test_shell_installer_downloads_raw_files_and_runs_import_in_container(): assert "git clone" not in script 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 + assert "Install new stack and run migration" in script + assert "Run migration only" in script def test_shell_installer_supports_legacy_tgshop_volume_and_dsn_paths(): diff --git a/tests/test_migration_doc_accuracy.py b/tests/test_migration_doc_accuracy.py index 30503c7..5dd79b7 100644 --- a/tests/test_migration_doc_accuracy.py +++ b/tests/test_migration_doc_accuracy.py @@ -1,25 +1,15 @@ -"""Pin facts that ``docs/migrations/remnawave-tg-shop.md`` and -``scripts/migrate_to_minishop.sh`` rely on. - -Both documents are written for a user upgrading from ``remnawave-tg-shop`` -(v2.7.0 era) to the current split-arch ``remnawave-minishop`` (v3.4+). They -make concrete claims about: - -* the set of container names produced by today's compose files; -* the set of volume names produced by today's compose files; -* the eras the migration script is allowed to stop containers from. - -If any of these drift apart from reality, the migration document silently -goes stale. These tests fail loudly instead. -""" +"""Pin facts shared by the migration docs and the unified install wizard.""" import re +import shutil +import subprocess import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] DOC_PATH = REPO_ROOT / "docs" / "migrations" / "remnawave-tg-shop.md" -SCRIPT_PATH = REPO_ROOT / "scripts" / "migrate_to_minishop.sh" +INSTALL_SCRIPT_PATH = REPO_ROOT / "scripts" / "install.sh" +REMOVED_SCRIPT_PATH = REPO_ROOT / "scripts" / "migrate_to_minishop.sh" COMPOSE_FILES = ( REPO_ROOT / "docker-compose.yml", REPO_ROOT / "deploy" / "examples" / "caddy" / "docker-compose.yml", @@ -28,7 +18,6 @@ COMPOSE_FILES = ( REPO_ROOT / "deploy" / "examples" / "no-proxy" / "docker-compose.yml", ) -# Names that the current architecture must produce in at least one compose file. EXPECTED_CONTAINER_NAMES = { "remnawave-minishop-backend", "remnawave-minishop-worker", @@ -52,15 +41,27 @@ def _all_compose_text() -> str: return "\n".join(_read(path) for path in COMPOSE_FILES if path.is_file()) +def _known_containers_from_install_script() -> set[str]: + text = _read(INSTALL_SCRIPT_PATH) + match = re.search(r'^KNOWN_LEGACY_CONTAINERS="([^"]+)"', text, flags=re.MULTILINE) + if not match: + return set() + return set(match.group(1).split()) + + class MigrationDocumentationFactsTests(unittest.TestCase): def setUp(self) -> None: self.doc = _read(DOC_PATH) - self.script = _read(SCRIPT_PATH) + self.script = _read(INSTALL_SCRIPT_PATH) self.compose = _all_compose_text() + def test_unified_install_script_is_the_only_migration_entrypoint(self): + self.assertTrue(INSTALL_SCRIPT_PATH.is_file()) + self.assertFalse(REMOVED_SCRIPT_PATH.exists()) + self.assertIn("scripts/install.sh", self.doc) + self.assertNotIn("migrate_to_minishop.sh", self.doc) + def test_doc_lists_every_running_container_in_current_compose(self): - """The architecture table must reflect what ``docker compose up`` - actually produces today.""" missing = sorted(name for name in EXPECTED_CONTAINER_NAMES if name not in self.doc) self.assertFalse( missing, @@ -77,152 +78,94 @@ class MigrationDocumentationFactsTests(unittest.TestCase): ) def test_doc_warns_about_renamed_telegram_webhook_secret(self): - # This is the single rename most likely to bite a v2.7.0 → HEAD user. self.assertIn("TELEGRAM_WEBHOOK_SECRET", self.doc) self.assertIn("WEBHOOK_SECRET_TOKEN", self.doc) def test_doc_says_webhook_base_url_is_required(self): - # Polling mode was dropped — without WEBHOOK_BASE_URL the bot refuses - # to start. A user migrating from v2.7.0 (where it was optional) must - # be told this explicitly. block = self.doc.lower() self.assertIn("webhook_base_url", block) - # "обязательна" is the marker text in the env-vars table. self.assertIn("обязательн", block) def test_doc_mentions_migrate_one_shot_service(self): - # The migrate sidecar is what makes schema migrations transparent on - # the second-stage upgrade. Don't bury it. - self.assertIn("migrate", self.doc) - # "one-shot" or "разовый" / "однораз" text variants accepted. normalized = self.doc.lower() + self.assertIn("migrate", normalized) self.assertTrue( "one-shot" in normalized or "однораз" in normalized, "migration doc must describe `migrate` as a one-shot service", ) def test_doc_mentions_postgres_host_compose_override_caveat(self): - # Otherwise users follow the sed-fix step blindly and then panic - # because their .env still has the "wrong" hostname under compose. self.assertIn("POSTGRES_HOST", self.doc) - # Russian: "переопределя…" / "перебивает" indicate the override is documented. text = self.doc.lower() self.assertTrue( - "переопредел" in text or "перебивает" in text, + "переопредел" in text or "service name" in text, "migration doc must explain that compose overrides POSTGRES_HOST", ) def test_doc_describes_redis_data_and_shop_data_as_fresh(self): - # We do not migrate redis-data or shop-data — make sure that's said - # so users don't try to copy them from the old stack. text = self.doc.lower() self.assertIn("redis-data", text) self.assertIn("shop-data", text) - # Some phrasing variant must say it's empty / fresh / new on purpose. - self.assertTrue( - any(marker in text for marker in ("пустым", "создаётся пуст", "новые,", "пуст —")) - ) + self.assertTrue(any(marker in text for marker in ("пустым", "создается пуст"))) def test_doc_explains_reverse_proxy_no_longer_single_upstream(self): - # The note "rename remnawave-tg-shop → remnawave-minishop in your - # proxy config" used to be enough; after the split it's wrong. - # Make sure both backend:8080 and frontend:80 are documented. self.assertIn("backend:8080", self.doc) self.assertIn("frontend:80", self.doc) - -def _known_containers_from_script() -> set[str]: - """Parse the literal ``KNOWN_CONTAINERS`` array from the script source. - - The array is defined with ``${OLD_PREFIX}`` / ``${NEW_PREFIX}`` placeholders - that we substitute here. Doing this statically (rather than sourcing the - script in bash) avoids running ``main`` and keeps the test independent of - a bash interpreter being available at runtime. - """ - text = _read(SCRIPT_PATH) - - prefix_match = re.search(r'^OLD_PREFIX="([^"]+)"', text, flags=re.MULTILINE) - new_match = re.search(r'^NEW_PREFIX="([^"]+)"', text, flags=re.MULTILINE) - if not prefix_match or not new_match: - return set() - old_prefix = prefix_match.group(1) - new_prefix = new_match.group(1) - - # ``\n)`` as a boundary: a non-greedy ``.*?\)`` would stop at the first - # close-paren in a comment like ``# (v2.7.0 upstream remnawave-tg-shop)``. - array_match = re.search( - r"KNOWN_CONTAINERS=\((.*?)\n\)", - text, - flags=re.DOTALL, - ) - if not array_match: - return set() - - body = array_match.group(1) - # Strip line comments and quotes, then expand the two placeholders. - names: set[str] = set() - for raw_line in body.splitlines(): - line = raw_line.split("#", 1)[0].strip() - if not line: - continue - for token in re.findall(r'"([^"]+)"', line): - expanded = token.replace("${OLD_PREFIX}", old_prefix).replace( - "${NEW_PREFIX}", new_prefix - ) - names.add(expanded) - return names + def test_doc_mentions_both_supported_tgshop_migration_methods(self): + self.assertIn("Copy old Docker volumes", self.doc) + self.assertIn("Dump from a source PostgreSQL DSN", self.doc) + self.assertIn("pg_dump", self.doc) -class MigrationScriptCoverageTests(unittest.TestCase): +class InstallWizardCoverageTests(unittest.TestCase): def setUp(self) -> None: - self.script = _read(SCRIPT_PATH) - self.known = _known_containers_from_script() + self.script = _read(INSTALL_SCRIPT_PATH) + self.known = _known_containers_from_install_script() def test_known_containers_covers_split_arch(self): - """A re-run on a partially migrated stack must be able to stop the new - containers, otherwise ``docker compose up`` later fails with name - conflicts.""" missing = sorted(EXPECTED_CONTAINER_NAMES - self.known) self.assertFalse( missing, - f"KNOWN_CONTAINERS missing split-arch entries: {missing}\nactual: {sorted(self.known)}", + f"KNOWN_LEGACY_CONTAINERS missing split-arch entries: {missing}", ) - def test_known_containers_still_covers_legacy_eras(self): - # We must also keep stopping the original (v2.7.0) and intermediate - # (v3.1.x – v3.2.x) container names. + def test_known_containers_still_covers_old_eras(self): for legacy in ("remnawave-tg-shop", "remnawave-tg-shop-db", "remnawave-minishop-db"): with self.subTest(container=legacy): self.assertIn(legacy, self.known) - def test_script_is_syntactically_valid_bash(self): - # The script is curl|bash'ed from raw.githubusercontent in the docs, - # so a syntax break is a hard regression. - import shutil - import subprocess + def test_installer_contains_tgshop_volume_and_dsn_paths(self): + self.assertIn("run_tgshop_volume_migration", self.script) + self.assertIn("run_tgshop_dsn_migration", self.script) + self.assertIn("remnawave-tg-shop-db-data", self.script) + self.assertIn("pg_dump --clean --if-exists", self.script) + self.assertIn("run_compose run --rm migrate", self.script) - bash = shutil.which("bash") - if not bash: # pragma: no cover - self.skipTest("bash not available in PATH") + def test_script_is_syntactically_valid_sh_and_bash(self): + sh = shutil.which("sh") + if not sh: # pragma: no cover + self.skipTest("sh not available in PATH") result = subprocess.run( - [bash, "-n", str(SCRIPT_PATH)], + [sh, "-n", str(INSTALL_SCRIPT_PATH)], check=False, capture_output=True, text=True, ) - self.assertEqual( - result.returncode, - 0, - ( - "bash -n flagged migrate_to_minishop.sh:\n" - f"stdout={result.stdout}\nstderr={result.stderr}" - ), - ) + self.assertEqual(result.returncode, 0, result.stderr) + + bash = shutil.which("bash") + if bash: + result = subprocess.run( + [bash, "-n", str(INSTALL_SCRIPT_PATH)], + check=False, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) class DocComposeFileReferencesTests(unittest.TestCase): - """The doc links the user to specific compose files — they must exist.""" - def test_referenced_compose_files_exist(self): doc = _read(DOC_PATH) for relpath in ( @@ -234,40 +177,22 @@ class DocComposeFileReferencesTests(unittest.TestCase): ): with self.subTest(path=relpath): self.assertIn(relpath, doc) - self.assertTrue( - (REPO_ROOT / relpath).is_file(), - f"{relpath} is referenced in migrations/remnawave-tg-shop.md " - "but missing on disk", - ) + self.assertTrue((REPO_ROOT / relpath).is_file()) def test_doc_references_migrator_module_path(self): - # The doc tells users to expect ``backend/db/migrator.py`` migrations - # to apply via the migrate service. If the file moves, the doc lies. doc = _read(DOC_PATH) self.assertIn("backend/db/migrator.py", doc) self.assertTrue((REPO_ROOT / "backend" / "db" / "migrator.py").is_file()) class MigrationFootprintRegexTests(unittest.TestCase): - """Spot-check that what compose actually defines matches what we documented.""" - def test_every_compose_volume_documented(self): - """If a future compose file introduces a new ``remnawave-minishop-*`` - named volume, the migration doc must call out whether it carries data - from the old stack or starts fresh.""" doc = _read(DOC_PATH) compose_text = _all_compose_text() - # Find every named volume of the form ``remnawave-minishop--data``. defined = set(re.findall(r"remnawave-minishop-[\w-]+-data", compose_text)) - # Caddy-only volumes only ship in the caddy compose file but are still - # documented; allow them either way. for volume in defined: with self.subTest(volume=volume): - self.assertIn( - volume, - doc, - f"new volume {volume} is defined in compose but missing from migration doc", - ) + self.assertIn(volume, doc) if __name__ == "__main__": # pragma: no cover