#!/usr/bin/env python3 """ Deploy script for OpenHands Agent Canvas on Komodo Core. Reads docker-compose.yml, ensures the Komodo Stack exists on server-tsj08, updates the Compose configuration, and triggers deployment via Komodo API. """ import os import sys import json import time import urllib.request import urllib.error STACK_NAME = "openhands-agent-canvas" DEFAULT_SERVER = "server-tsj08" def get_komodo_config(): host = os.getenv("KOMODO_HOST", "").strip() key = os.getenv("KOMODO_API_KEY", "").strip() secret = os.getenv("KOMODO_API_SECRET", "").strip() if not (host and key and secret): cfg_path = os.path.expanduser("~/.config/komodo/komodo.cli.toml") if os.path.exists(cfg_path): try: import tomllib with open(cfg_path, "rb") as f: cfg = tomllib.load(f) host = host or cfg.get("host", "http://localhost:9120") key = key or cfg.get("cli_key", "") secret = secret or cfg.get("cli_secret", "") except Exception as e: print(f"[WARN] Failed to parse {cfg_path}: {e}") host = host or "http://localhost:9120" if not (key and secret): print("::error::KOMODO_API_KEY and KOMODO_API_SECRET must be set.") sys.exit(1) return host.rstrip("/"), key, secret def api_call(host, key, secret, endpoint, payload): url = f"{host}{endpoint}" req = urllib.request.Request( url, data=json.dumps(payload).encode("utf-8"), headers={ "Content-Type": "application/json", "x-api-key": key, "x-api-secret": secret, }, method="POST", ) try: with urllib.request.urlopen(req, timeout=120) as resp: data = resp.read().decode("utf-8") return json.loads(data) if data else {} except urllib.error.HTTPError as e: err = e.read().decode("utf-8", errors="replace") raise RuntimeError(f"Komodo API {endpoint} error {e.code}: {err}") from e def main(): host, key, secret = get_komodo_config() print(f"=== Komodo Target: {host} ===") # 1. Resolve server ID servers = api_call(host, key, secret, "/read/ListServers", {}) server_id = None for s in servers: if s.get("name") == DEFAULT_SERVER or s.get("id") == DEFAULT_SERVER: server_id = s.get("id") break if not server_id and servers: server_id = servers[0].get("id") print(f"[INFO] Server '{DEFAULT_SERVER}' not found by name, using '{servers[0].get('name')}' ({server_id})") elif server_id: print(f"[INFO] Target server '{DEFAULT_SERVER}' resolved to id: {server_id}") else: print(f"::error::No active servers found in Komodo.") sys.exit(1) # 2. Read docker-compose.yml compose_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "docker-compose.yml") if not os.path.exists(compose_path): compose_path = "docker-compose.yml" with open(compose_path, "r", encoding="utf-8") as f: compose_content = f.read() # 3. Check or Create Stack stacks = api_call(host, key, secret, "/read/ListStacks", {}) existing_stack = None for stk in stacks: if stk.get("name") == STACK_NAME: existing_stack = stk break stack_config = { "server_id": server_id, "auto_pull": True, "run_build": False, "auto_update": True, "auto_update_all_services": True, "file_contents": compose_content, "environment": os.getenv("STACK_ENVIRONMENT", ""), } if not existing_stack: print(f"[INFO] Creating Komodo Stack '{STACK_NAME}'...") res = api_call( host, key, secret, "/write/CreateStack", {"name": STACK_NAME, "config": stack_config} ) print(f"[INFO] Created Stack successfully: {res.get('name', STACK_NAME)}") else: print(f"[INFO] Updating Komodo Stack '{STACK_NAME}' compose definition...") api_call( host, key, secret, "/write/UpdateStack", {"id": existing_stack["id"], "config": stack_config} ) print(f"[INFO] Updated Stack configuration.") # 4. Trigger Deploy print(f"[INFO] Executing DeployStack for '{STACK_NAME}'...") deploy_res = api_call( host, key, secret, "/execute/DeployStack", {"stack": STACK_NAME, "services": [], "stop_time": None} ) print(f"[SUCCESS] Deploy execution dispatched: {deploy_res}") if __name__ == "__main__": main()