diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml new file mode 100644 index 0000000..c15e20e --- /dev/null +++ b/.github/workflows/deployment.yml @@ -0,0 +1,37 @@ +name: Deployment + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: deployment-${{ github.ref }} + cancel-in-progress: true + +jobs: + deploy: + name: ${{ matrix.image }} / ${{ matrix.method }} / ${{ matrix.engine }} + runs-on: ubuntu-latest + timeout-minutes: 40 + strategy: + fail-fast: false + matrix: + image: ["ubuntu:24.04", "images:debian/12/cloud"] + method: [checkout, bash] + engine: [mtg, mtproxy] + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Initialize LXD + run: | + sudo snap install lxd --channel=5.21/stable || sudo snap refresh lxd --channel=5.21/stable + sudo lxd waitready + sudo lxd init --auto + - name: Install and deploy in a clean guest + run: tests/integration/deploy.sh '${{ matrix.image }}' '${{ matrix.method }}' '${{ matrix.engine }}' diff --git a/README.md b/README.md index 9704206..f71ba17 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,17 @@ server. [MIT licensed](LICENSE). ## Start here +On a Debian/Ubuntu control machine, install the CLI environment into `/opt/mega-tg-proxy-server`: + +```sh +curl -fsSL https://raw.githubusercontent.com/andre487/MegaTgProxyServer/main/install.sh | sudo bash +``` + +The installer asks for confirmation through the terminal, installs dependencies, and prepares +the CLI. Configure inventory and Vault separately; it does not provision servers. +Reruns reuse the existing checkout without updating it. To select a branch or tag for +a fresh installation, use `sudo env MEGATG_REF=TAG bash` in place of `sudo bash`. + Requirements: macOS/Linux, Git, Python 3.12+, [uv](https://docs.astral.sh/uv/), OpenSSH, and SSH access to each server. Use an existing non-root administrator with sudo, or create one with the two-phase `bootstrap` command described in the guides. diff --git a/docs/en/README.md b/docs/en/README.md index 0375437..ee107df 100644 --- a/docs/en/README.md +++ b/docs/en/README.md @@ -12,6 +12,17 @@ ## Setup +On a Debian/Ubuntu control machine, install the CLI environment into `/opt/mega-tg-proxy-server`: + +```sh +curl -fsSL https://raw.githubusercontent.com/andre487/MegaTgProxyServer/main/install.sh | sudo bash +``` + +The installer asks for confirmation through the terminal, installs dependencies, and prepares +the CLI. Configure inventory and Vault separately; it does not provision servers. +Reruns reuse the existing checkout without updating it. To select a branch or tag for +a fresh installation, use `sudo env MEGATG_REF=TAG bash` in place of `sudo bash`. + Install Git, Python 3.12+, uv, and OpenSSH on macOS/Linux. Targets must run Debian/Ubuntu and already have a non-root account with public-key SSH and sudo. Set ansible_user and tg_admin.user to that account; use `bootstrap` below for initial root access. Use --ask-become-pass @@ -320,3 +331,12 @@ Use `--tags cleanup` for cleanup only; check mode never removes resources. Set `tg_links_order` in `tg_proxies.vars` to a list of exact public domains/IPs in the desired order. It applies to `configs`, TXT, stdout and hook exports; `--limit` preserves relative ordering. Unlisted addresses follow in unspecified order. + +Deployment integration tests run in disposable LXD guests on GitHub Actions: Ubuntu 24.04 +and Debian 12, checkout and Bash installation, MTG and official MTProxy. They exercise +bootstrap, apply/verify, plan, private link export, and a second deployment with cleanup. +Run one case on a Linux host with initialized LXD: + +```sh +tests/integration/deploy.sh ubuntu:24.04 bash mtg +``` diff --git a/docs/ru/README.md b/docs/ru/README.md index 8fdb0ea..f39c526 100644 --- a/docs/ru/README.md +++ b/docs/ru/README.md @@ -12,6 +12,17 @@ ## Подготовка +На управляющей Debian/Ubuntu можно установить окружение в `/opt/mega-tg-proxy-server`: + +```sh +curl -fsSL https://raw.githubusercontent.com/andre487/MegaTgProxyServer/main/install.sh | sudo bash +``` + +Установщик запрашивает подтверждение через терминал, устанавливает зависимости и готовит CLI. +Inventory и пароль Vault нужно настроить отдельно; серверы автоматически не настраиваются. +Повторный запуск использует существующий checkout без обновления. Для выбора ветки или тега +при первой установке задайте `MEGATG_REF` через `sudo env MEGATG_REF=TAG bash`. + На управляющей macOS/Linux нужны Git, Python 3.12+, uv и OpenSSH. На Debian/Ubuntu нужен непривилегированный администратор с SSH-ключом и sudo. На новой ноде создайте его командой `bootstrap` (см. ниже). @@ -326,3 +337,12 @@ Backend list и protocol key скачивает Ansible на хосте; startup точные публичные домены/IP из ссылок в нужном порядке. Этот порядок используется для `configs`, TXT, stdout и файла hook; `--limit` сохраняет относительный порядок. Адреса, которых нет в списке, выводятся после перечисленных в произвольном порядке. + +Интеграционные тесты развёртывания запускаются в одноразовых LXD-гостях в GitHub Actions: +Ubuntu 24.04 и Debian 12, установка из checkout и через Bash, MTG и официальный MTProxy. +Проверяются bootstrap, apply/verify, plan, приватный экспорт ссылок и повторная раскатка +с очисткой. Запуск одного сценария на Linux с настроенным LXD: + +```sh +tests/integration/deploy.sh ubuntu:24.04 bash mtg +``` diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..91a943a --- /dev/null +++ b/install.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Keep execution after the complete function definition for curl | bash. +main() { + set -euo pipefail + umask 077 + if [[ ${1:-} == --help ]]; then + echo 'Usage: curl -fsSL https://raw.githubusercontent.com/andre487/MegaTgProxyServer/main/install.sh | sudo bash' + echo 'Interactive Debian/Ubuntu CLI installation into /opt/mega-tg-proxy-server. Requires a terminal.' + echo 'Set MEGATG_REF to select a branch or tag for a fresh checkout (default: main).' + return + fi + if [[ $# -ne 0 ]]; then + echo 'Unexpected arguments; use --help.' >&2 + return 2 + fi + if [[ $EUID -ne 0 ]]; then + echo 'Run as root: curl -fsSL URL | sudo bash' >&2 + return 1 + fi + if [[ ! -r /etc/os-release ]]; then + echo 'Only Debian and Ubuntu are supported.' >&2 + return 1 + fi + . /etc/os-release + case "$ID" in + debian|ubuntu) ;; + *) echo 'Only Debian and Ubuntu are supported.' >&2; return 1 ;; + esac + # stdin contains the script when piped; all prompts must read the terminal. + if ! exec &2 + return 1 + fi + local install_dir=/opt/mega-tg-proxy-server answer + echo "Install dependencies and MegaTgProxyServer into $install_dir, then prepare the CLI environment." + read -r -p 'Continue? [y/N] ' answer + [[ "$answer" == [yY] ]] || return 0 + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl git python3 python3-venv openssh-client sudo + if [[ ! -e "$install_dir" ]]; then + git clone --depth 1 --branch "${MEGATG_REF:-main}" https://github.com/andre487/MegaTgProxyServer.git "$install_dir" + elif [[ ! -f "$install_dir/cli.py" ]]; then + echo "$install_dir already exists without CLI support; move it aside or update it manually." >&2 + return 1 + fi + cd "$install_dir" + if ! command -v uv >/dev/null 2>&1; then + python3 -m venv .bootstrap + .bootstrap/bin/pip install 'uv==0.12.5' + ln -s "$install_dir/.bootstrap/bin/uv" /usr/local/bin/uv + fi + uv sync --frozen --python 3.12 + ./mega-tg-proxy deps + echo "Installed in $install_dir. Configure a private inventory and Vault password as described in docs/en/README.md or docs/ru/README.md." + echo "Then run: cd $install_dir && ./mega-tg-proxy plan --limit HOST" +} + +main "$@" diff --git a/tests/integration/deploy.sh b/tests/integration/deploy.sh new file mode 100755 index 0000000..c8ccd67 --- /dev/null +++ b/tests/integration/deploy.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Runs only in disposable LXD guests; never point this at a production server. +set -euo pipefail +image=${1:?Debian/Ubuntu LXD image required} +method=${2:?checkout or bash required} +engine=${3:?mtg or mtproxy required} +case "$method:$engine" in checkout:mtg|checkout:mtproxy|bash:mtg|bash:mtproxy) ;; *) exit 2 ;; esac +instance="megatg-ci-$RANDOM" +work_dir=$(mktemp -d) +cleanup() { + sudo lxc delete --force "$instance" >/dev/null 2>&1 || true + rm -rf "$work_dir" +} +trap cleanup EXIT +sudo iptables -P FORWARD ACCEPT +sudo ip6tables -P FORWARD ACCEPT +sudo lxc launch "$image" "$instance" -c security.nesting=true -c security.privileged=true +# Copy the working tree, including uncommitted installer changes for local runs. +tar --exclude=.git --exclude=.venv --exclude=.generated --exclude=.hooks \ + --exclude=.secrets --exclude=inventory-path --exclude=inventory.yml \ + --exclude=__pycache__ -czf "$work_dir/source.tar.gz" . +sudo lxc file push "$work_dir/source.tar.gz" "$instance/root/source.tar.gz" +sudo lxc exec "$instance" -- bash -s -- "$method" "$engine" < tests/integration/guest.sh diff --git a/tests/integration/guest.sh b/tests/integration/guest.sh new file mode 100755 index 0000000..c70548e --- /dev/null +++ b/tests/integration/guest.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -euo pipefail +method=${1:?} +engine=${2:?} +export DEBIAN_FRONTEND=noninteractive +# Some CI runners disallow plain HTTP in nested guests. +sed -i 's|http://|https://|g' /etc/apt/sources.list 2>/dev/null || true +sed -i 's|http://|https://|g' /etc/apt/sources.list.d/*.sources 2>/dev/null || true +apt-get update +apt-get install -y ca-certificates git python3 python3-venv openssh-server sudo +mkdir /root/source +tar --no-same-owner -xzf /root/source.tar.gz -C /root/source +cd /root/source +git init -b main +git add . +git -c user.name=CI -c user.email=ci@example.com commit -qm 'Test working tree' +if [[ $method == bash ]]; then + # Exercise a fresh clone without fetching an unrelated main revision. + git config --global url.file:///root/source.insteadOf https://github.com/andre487/MegaTgProxyServer.git + printf 'y\n' | script -qefc 'bash /root/source/install.sh' /dev/null + cd /opt/mega-tg-proxy-server + test -x .bootstrap/bin/uv +else + python3 -m venv .bootstrap + .bootstrap/bin/pip install 'uv==0.12.5' + export PATH="$PWD/.bootstrap/bin:$PATH" + uv sync --frozen --python 3.12 + ./mega-tg-proxy deps +fi +ssh-keygen -q -t ed25519 -N '' -f /root/ci-key +install -d -m 0700 /root/.ssh +cat /root/ci-key.pub >> /root/.ssh/authorized_keys +chmod 0600 /root/.ssh/authorized_keys +systemctl enable --now ssh +ssh-keyscan -H 127.0.0.1 > /root/.ssh/known_hosts +export CI_ENGINE="$engine" +.bootstrap/bin/uv run --frozen python - <<'PY' +import os +from pathlib import Path +from ruamel.yaml import YAML +engine = os.environ['CI_ENGINE'] +host = { + 'ansible_host': '127.0.0.1', 'ansible_user': 'ci-admin', + 'ansible_ssh_private_key_file': '/root/ci-key', + 'ansible_python_interpreter': '/usr/bin/python3', + 'tg_endpoint': 'ci.example.com', 'tg_port': 443, + 'tg_engine': engine, 'tg_mode': 'faketls' if engine == 'mtg' else 'classic', + 'tg_domain': 'www.google.com', 'tg_egress': 'direct', + 'tg_container_name': 'mtg-proxy' if engine == 'mtg' else 'mtproto-proxy', + 'tg_secret': '0123456789abcdef0123456789abcdef', + 'tg_unattended_upgrades': False, + 'tg_admin': {'user': 'ci-admin', 'public_key': Path('/root/ci-key.pub').read_text().strip(), + 'private_key_file': '/root/ci-key', 'bootstrap_user': 'root', + 'bootstrap_auth': 'key'}, +} +YAML().dump({'all': {'children': {'tg_proxies': {'hosts': {'ci': host}}}}}, Path('/root/ci-inventory.yml')) +Path('/root/ci-inventory.yml').chmod(0o600) +PY +cli=(./mega-tg-proxy --inventory /root/ci-inventory.yml --limit ci --no-hooks) +"${cli[@]}" bootstrap +"${cli[@]}" apply --verify +"${cli[@]}" plan +"${cli[@]}" configs --output /root/links.txt +grep -q 'server=ci.example.com' /root/links.txt +test "$(stat -c %a /root/links.txt)" = 600 +# A second deployment also exercises cleanup with an existing container. +"${cli[@]}" apply --verify +docker inspect --format '{{.State.Running}}' "$( [[ $engine == mtg ]] && echo mtg-proxy || echo mtproto-proxy )" | grep -qx true diff --git a/tests/test_installer.py b/tests/test_installer.py new file mode 100644 index 0000000..e672823 --- /dev/null +++ b/tests/test_installer.py @@ -0,0 +1,22 @@ +"""Installer entry points must work when stdin is the downloaded script.""" +from pathlib import Path +import subprocess +import unittest + + +class InstallerTests(unittest.TestCase): + def run_installer(self, *args): + script = Path(__file__).resolve().parents[1] / 'install.sh' + return subprocess.run(['bash', '-s', '--', *args], input=script.read_text(), + text=True, capture_output=True, timeout=10) + + def test_piped_help_needs_neither_root_nor_terminal(self): + result = self.run_installer('--help') + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn('MegaTgProxyServer/main/install.sh', result.stdout) + self.assertIn('MEGATG_REF', result.stdout) + + def test_unknown_argument_fails_before_installation(self): + result = self.run_installer('--unknown') + self.assertEqual(result.returncode, 2) + self.assertIn('Unexpected arguments', result.stderr)