Initial commit
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
"""CLI entry: build locally, transfer images via pussh, rsync config — no registry pulls."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_SCRIPTS = Path(__file__).resolve().parent.parent
|
||||
if str(_SCRIPTS) not in sys.path:
|
||||
sys.path.insert(0, str(_SCRIPTS))
|
||||
|
||||
from deploy.compose_build import ( # noqa: E402
|
||||
ComposeBuildPhase,
|
||||
classify_compose_images,
|
||||
fromchat_image,
|
||||
generate_production_compose,
|
||||
pull_third_party_images,
|
||||
verify_fromchat_images_local,
|
||||
)
|
||||
from deploy.config import load_settings # noqa: E402
|
||||
import deploy.docker_local as docker_local # noqa: E402
|
||||
from deploy.paths import ProjectPaths # noqa: E402
|
||||
from deploy.ssh_auth import SshAuth # noqa: E402
|
||||
from deploy.transfer import DeployTransfer # noqa: E402
|
||||
from deploy.updater import setup_updater_remote # noqa: E402
|
||||
import deploy.ui as ui # noqa: E402
|
||||
from deploy.util import dedupe_preserve, local_docker_image_tags # noqa: E402
|
||||
|
||||
UPDATER_IMAGE = fromchat_image("updater", "latest")
|
||||
|
||||
|
||||
def _sync_caddy_build_files(paths: ProjectPaths, compose_components: list[str]) -> None:
|
||||
if "caddy" not in compose_components or not paths.caddy_build_dir:
|
||||
return
|
||||
if paths.caddyfile_template.is_file():
|
||||
shutil.copy2(paths.caddyfile_template, paths.caddy_build_dir / "Caddyfile")
|
||||
|
||||
|
||||
def _prepare_staging(paths: ProjectPaths, compose_components: list[str], tag: str) -> Path:
|
||||
staging = paths.staging_dir
|
||||
if staging.exists():
|
||||
shutil.rmtree(staging)
|
||||
staging.mkdir(parents=True)
|
||||
(staging / "config").mkdir()
|
||||
|
||||
if compose_components:
|
||||
generate_production_compose(
|
||||
paths, components=compose_components, tag=tag, output=staging / "compose.yml"
|
||||
)
|
||||
(staging / ".fromchat-version").write_text(tag + "\n", encoding="utf-8")
|
||||
|
||||
if paths.livekit_config.is_file():
|
||||
shutil.copy2(paths.livekit_config, staging / "config" / "livekit.yaml")
|
||||
|
||||
if "caddy" in compose_components:
|
||||
caddyfile = staging / "Caddyfile"
|
||||
if paths.caddyfile_template.is_file():
|
||||
shutil.copy2(paths.caddyfile_template, caddyfile)
|
||||
|
||||
if paths.systemd_unit_template.is_file():
|
||||
unit = paths.systemd_unit_template.read_text(encoding="utf-8")
|
||||
if "WorkingDirectory=" not in unit:
|
||||
unit += "\nWorkingDirectory=/opt/fromchat-server\n"
|
||||
(staging / "fromchat.service").write_text(unit, encoding="utf-8")
|
||||
|
||||
return staging
|
||||
|
||||
|
||||
def _load_generated_compose(staging: Path) -> dict:
|
||||
compose_file = staging / "compose.yml"
|
||||
if not compose_file.is_file():
|
||||
return {}
|
||||
env = os.environ.copy()
|
||||
p = subprocess.run(
|
||||
["docker", "compose", "-f", "compose.yml", "config", "--format", "json"],
|
||||
cwd=staging,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
)
|
||||
if p.returncode != 0:
|
||||
ui.warning("docker compose config on staging failed; using raw YAML structure")
|
||||
import yaml
|
||||
|
||||
return yaml.safe_load(compose_file.read_text(encoding="utf-8")) or {}
|
||||
return json.loads(p.stdout)
|
||||
|
||||
|
||||
def _collect_all_pushable(
|
||||
build_phase: ComposeBuildPhase,
|
||||
paths: ProjectPaths,
|
||||
stack: list[str],
|
||||
include_updater: bool,
|
||||
) -> list:
|
||||
pushable = []
|
||||
ui.step("Detecting buildable services")
|
||||
for component in stack:
|
||||
if component == "caddy":
|
||||
ps = build_phase.collect_caddy_pushable()
|
||||
if ps:
|
||||
pushable.append(ps)
|
||||
ui.substep(f"caddy: {ps.service}")
|
||||
continue
|
||||
compose_root = paths.project_root_for(component)
|
||||
if not compose_root:
|
||||
continue
|
||||
services = build_phase.list_services(compose_root)
|
||||
if not services:
|
||||
ui.warning(f"No services in {compose_root}/compose.yml")
|
||||
continue
|
||||
compose_json = build_phase.load_compose_json(compose_root)
|
||||
found = build_phase.collect_pushable(compose_json, services, compose_root)
|
||||
ui.substep(f"{component}: {', '.join(p.service for p in found) or '(none)'}")
|
||||
pushable.extend(found)
|
||||
|
||||
if include_updater:
|
||||
ps = build_phase.collect_updater_pushable()
|
||||
if ps:
|
||||
pushable.append(ps)
|
||||
ui.substep(f"updater: {ps.service}")
|
||||
|
||||
return pushable
|
||||
|
||||
|
||||
def main() -> None:
|
||||
paths = ProjectPaths.from_deploy_package()
|
||||
settings = load_settings(paths, sys.argv)
|
||||
ui.banner()
|
||||
ui.info("Build locally, pull remote images on this PC, transfer via pussh")
|
||||
ui.info(f"Components: {', '.join(settings.components)}")
|
||||
ui.info(f"Image tag: {settings.tag}")
|
||||
if paths.backend_dir:
|
||||
ui.info(f"Backend: {paths.backend_dir}")
|
||||
if paths.web_dir:
|
||||
ui.info(f"Web: {paths.web_dir}")
|
||||
|
||||
creds = SshAuth(settings.server).authenticate()
|
||||
transfer = DeployTransfer(paths)
|
||||
deploy_resolved = transfer.resolve_deploy_path_on_server(settings.server, settings.deploy_path)
|
||||
|
||||
stack = settings.compose_components
|
||||
include_updater = "updater" in settings.components
|
||||
|
||||
docker_local.ensure_daemon()
|
||||
docker_local.ensure_buildx(settings.use_docker_build)
|
||||
|
||||
build_phase = ComposeBuildPhase(
|
||||
paths,
|
||||
tag=settings.tag,
|
||||
platform=settings.platform,
|
||||
use_docker_build=settings.use_docker_build,
|
||||
)
|
||||
|
||||
all_pushable = _collect_all_pushable(
|
||||
build_phase, paths, stack, include_updater=include_updater
|
||||
)
|
||||
|
||||
if not all_pushable:
|
||||
if stack or include_updater:
|
||||
ui.error("No buildable services found for selected components")
|
||||
raise SystemExit(1)
|
||||
|
||||
_sync_caddy_build_files(paths, stack)
|
||||
|
||||
ui.build_banner()
|
||||
to_build = build_phase.plan_builds(all_pushable)
|
||||
build_phase.run_builds(to_build)
|
||||
|
||||
staging = _prepare_staging(paths, stack, settings.tag)
|
||||
|
||||
ui.deploy_banner(settings.server)
|
||||
transfer.ensure_pussh()
|
||||
|
||||
images_to_transfer: list[str] = []
|
||||
|
||||
if stack:
|
||||
generated = _load_generated_compose(staging)
|
||||
services = list((generated.get("services") or {}).keys())
|
||||
local_tags = local_docker_image_tags()
|
||||
fromchat_images, third_party = classify_compose_images(generated, services)
|
||||
|
||||
verify_fromchat_images_local(fromchat_images, local_tags, ui)
|
||||
pull_third_party_images(third_party, platform=settings.platform)
|
||||
|
||||
images_to_transfer.extend(fromchat_images)
|
||||
images_to_transfer.extend(third_party)
|
||||
|
||||
if include_updater:
|
||||
local_tags = local_docker_image_tags()
|
||||
verify_fromchat_images_local([UPDATER_IMAGE], local_tags, ui)
|
||||
images_to_transfer.append(UPDATER_IMAGE)
|
||||
|
||||
images_to_transfer = dedupe_preserve(images_to_transfer)
|
||||
transfer.pussh_images(creds, images_to_transfer)
|
||||
|
||||
if stack:
|
||||
transfer.rsync_staging(creds, settings.deploy_path, staging)
|
||||
transfer.copy_env_prod(creds, settings.deploy_path)
|
||||
if "backend" in stack:
|
||||
deploy_resolved = transfer.sync_firebase_cert(creds, settings.deploy_path)
|
||||
transfer.run_remote_systemd(creds, deploy_resolved)
|
||||
|
||||
if include_updater:
|
||||
setup_updater_remote(
|
||||
creds,
|
||||
settings.deploy_path,
|
||||
deploy_resolved,
|
||||
components=settings.components,
|
||||
paths=paths,
|
||||
git_token=settings.git_token,
|
||||
sudo_password=creds.sudo_password,
|
||||
)
|
||||
|
||||
print()
|
||||
ui.success("Deployment complete!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user