From cd48d99c65d63f95a1acf1b4528feafbb465ab8a Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Mon, 3 Aug 2026 02:05:19 +0200 Subject: [PATCH] deploy.py: --initial Modus fuer vollautomatische Erstinstallation ueber Coolify API --- scripts/deploy.py | 308 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 307 insertions(+), 1 deletion(-) diff --git a/scripts/deploy.py b/scripts/deploy.py index 0a7c717..e1fc24f 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -679,6 +679,306 @@ def verify_only() -> int: # ─── CLI ─────────────────────────────────────────────────────────────── +# ─── Initial deployment: create all Coolify resources from scratch ────────── + +# PostgreSQL Compose (pgvector for embeddings) +POSTGRES_COMPOSE = """\ +services: + postgres: + image: pgvector/pgvector:pg16 + restart: unless-stopped + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME} + PGDATA: /var/lib/postgresql/data/pgdata + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U ${DB_USER} -d ${DB_NAME}'] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s +volumes: + pgdata: +""" + +# Redis Compose +REDIS_COMPOSE = """\ +services: + redis: + image: redis:7-alpine + restart: unless-stopped + command: redis-server --requirepass ${REDIS_PASSWORD} + volumes: + - redisdata:/data + healthcheck: + test: ['CMD-SHELL', 'redis-cli ping || exit 1'] + interval: 10s + timeout: 5s + retries: 5 +volumes: + redisdata: +""" + +# ENV variables for PostgreSQL service +POSTGRES_ENVS = [ + {"key": "DB_USER", "value": "crm_user"}, + {"key": "DB_PASSWORD", "value": "4B6X2wlfbIx-PyaG8kGutsatdLbjdBUI"}, + {"key": "DB_NAME", "value": "crm_db"}, +] + +# ENV variables for Redis service +REDIS_ENVS = [ + {"key": "REDIS_PASSWORD", "value": "lAjCaTf3XFP5XSaPJ1HElgLAJhQQswLT"}, +] + +# API Application configuration +API_GIT_REPO = os.environ.get("API_GIT_REPO", "https://forgejo.media-on.de/Leopoldadmin/leocrm.git") +API_GIT_BRANCH = os.environ.get("API_GIT_BRANCH", "main") + +# API ENV variables +API_ENVS = [ + {"key": "DATABASE_URL", "value": "postgresql+asyncpg://crm_api:4B6X2wlfbIx-PyaG8kGutsatdLbjdBUI@crm-postgres:5432/crm_db"}, + {"key": "AUTH_DATABASE_URL", "value": "postgresql+asyncpg://crm_auth:4B6X2wlfbIx-PyaG8kGutsatdLbjdBUI@crm-postgres:5432/crm_db"}, + {"key": "WORKER_DATABASE_URL", "value": "postgresql+asyncpg://crm_worker:4B6X2wlfbIx-PyaG8kGutsatdLbjdBUI@crm-postgres:5432/crm_db"}, + {"key": "MIGRATION_DATABASE_URL", "value": "postgresql+asyncpg://crm_user:4B6X2wlfbIx-PyaG8kGutsatdLbjdBUI@crm-postgres:5432/crm_db"}, + {"key": "REDIS_URL", "value": "redis://default:lAjCaTf3XFP5XSaPJ1HElgLAJhQQswLT@crm-redis:6379/0"}, + {"key": "SECRET_KEY", "value": "vVdAnvyc-ob4myE5D1rAYn-SovzoBfQLP1z4wmWteTmFPV_lveCGIn2upNoiP590"}, + {"key": "ENVIRONMENT", "value": "production"}, + {"key": "STORAGE_PATH", "value": "/data/storage"}, + {"key": "FRONTEND_URL", "value": "https://crm.media-on.de"}, + {"key": "CORS_ORIGINS", "value": "https://crm.media-on.de"}, + {"key": "SESSION_COOKIE_SECURE", "value": "true"}, + {"key": "LOG_LEVEL", "value": "INFO"}, +] + + +def create_service(client: CoolifyClient, project_uuid: str, environment_name: str, + server_uuid: str, compose_raw: str, name: str) -> str | None: + """Create a Coolify service from a docker-compose definition. + Returns the service UUID or None on failure.""" + import base64 + encoded = base64.b64encode(compose_raw.encode()).decode() + try: + resp = httpx.post( + f"{client.base_url}/api/v1/services", + headers=client.headers, + json={ + "project_uuid": project_uuid, + "environment_name": environment_name, + "server_uuid": server_uuid, + "docker_compose_raw": encoded, + "name": name, + }, + timeout=30, + ) + if resp.status_code in (200, 201): + data = resp.json() + return data.get("uuid") + print(f" Error creating service {name}: {resp.status_code} {resp.text[:200]}") + return None + except Exception as e: + print(f" Error creating service {name}: {e}") + return None + + +def set_service_envs(client: CoolifyClient, service_uuid: str, envs: list[dict]) -> None: + """Set ENV variables for a service via Coolify API.""" + for env in envs: + resp = httpx.post( + f"{client.base_url}/api/v1/services/{service_uuid}/envs", + headers=client.headers, + json=env, + timeout=30, + ) + if resp.status_code == 409: + resp = httpx.patch( + f"{client.base_url}/api/v1/services/{service_uuid}/envs", + headers=client.headers, + json=env, + timeout=30, + ) + if resp.status_code not in (200, 201): + print(f" Warning: could not set ENV {env['key']} ({resp.status_code})") + + +def create_api_application(client: CoolifyClient, project_uuid: str, + environment_name: str, server_uuid: str) -> str | None: + """Create the API application from a Git repository. + Returns the application UUID or None on failure.""" + try: + resp = httpx.post( + f"{client.base_url}/api/v1/applications", + headers=client.headers, + json={ + "project_uuid": project_uuid, + "environment_name": environment_name, + "server_uuid": server_uuid, + "git_repository": API_GIT_REPO, + "git_branch": API_GIT_BRANCH, + "build_pack": "dockerfile", + "name": "leocrm-api", + }, + timeout=30, + ) + if resp.status_code in (200, 201): + data = resp.json() + return data.get("uuid") + print(f" Error creating API application: {resp.status_code} {resp.text[:300]}") + return None + except Exception as e: + print(f" Error creating API application: {e}") + return None + + +def deploy_initial() -> int: + """Initial deployment: create all Coolify resources from scratch. + + Creates: + 1. PostgreSQL service (pgvector/pgvector:pg16) + 2. Redis service (redis:7-alpine) + 3. API application (from Git repo) + 4. Worker service (same image as API) + 5. Sets all ENV variables + 6. Deploys API (builds image + runs migrations) + 7. Deploys Worker + """ + print(f"\n{'='*60}") + print(" LeoCRM Initial Deployment") + print(f"{'='*60}\n") + + if not COOLIFY_TOKEN: + print("ERROR: COOLIFY_API_TOKEN not set") + return 2 + + client = CoolifyClient(COOLIFY_BASE_URL, COOLIFY_TOKEN) + steps: list[tuple[str, StepResult]] = [] + + # Get project, environment, server + PROJECT_UUID = os.environ.get("COOLIFY_PROJECT_UUID", "damkcrjuy4cjofo954ahmooe") + ENVIRONMENT_NAME = os.environ.get("COOLIFY_ENVIRONMENT", "production") + SERVER_UUID = os.environ.get("COOLIFY_SERVER_UUID", "lw80w8scs444gwcw084s00s4") + + # Step 1: Create PostgreSQL service + print("\n[1/7] Creating PostgreSQL service...") + pg_uuid = create_service(client, PROJECT_UUID, ENVIRONMENT_NAME, SERVER_UUID, + POSTGRES_COMPOSE, "crm-postgres") + if pg_uuid: + print(f" PostgreSQL service created: {pg_uuid[:12]}") + set_service_envs(client, pg_uuid, POSTGRES_ENVS) + # Deploy to start the container + client.deploy_application(pg_uuid) + time.sleep(10) + steps.append(("PostgreSQL service", StepResult(True, f"Created: {pg_uuid[:12]}"))) + else: + steps.append(("PostgreSQL service", StepResult(False, "Failed to create"))) + print_summary(steps) + return 1 + _print_result(steps[-1][1]) + + # Step 2: Create Redis service + print("\n[2/7] Creating Redis service...") + redis_uuid = create_service(client, PROJECT_UUID, ENVIRONMENT_NAME, SERVER_UUID, + REDIS_COMPOSE, "crm-redis") + if redis_uuid: + print(f" Redis service created: {redis_uuid[:12]}") + set_service_envs(client, redis_uuid, REDIS_ENVS) + client.deploy_application(redis_uuid) + time.sleep(10) + steps.append(("Redis service", StepResult(True, f"Created: {redis_uuid[:12]}"))) + else: + steps.append(("Redis service", StepResult(False, "Failed to create"))) + print_summary(steps) + return 1 + _print_result(steps[-1][1]) + + # Step 3: Create API application + print("\n[3/7] Creating API application...") + api_uuid = create_api_application(client, PROJECT_UUID, ENVIRONMENT_NAME, SERVER_UUID) + if api_uuid: + print(f" API application created: {api_uuid[:12]}") + # Set API ENV variables + for env in API_ENVS: + resp = httpx.post( + f"{client.base_url}/api/v1/applications/{api_uuid}/envs", + headers=client.headers, + json=env, + timeout=30, + ) + if resp.status_code == 409: + resp = httpx.patch( + f"{client.base_url}/api/v1/applications/{api_uuid}/envs", + headers=client.headers, + json=env, + timeout=30, + ) + steps.append(("API application", StepResult(True, f"Created: {api_uuid[:12]}"))) + else: + steps.append(("API application", StepResult(False, "Failed to create"))) + print_summary(steps) + return 1 + _print_result(steps[-1][1]) + + # Step 4: Create Worker service + print("\n[4/7] Creating Worker service...") + worker_uuid = create_service(client, PROJECT_UUID, ENVIRONMENT_NAME, SERVER_UUID, + WORKER_COMPOSE_YAML, "leocrm-worker") + if worker_uuid: + print(f" Worker service created: {worker_uuid[:12]}") + # Set connect_to_docker_network + httpx.patch( + f"{client.base_url}/api/v1/services/{worker_uuid}", + headers=client.headers, + json={"connect_to_docker_network": True}, + timeout=30, + ) + set_service_envs(client, worker_uuid, WORKER_ENVS) + steps.append(("Worker service", StepResult(True, f"Created: {worker_uuid[:12]}"))) + else: + steps.append(("Worker service", StepResult(False, "Failed to create"))) + print_summary(steps) + return 1 + _print_result(steps[-1][1]) + + # Step 5: Deploy API (builds image + runs migrations via prestart.sh) + print("\n[5/7] Deploying API (build + migrations)...") + r = deploy_api(client, skip_build=False) + steps.append(("API deploy", r)) + _print_result(r) + if not r.success: + print_summary(steps) + return 1 + + # Step 6: Deploy Worker + print("\n[6/7] Deploying Worker...") + # Tag the latest API image as :latest + tag_code, tag_output = ssh_run( + 'docker images --format "{{.Repository}}:{{.Tag}}" | ' + 'grep "^stvabl4vaqru7jclx4ittzr3:" | grep -v latest | head -1 | ' + 'xargs -I{} docker tag {} stvabl4vaqru7jclx4ittzr3:latest' + ) + # Deploy worker + result = client.deploy_application(worker_uuid) + deploy_uuid = _extract_deploy_uuid(result) + if deploy_uuid: + dep_result = _wait_deployment(client, deploy_uuid, timeout=120) + steps.append(("Worker deploy", dep_result)) + else: + wr = _wait_service_healthy(client, worker_uuid, timeout=120) + steps.append(("Worker deploy", wr)) + _print_result(steps[-1][1]) + + # Step 7: Verification + print("\n[7/7] Running verification...") + verify_results = run_verification(client, check_worker=True) + steps.extend(verify_results) + + all_ok = print_summary(steps) + return 0 if all_ok else 1 + + def main() -> None: parser = argparse.ArgumentParser( description="LeoCRM automated deployment script (Coolify API only)", @@ -699,13 +999,19 @@ Examples: "--worker-only", action="store_true", help="Only deploy the worker service", ) + parser.add_argument( + "--initial", action="store_true", + help="Initial deployment: create all Coolify resources from scratch", + ) parser.add_argument( "--verify-only", action="store_true", help="Only run verification checks (no deployment)", ) args = parser.parse_args() - if args.verify_only: + if args.initial: + sys.exit(deploy_initial()) + elif args.verify_only: sys.exit(verify_only()) elif args.worker_only: sys.exit(deploy_worker_only(skip_build=args.skip_build))