Deploy Tabularium on ces-control with verified image reuse and persistent caches
Verify and deploy Tabularium / verify-and-deploy (push) Waiting to run
Verify and deploy Tabularium / verify-and-deploy (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""Deploy a verified image, never rebuild during deployment or rollback."""
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import urllib.request
|
||||
from ci import APP, ROOT, fingerprint, run
|
||||
from smoke import verify
|
||||
|
||||
DEPLOY_ROOT = Path("/opt/endorphin-tabularium-docs")
|
||||
CONFIG = Path("/etc/endorphin-tabularium-docs/production.env")
|
||||
|
||||
|
||||
def latest_main() -> str:
|
||||
base = os.environ["GITEA_SERVER_URL"].rstrip("/")
|
||||
if base != "https://gitea.entroia.com":
|
||||
raise ValueError("Unexpected Gitea API origin")
|
||||
request = urllib.request.Request(base + "/api/v1/repos/Zayden-Jung/endorphin-tabularium-docs/branches/main",
|
||||
headers={"Authorization": "token " + os.environ["GITEA_TOKEN"]})
|
||||
with urllib.request.urlopen(request, timeout=25) as response:
|
||||
return json.load(response)["commit"]["id"]
|
||||
|
||||
|
||||
def compose(release: Path, *args: str) -> None:
|
||||
run("docker", "compose", "-p", APP, "--env-file", str(CONFIG),
|
||||
"--env-file", str(release / "release.env"), "-f", str(release / "compose.yml"), *args)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if os.geteuid() != 0:
|
||||
raise RuntimeError("Deployment requires root on ces-control")
|
||||
sha = os.environ["TABULARIUM_RELEASE_SHA"]
|
||||
if not re.fullmatch(r"[0-9a-f]{40}", sha) or sha != run("git", "rev-parse", "HEAD", capture=True):
|
||||
raise ValueError("Release SHA does not match checkout")
|
||||
DEPLOY_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
with (DEPLOY_ROOT / ".deploy.lock").open("w") as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX)
|
||||
if latest_main() != sha:
|
||||
print("Skipping stale release: main has advanced", flush=True)
|
||||
return
|
||||
expected = f"{APP}:verified-main-{fingerprint()}"
|
||||
actual = (ROOT / ".artifacts/verified-image").read_text().strip()
|
||||
if actual != expected:
|
||||
raise RuntimeError("Verified image does not match current source")
|
||||
run("docker", "image", "inspect", actual, capture=True)
|
||||
CONFIG.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not CONFIG.exists():
|
||||
CONFIG.write_text("TABULARIUM_BIND=0.0.0.0\nTABULARIUM_PORT=18132\n")
|
||||
CONFIG.chmod(0o600)
|
||||
settings = dict(line.split("=", 1) for line in CONFIG.read_text().splitlines()
|
||||
if line and not line.startswith("#"))
|
||||
port = int(settings.get("TABULARIUM_PORT", "18132"))
|
||||
if not 1024 <= port <= 65535:
|
||||
raise ValueError("Invalid port")
|
||||
current = DEPLOY_ROOT / "current"
|
||||
previous = current.resolve() if current.is_symlink() else None
|
||||
release = DEPLOY_ROOT / "releases" / sha
|
||||
release.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(ROOT / "docker-compose.ces-control.yml", release / "compose.yml")
|
||||
(release / "release.env").write_text(f"TABULARIUM_IMAGE={actual}\n")
|
||||
try:
|
||||
compose(release, "up", "-d", "--no-build", "--wait", "--wait-timeout", "120")
|
||||
verify(f"http://127.0.0.1:{port}")
|
||||
except Exception:
|
||||
if previous is not None:
|
||||
print("Deployment failed; restoring previous verified release", flush=True)
|
||||
compose(previous, "up", "-d", "--no-build", "--wait", "--wait-timeout", "120")
|
||||
else:
|
||||
compose(release, "stop", "docs")
|
||||
raise
|
||||
pending = DEPLOY_ROOT / "current.next"
|
||||
if pending.is_symlink():
|
||||
pending.unlink()
|
||||
pending.symlink_to(release)
|
||||
pending.replace(current)
|
||||
compose(release, "ps")
|
||||
print(f"Deployed release={sha} image={actual} port={port}", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user