170 lines
5.4 KiB
Python
170 lines
5.4 KiB
Python
"""Remote auto-updater install (matches install.sh setup_updater)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import getpass
|
|
import os
|
|
import shlex
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
from deploy.paths import ProjectPaths
|
|
from deploy.ssh_auth import SshCredentials, ssh_argv, ssh_common_options
|
|
import deploy.ui as ui
|
|
|
|
DEFAULT_BACKEND_REPO = "https://github.com/fromchat-messenger/backend.git"
|
|
DEFAULT_WEB_REPO = "https://github.com/fromchat-messenger/web.git"
|
|
DEFAULT_APP_REPO = "https://github.com/fromchat-messenger/app.git"
|
|
UPDATER_IMAGE = "fromchat/updater:latest"
|
|
|
|
|
|
def _repo_urls(paths: ProjectPaths) -> tuple[str, str, str]:
|
|
backend = os.environ.get("FROMCHAT_BACKEND_REPO", DEFAULT_BACKEND_REPO)
|
|
web = os.environ.get("FROMCHAT_WEB_REPO", DEFAULT_WEB_REPO)
|
|
app = os.environ.get("FROMCHAT_APP_REPO", DEFAULT_APP_REPO)
|
|
if paths.env_file.is_file():
|
|
for line in paths.env_file.read_text(encoding="utf-8").splitlines():
|
|
line = line.strip()
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
key, _, val = line.partition("=")
|
|
key = key.strip()
|
|
val = val.strip().strip('"').strip("'")
|
|
if key == "FROMCHAT_BACKEND_REPO" and val:
|
|
backend = val
|
|
elif key == "FROMCHAT_WEB_REPO" and val:
|
|
web = val
|
|
elif key == "FROMCHAT_APP_REPO" and val:
|
|
app = val
|
|
return backend, web, app
|
|
|
|
|
|
def resolve_git_token(explicit: str | None) -> str:
|
|
if explicit:
|
|
return explicit
|
|
for key in ("GIT_TOKEN", "GITHUB_TOKEN", "RELEASES_TOKEN"):
|
|
val = os.environ.get(key, "").strip()
|
|
if val:
|
|
return val
|
|
ui.step("Git token for the auto-updater (GitHub or Gitea)")
|
|
print(" GitHub: https://github.com/settings/tokens/new?scopes=read:packages,repo")
|
|
print(" Gitea: Settings → Applications → Generate New Token")
|
|
token = getpass.getpass(" Paste token (input hidden): ").strip()
|
|
if not token:
|
|
ui.error("Git token is required when updater is selected.")
|
|
raise SystemExit(1)
|
|
return token
|
|
|
|
|
|
def _updater_compose_source(paths: ProjectPaths) -> Path:
|
|
parent = paths.deployment_root.parent
|
|
candidates = [
|
|
paths.deployment_root.parent / "updater" / "compose.yml",
|
|
parent / "updater" / "compose.yml",
|
|
]
|
|
env_dir = os.environ.get("FROMCHAT_UPDATER_DIR", "")
|
|
if env_dir:
|
|
candidates.insert(0, Path(env_dir).expanduser() / "compose.yml")
|
|
for cand in candidates:
|
|
if cand.is_file():
|
|
return cand
|
|
ui.error(
|
|
"updater/compose.yml not found. Set FROMCHAT_UPDATER_DIR or keep ../updater sibling."
|
|
)
|
|
raise SystemExit(1)
|
|
|
|
|
|
def write_updater_env(
|
|
dest: Path,
|
|
*,
|
|
token: str,
|
|
deploy_path_resolved: str,
|
|
components: list[str],
|
|
paths: ProjectPaths,
|
|
) -> None:
|
|
backend, web, app = _repo_urls(paths)
|
|
components_csv = ",".join(components)
|
|
dest.write_text(
|
|
"\n".join(
|
|
[
|
|
f"GITHUB_TOKEN={token}",
|
|
f"GIT_TOKEN={token}",
|
|
f"BACKEND_REPO={backend}",
|
|
f"WEB_REPO={web}",
|
|
f"DEPLOYMENT_REPO={app}",
|
|
f"COMPOSE_PROJECT_DIR={deploy_path_resolved}",
|
|
f"FROMCHAT_COMPONENTS={components_csv}",
|
|
"CHECK_INTERVAL_SECONDS=60",
|
|
"",
|
|
]
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def setup_updater_remote(
|
|
creds: SshCredentials,
|
|
deploy_path: str,
|
|
deploy_path_resolved: str,
|
|
*,
|
|
components: list[str],
|
|
paths: ProjectPaths,
|
|
git_token: str | None,
|
|
sudo_password: str,
|
|
) -> None:
|
|
ui.step("Setting up auto-updater on server")
|
|
token = resolve_git_token(git_token)
|
|
|
|
staging = paths.staging_dir / "updater"
|
|
if staging.exists():
|
|
shutil.rmtree(staging)
|
|
staging.mkdir(parents=True)
|
|
|
|
compose_src = _updater_compose_source(paths)
|
|
shutil.copy2(compose_src, staging / "compose.yml")
|
|
write_updater_env(
|
|
staging / ".env",
|
|
token=token,
|
|
deploy_path_resolved=deploy_path_resolved,
|
|
components=components,
|
|
paths=paths,
|
|
)
|
|
|
|
remote_updater = f"{deploy_path}/updater"
|
|
ui.substep("Syncing updater/ to server…")
|
|
subprocess.run(
|
|
ssh_argv(creds.server, f"mkdir -p {shlex.quote(remote_updater)}"),
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
rsync = subprocess.run(
|
|
[
|
|
"rsync",
|
|
"-avz",
|
|
"-e",
|
|
"ssh " + " ".join(shlex.quote(o) for o in ssh_common_options()),
|
|
f"{staging}/",
|
|
f"{creds.server}:{remote_updater}/",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if rsync.returncode != 0:
|
|
ui.error("Failed to sync updater directory")
|
|
for line in (rsync.stderr or rsync.stdout or "").splitlines():
|
|
print(f" {line}")
|
|
raise SystemExit(1)
|
|
|
|
ui.substep("Starting updater service…")
|
|
remote_cmd = (
|
|
f"cd {shlex.quote(remote_updater)} && "
|
|
f"COMPOSE_PROJECT_DIR={shlex.quote(deploy_path_resolved)} "
|
|
f"docker compose --env-file .env up -d --wait --timeout 120"
|
|
)
|
|
if subprocess.run(ssh_argv(creds.server, remote_cmd), capture_output=True).returncode != 0:
|
|
ui.error("Failed to start updater on server")
|
|
raise SystemExit(1)
|
|
|
|
ui.success("Updater service started on server")
|