deploy.py: --initial Modus fuer vollautomatische Erstinstallation ueber Coolify API
This commit is contained in:
+307
-1
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user