"""Deploy a build with Komodo. Run by ci.yml after the images are pushed. deploy.py A vX.Y.Z tag deploys X.Y.Z to prod: the version is committed to t5de/t5de-infra (stacks//prod.env), then Komodo deploys -prod from it. Pushes to main deploy -staging, which runs the :main image. Env: KOMODO_API_KEY and KOMODO_API_SECRET (a Komodo service user that can only deploy), INFRA_TOKEN (writes t5de/t5de-infra; releases only), KOMODO_URL (default http://10.10.10.239:9120). """ import base64 import json import os import re import sys import time import urllib.error import urllib.request KOMODO_URL = os.environ.get("KOMODO_URL") or "http://10.10.10.239:9120" GITEA_URL = os.environ.get("GITHUB_SERVER_URL", "https://git.t5de.dev").rstrip("/") INFRA_REPO = "t5de/t5de-infra" RELEASE_TAG = re.compile(r"v(\d+\.\d+\.\d+)") def call(method: str, url: str, body: dict | None = None, headers: dict | None = None) -> dict: data = json.dumps(body).encode() if body is not None else None req = urllib.request.Request(url, data=data, method=method, headers={"Content-Type": "application/json", **(headers or {})}) try: with urllib.request.urlopen(req, timeout=60) as resp: return json.loads(resp.read() or b"{}") except urllib.error.HTTPError as e: sys.exit(f"{method} {url}: {e.code} {e.read().decode(errors='replace')}") def komodo(kind: str, type_: str, params: dict) -> dict: keys = {"X-Api-Key": os.environ["KOMODO_API_KEY"], "X-Api-Secret": os.environ["KOMODO_API_SECRET"]} return call("POST", f"{KOMODO_URL}/{kind}", {"type": type_, "params": params}, keys) def pin_version(infra_dir: str, variable: str, version: str) -> None: """Set = in the infra repo's prod.env, committing only if it changed.""" path = f"stacks/{infra_dir}/prod.env" url = f"{GITEA_URL}/api/v1/repos/{INFRA_REPO}/contents/{path}" auth = {"Authorization": f"token {os.environ['INFRA_TOKEN']}"} current = call("GET", f"{url}?ref=main", headers=auth) text = base64.b64decode(current["content"]).decode() updated, n = re.subn(rf"(?m)^{variable}=.*$", f"{variable}={version}", text) if n != 1: sys.exit(f"{path}: expected one {variable}= line, found {n}") if updated == text: print(f"{path} already has {variable}={version}") return release_url = f"{GITEA_URL}/{os.environ.get('GITHUB_REPOSITORY', '')}/releases/tag/v{version}" call("PUT", url, { "branch": "main", "sha": current["sha"], "content": base64.b64encode(updated.encode()).decode(), "message": f"{infra_dir} {version} to prod\n\nFrom {release_url}", }, auth) print(f"Committed {variable}={version} to {INFRA_REPO}/{path}") def deploy(stack: str) -> None: update = komodo("execute", "DeployStack", {"stack": stack}) update_id = update["_id"]["$oid"] print(f"Deploying {stack} (Komodo update {update_id})") deadline = time.time() + 15 * 60 while time.time() < deadline: update = komodo("read", "GetUpdate", {"id": update_id}) if update.get("status") == "Complete": for log in update.get("logs", []): print(f"--- {log.get('stage')}: {'ok' if log.get('success') else 'FAILED'}") if not log.get("success"): print(log.get("stdout", ""), log.get("stderr", ""), sep="\n") if not update.get("success"): sys.exit(f"{stack}: deploy failed") print(f"{stack}: deployed") return time.sleep(5) sys.exit(f"{stack}: deploy didn't finish in 15 minutes") def main() -> None: stack, infra_dir, variable = sys.argv[1:4] ref_name = os.environ.get("GITHUB_REF_NAME", "") release = os.environ.get("GITHUB_REF_TYPE") == "tag" and RELEASE_TAG.fullmatch(ref_name) if release: pin_version(infra_dir, variable, release.group(1)) deploy(f"{stack}-prod") else: deploy(f"{stack}-staging") if __name__ == "__main__": main()