feat: initial OpenHands Agent Canvas stack with Docker-in-Docker and automatic Komodo deployment
Deploy OpenHands Agent Canvas / Deploy to Komodo (push) Failing after 14s
Deploy OpenHands Agent Canvas / Deploy to Komodo (push) Failing after 14s
This commit is contained in:
commit
5dc53d1a66
6 files changed
+270
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
# OpenHands Agent Canvas Configuration
|
||||||
|
OPENHANDS_PORT=18000
|
||||||
|
|
||||||
|
# LLM Providers (configurabili qui oppure direttamente da interfaccia web)
|
||||||
|
OPENAI_API_KEY=
|
||||||
|
ANTHROPIC_API_KEY=
|
||||||
|
GEMINI_API_KEY=
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
name: Deploy OpenHands Agent Canvas
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy to Komodo
|
||||||
|
runs-on: hp
|
||||||
|
env:
|
||||||
|
KOMODO_HOST: ${{ secrets.KOMODO_HOST || 'http://localhost:9120' }}
|
||||||
|
KOMODO_API_KEY: ${{ secrets.KOMODO_API_KEY }}
|
||||||
|
KOMODO_API_SECRET: ${{ secrets.KOMODO_API_SECRET }}
|
||||||
|
steps:
|
||||||
|
- name: Install System Prerequisites
|
||||||
|
run: |
|
||||||
|
if command -v apk >/dev/null 2>&1; then
|
||||||
|
apk update
|
||||||
|
apk add --no-cache python3 curl bash
|
||||||
|
fi
|
||||||
|
python3 --version
|
||||||
|
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy Stack via Komodo
|
||||||
|
run: |
|
||||||
|
echo "=== Deploying OpenHands Agent Canvas ==="
|
||||||
|
python3 scripts/deploy.py
|
||||||
|
|
||||||
|
- name: Verify Health
|
||||||
|
run: |
|
||||||
|
echo "=== Checking health endpoint on port 18000 ==="
|
||||||
|
sleep 10
|
||||||
|
for i in 1 2 3 4 5 6; do
|
||||||
|
if curl -fsSL -o /dev/null "http://127.0.0.1:18000/" 2>/dev/null || curl -fsSL -o /dev/null "http://127.0.0.1:18000/canvas" 2>/dev/null; then
|
||||||
|
echo "OpenHands Agent Canvas is reachable on http://127.0.0.1:18000/ !"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Waiting for container startup (attempt $i/6)..."
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
echo "Container is deploying in background. Check status in Komodo Web UI."
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
|
||||||
|
# Local runtime data
|
||||||
|
data/
|
||||||
|
projects/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Python cache
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# OpenHands Agent Canvas Stack
|
||||||
|
|
||||||
|
Deploy automatico di **OpenHands Agent Canvas** con runtime Docker (Docker-in-Docker / DooD tramite socket Docker host) gestito via Gitea Actions e Komodo Core su `server-tsj08`.
|
||||||
|
|
||||||
|
## 🚀 Architettura
|
||||||
|
|
||||||
|
- **Immagine Base:** `ghcr.io/openhands/agent-canvas:latest`
|
||||||
|
- **Runtime Isolato per Conversazione:** `OH_CONVERSATION_RUNTIME=docker` con immagine `ghcr.io/openhands/agent-server:latest-python`
|
||||||
|
- **Socket Docker:** `/var/run/docker.sock` montato nel container per consentire lo spawn di runtime isolati per ciascun task/conversazione
|
||||||
|
- **Rete:** `scoobydoo` (external network di Komodo)
|
||||||
|
- **Porta Host:** `18000` (mappata su container `8000`)
|
||||||
|
- **Web UI URL:** `http://<IP-SERVER>:18000/` o `http://<IP-SERVER>:18000/canvas`
|
||||||
|
|
||||||
|
## 🔄 CI/CD & Deploy Automatico
|
||||||
|
|
||||||
|
Ad ogni push sui branch `main` o `master`:
|
||||||
|
1. Il workflow `.gitea/workflows/deploy.yml` viene attivato sul runner `hp`.
|
||||||
|
2. Esegue `scripts/deploy.py`, che dialoga con Komodo Core API (`http://localhost:9120`).
|
||||||
|
3. Aggiorna la configurazione dello stack `openhands-agent-canvas` e avvia il deploy del container.
|
||||||
|
4. Esegue un test di raggiungibilità HTTP sulla porta `18000`.
|
||||||
|
|
||||||
|
## 🔐 Configurazione Segreti
|
||||||
|
|
||||||
|
I seguenti segreti sono configurati nei Secrets del repository Gitea:
|
||||||
|
- `KOMODO_HOST`
|
||||||
|
- `KOMODO_API_KEY`
|
||||||
|
- `KOMODO_API_SECRET`
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
agent-canvas:
|
||||||
|
image: ghcr.io/openhands/agent-canvas:latest
|
||||||
|
container_name: openhands-agent-canvas
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${OPENHANDS_PORT:-18000}:8000"
|
||||||
|
environment:
|
||||||
|
- AGENT_CANVAS_ALLOW_LAN_SESSION_KEY=true
|
||||||
|
# Modalita container runtime per conversazione / sandbox Docker
|
||||||
|
- OH_CONVERSATION_RUNTIME=docker
|
||||||
|
- OH_CONVERSATION_IMAGE=ghcr.io/openhands/agent-server:latest-python
|
||||||
|
# LLM Provider credentials
|
||||||
|
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
||||||
|
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
||||||
|
- GEMINI_API_KEY=${GEMINI_API_KEY:-}
|
||||||
|
volumes:
|
||||||
|
# Docker socket per instanziare i runtime effimeri dei container
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
# Dati applicativi e sessioni
|
||||||
|
- openhands-data:/home/openhands/.openhands
|
||||||
|
# Workspace montato per i progetti
|
||||||
|
- openhands-projects:/projects
|
||||||
|
labels:
|
||||||
|
npm.proxy.domains: "openhands.nickmalik.dev"
|
||||||
|
npm.proxy.host: "openhands-agent-canvas"
|
||||||
|
npm.proxy.port: "8000"
|
||||||
|
npm.proxy.scheme: "http"
|
||||||
|
npm.proxy.ssl.force: "true"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
openhands-data:
|
||||||
|
openhands-projects:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: scoobydoo
|
||||||
|
external: true
|
||||||
Executable
+137
@@ -0,0 +1,137 @@
|
|||||||
|
#!/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()
|
||||||
Reference in new issue
Block a user