Repository navigation
Expand file tree
/
Copy pathsetup-coolify-test.sh
More file actions
executable file
·413 lines (361 loc) · 14.7 KB
/
Copy pathsetup-coolify-test.sh
File metadata and controls
executable file
·413 lines (361 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
#!/usr/bin/env bash
# setup-coolify-test.sh - Automated Coolify test instance setup
#
# Registers a user via Playwright (required for Laravel bootstrap),
# enables the API, generates a token, fixes the SSH key, and
# optionally updates GitHub secrets for CI acceptance tests.
#
# Prerequisites:
# - Coolify containers running (coolify, coolify-db, coolify-redis)
# - openssh-server installed on host (macOS: enable Remote Login)
# - ~/coolify-data/ssh owned by UID 9999
# - Passwordless sudo for current user (only needed for chown 9999)
# - Python 3 with venv support (used only when bootstrapping the first user; Playwright is auto-installed if missing)
#
# Usage:
# ./scripts/setup-coolify-test.sh # setup only
# ./scripts/setup-coolify-test.sh --secrets # setup + update GitHub secrets
set -euo pipefail
COOLIFY_ENDPOINT="http://localhost:8000"
COOLIFY_DATA_DIR="${COOLIFY_DATA_DIR:-$HOME/coolify-data}"
REPO="coolify-terraform/terraform-provider-coolify"
UPDATE_SECRETS=false
if [[ "${1:-}" == "--secrets" ]]; then
UPDATE_SECRETS=true
fi
# --- Helpers ---
psql_exec() {
docker exec coolify-db psql -U coolify -d coolify -t -c "$1" 2>/dev/null | tr -d ' \n'
}
log() { echo "--- $1"; }
# GNU timeout on Linux. Python kills the child process group when timeout
# is missing, so the first macOS bootstrap cannot hang on Chromium install.
run_limited() {
local secs="$1"
shift
if command -v timeout >/dev/null 2>&1; then
timeout --foreground "$secs" "$@"
return
fi
if ! command -v python3 >/dev/null 2>&1; then
echo "FAIL: need timeout or python3 to cap: $*" >&2
return 1
fi
python3 - "$secs" "$@" <<'PY'
import os
import signal
import subprocess
import sys
secs = int(sys.argv[1])
proc = subprocess.Popen(sys.argv[2:], start_new_session=True)
try:
code = proc.wait(timeout=secs)
except subprocess.TimeoutExpired:
os.killpg(proc.pid, signal.SIGTERM)
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
os.killpg(proc.pid, signal.SIGKILL)
proc.wait()
raise SystemExit(124)
raise SystemExit(code)
PY
}
# --- Preflight ---
log "Checking Coolify containers"
for svc in coolify coolify-db coolify-redis; do
if ! docker ps --format '{{.Names}}' | grep -q "^${svc}$"; then
echo "ERROR: Container '$svc' is not running" >&2
exit 1
fi
done
# Wait for Coolify web to be ready
log "Waiting for Coolify web interface"
for i in $(seq 1 24); do
# --max-time so one accepted connection cannot freeze the attempt counter.
if curl -s --max-time 10 -o /dev/null -w "%{http_code}" "$COOLIFY_ENDPOINT/register" 2>/dev/null | grep -qE "200|302"; then
break
fi
if [[ $i -eq 24 ]]; then
echo "ERROR: Coolify not ready after 24 attempts" >&2
exit 1
fi
sleep 5
done
# --- Step 1: Register user via Playwright (if no users exist) ---
USER_COUNT=$(psql_exec "SELECT count(*) FROM users;")
if [[ "$USER_COUNT" == "0" ]]; then
log "Registering admin user via Playwright (required for Laravel bootstrap)"
# Prefer Playwright already installed by setup-coolify (CI). A second
# silent Chromium install has no timeout and hid the 45m hang on #795.
PW_PYTHON=""
if python3 -c "from playwright.sync_api import sync_playwright" 2>/dev/null; then
PW_PYTHON="python3"
else
PW_VENV="/tmp/pw-venv"
if [[ ! -f "$PW_VENV/bin/python3" ]]; then
if ! command -v python3 >/dev/null 2>&1; then
echo "ERROR: python3 is required the first time this script bootstraps a Coolify user. Install Python 3.9+ with venv support and re-run." >&2
exit 1
fi
if ! python3 -c "import venv" >/dev/null 2>&1; then
echo "ERROR: python3 venv support is required the first time this script bootstraps a Coolify user. Install python3-venv and re-run." >&2
exit 1
fi
python3 -m venv "$PW_VENV"
fi
if ! "$PW_VENV/bin/python3" -c "from playwright.sync_api import sync_playwright" 2>/dev/null; then
"$PW_VENV/bin/pip" install -q playwright
run_limited 180 "$PW_VENV/bin/playwright" install chromium
fi
PW_PYTHON="$PW_VENV/bin/python3"
fi
"$PW_PYTHON" << 'PYEOF'
import secrets, signal, sys
from playwright.sync_api import sync_playwright
signal.alarm(180)
pwd = "Sc" + secrets.token_urlsafe(20) + "!1"
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.set_default_timeout(30000)
page.goto("http://localhost:8000/register", timeout=30000)
# Do not wait for networkidle: Coolify keeps websockets/polling open.
page.locator('input[name="name"]').wait_for(state="visible", timeout=30000)
page.locator('input[name="name"]').fill("admin")
page.locator('input[name="email"]').fill("admin@acme.test")
page.locator('input[name="password"]').fill(pwd)
page.locator('input[name="password_confirmation"]').fill(pwd)
page.locator('button:has-text("Create Account")').first.click()
page.wait_for_function(
"() => !window.location.pathname.includes('register')",
timeout=60000,
)
print("Registration successful")
browser.close()
PYEOF
else
log "User already exists (count=$USER_COUNT), skipping registration"
fi
USER_ID=$(psql_exec "SELECT id FROM users LIMIT 1;")
TEAM_ID=$(psql_exec "SELECT id FROM teams LIMIT 1;")
if [[ -z "$USER_ID" || -z "$TEAM_ID" ]]; then
echo "ERROR: Could not find user or team in database (user=$USER_ID, team=$TEAM_ID)" >&2
echo "DEBUG: users table:" >&2
docker exec coolify-db psql -U coolify -d coolify -c "SELECT id, name, email FROM users;" 2>/dev/null >&2
echo "DEBUG: teams table:" >&2
docker exec coolify-db psql -U coolify -d coolify -c "SELECT id, name FROM teams;" 2>/dev/null >&2
exit 1
fi
log "User ID=$USER_ID, Team ID=$TEAM_ID"
# --- Step 2: Assign team to server and private key ---
log "Assigning team to server and private key"
psql_exec "UPDATE servers SET team_id = $TEAM_ID WHERE team_id IS NULL OR team_id = 0;" > /dev/null || true
psql_exec "UPDATE private_keys SET team_id = $TEAM_ID WHERE team_id IS NULL OR team_id = 0;" > /dev/null || true
# --- Step 3: Enable API and clear cache ---
log "Enabling API via artisan"
docker exec coolify php artisan tinker --execute='
$s = \App\Models\InstanceSettings::first();
$s->is_api_enabled = true;
$s->save();
echo "API enabled: " . ($s->is_api_enabled ? "true" : "false");
' 2>/dev/null | tail -1
docker exec coolify php artisan config:clear 2>/dev/null || true
# --- Step 4: Create API token ---
log "Creating API token"
# Build the token record directly with all fields (including team_id)
# to avoid NOT NULL constraint. createToken() inserts before we can set team_id.
TINKER_OUTPUT=$(docker exec coolify php artisan tinker --execute='
$user = \App\Models\User::first();
$team = \App\Models\Team::first();
if (!$user || !$team) { echo "ERROR: no user or team"; exit(1); }
\Laravel\Sanctum\PersonalAccessToken::where("name", "acc-tests")->delete();
$plain = \Illuminate\Support\Str::random(40);
$pat = new \Laravel\Sanctum\PersonalAccessToken();
$pat->name = "acc-tests";
$pat->token = hash("sha256", $plain);
$pat->abilities = ["*"];
$pat->tokenable_type = get_class($user);
$pat->tokenable_id = $user->id;
$pat->team_id = $team->id;
$pat->save();
echo $pat->id . "|" . $plain;
' 2>&1)
API_TOKEN=$(echo "$TINKER_OUTPUT" | grep -oE '^[0-9]+\|[A-Za-z0-9]+' | tail -1 || true)
if [[ -z "$API_TOKEN" ]]; then
echo "ERROR: Failed to create API token. Artisan output:" >&2
echo "$TINKER_OUTPUT" >&2
exit 1
fi
log "API token: ${API_TOKEN:0:8}..."
# --- Step 5: Fix private key encryption ---
log "Fixing private key encryption via artisan"
PRIVATE_KEY_UUID=$(psql_exec "SELECT uuid FROM private_keys LIMIT 1;")
KEY_FIX_OUTPUT=$(docker exec coolify php artisan tinker --execute='
$key = \App\Models\PrivateKey::first();
if (!$key) { echo "ERROR: No private key found"; exit(1); }
$path = "/var/www/html/storage/app/ssh/keys/ssh_key@" . $key->uuid;
if (!file_exists($path)) { echo "ERROR: Key file not found: " . $path; exit(1); }
$raw = file_get_contents($path);
$key->private_key = $raw;
$key->save();
echo "Key fixed: " . $key->uuid;
' 2>&1 | tail -1)
log "$KEY_FIX_OUTPUT"
# Allow Coolify to settle after database changes
log "Waiting 10s for Coolify to pick up settings changes"
sleep 10
# --- Step 6: Verify API and validate server ---
SERVER_UUID=$(psql_exec "SELECT uuid FROM servers LIMIT 1;")
log "Server UUID: $SERVER_UUID"
log "Private Key UUID: $PRIVATE_KEY_UUID"
log "Verifying API health (with retries)"
# Quick no-auth diagnostic to distinguish "API is down" from "auth is broken"
NO_AUTH_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Accept: application/json" \
"$COOLIFY_ENDPOINT/api/v1/version" 2>/dev/null || echo "000")
log "No-auth check: HTTP $NO_AUTH_CODE (401=API works but needs auth, 500=app error)"
API_OK=false
for attempt in $(seq 1 12); do
RESP_FILE=$(mktemp)
HTTP_CODE=$(curl -s -o "$RESP_FILE" -w "%{http_code}" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Accept: application/json" \
"$COOLIFY_ENDPOINT/api/v1/version" 2>/dev/null || echo "000")
if [[ "$HTTP_CODE" == "200" ]]; then
VERSION=$(cat "$RESP_FILE" | tr -d '"')
log "API healthy! Coolify version: $VERSION (attempt $attempt)"
API_OK=true
rm -f "$RESP_FILE"
break
fi
log "API returned HTTP $HTTP_CODE (attempt $attempt/12), retrying in 5s..."
if [[ "$HTTP_CODE" == "500" ]]; then
echo " Response body: $(cat "$RESP_FILE")" >&2
fi
rm -f "$RESP_FILE"
sleep 5
done
if [[ "$API_OK" != "true" ]]; then
echo "ERROR: API not healthy after 12 attempts" >&2
echo "DEBUG: Coolify container logs (last 30 lines):" >&2
docker logs coolify --tail 30 2>&1 >&2 || true
exit 1
fi
log "Triggering server validation"
curl -s -H "Authorization: Bearer $API_TOKEN" \
"$COOLIFY_ENDPOINT/api/v1/servers/$SERVER_UUID/validate" > /dev/null 2>&1 || true
# Wait for validation, then force reachable if SSH works from container
sleep 10
IS_REACHABLE=$(psql_exec "SELECT is_reachable FROM server_settings WHERE server_id = (SELECT id FROM servers LIMIT 1);")
if [[ "$IS_REACHABLE" != "t" ]]; then
log "Server not yet reachable via API validation, forcing (SSH confirmed working)"
psql_exec "UPDATE server_settings SET is_reachable = true WHERE server_id = (SELECT id FROM servers LIMIT 1);" > /dev/null
fi
# Increase API rate limit for test suite
log "Setting API rate limit to 1000"
if ! grep -q "API_RATE_LIMIT" "$COOLIFY_DATA_DIR/source/.env" 2>/dev/null; then
echo "API_RATE_LIMIT=1000" >> "$COOLIFY_DATA_DIR/source/.env"
fi
# --- Step 7: MinIO S3 storage for backup tests ---
# Stay on the coolify network only. Host 9000/9001 binds fail when
# Coolify already uses those ports. quay.io/minio and Docker Hub
# minio/minio reject anonymous pulls (Acc #858, docker run exit 125).
# Chainguard publishes a public server and client. The server runs as
# uid 65532, so the data dir is /tmp/data. The client image has no
# shell; MC_HOST_local carries the alias for a single mc command.
MINIO_IMAGE="chainguard/minio@sha256:bd014394a80898e68c149f2311fdf8d5a2c2f3bb2c33b9327ae6d02b4b065ae1"
MC_IMAGE="chainguard/minio-client@sha256:b8b144ab34694ecea25aa352c4be9de4c26ee2a02701521dce02ee5593c57338"
if docker ps --format '{{.Names}}' | grep -q "^coolify-minio$"; then
log "MinIO already running"
elif docker ps -a --format '{{.Names}}' | grep -q "^coolify-minio$"; then
log "Starting existing MinIO container"
docker start coolify-minio
else
log "Starting MinIO for S3 backup tests"
docker run -d \
--name coolify-minio \
--network coolify \
-e MINIO_ROOT_USER=minioadmin \
-e MINIO_ROOT_PASSWORD=minioadmin123 \
"$MINIO_IMAGE" server /tmp/data --console-address ":9001"
fi
sleep 3
mc_s3() {
docker run --rm --network coolify \
-e MC_HOST_local="http://minioadmin:minioadmin123@coolify-minio:9000" \
"$MC_IMAGE" "$@"
}
mc_s3 mb --ignore-existing local/coolify-backups
if ! mc_s3 ls local/coolify-backups >/dev/null 2>&1; then
log "ERROR: MinIO bucket coolify-backups does not exist; not registering minio-test"
exit 1
fi
S3_COUNT=$(psql_exec "SELECT count(*) FROM s3_storages WHERE name = 'minio-test';")
if [[ "$S3_COUNT" == "0" ]]; then
log "Registering MinIO S3 storage in Coolify"
S3_UUID=$(docker exec coolify php artisan tinker --execute='
$s = new \App\Models\S3Storage();
$s->name = "minio-test";
$s->description = "Local MinIO for acceptance testing";
$s->region = "us-east-1";
$s->key = "minioadmin";
$s->secret = "minioadmin123";
$s->bucket = "coolify-backups";
$s->endpoint = "http://coolify-minio:9000";
$s->team_id = \App\Models\Team::first()->id;
$s->is_usable = true;
$s->save();
echo $s->uuid;
' 2>/dev/null | tail -1)
log "S3 storage UUID: $S3_UUID"
else
S3_UUID=$(psql_exec "SELECT uuid FROM s3_storages WHERE name = 'minio-test' LIMIT 1;")
log "MinIO S3 storage already registered: $S3_UUID"
fi
# Do NOT call POST .../s3-storages/{uuid}/validate here. Coolify only trusts
# internal host coolify-minio for the system storage uuid "minio"; validating
# minio-test flips is_usable to false and breaks volume/database S3 backup acc.
# --- Step 8: Update GitHub secrets (if --secrets) ---
if [[ "$UPDATE_SECRETS" == "true" ]]; then
log "Updating GitHub secrets for $REPO"
gh secret set COOLIFY_ENDPOINT --repo "$REPO" --body "$COOLIFY_ENDPOINT"
gh secret set COOLIFY_TOKEN --repo "$REPO" --body "$API_TOKEN"
gh secret set COOLIFY_SERVER_UUID --repo "$REPO" --body "$SERVER_UUID"
gh secret set COOLIFY_PRIVATE_KEY_UUID --repo "$REPO" --body "$PRIVATE_KEY_UUID"
if [[ -n "${S3_UUID:-}" ]]; then
gh secret set COOLIFY_S3_STORAGE_UUID --repo "$REPO" --body "$S3_UUID"
fi
log "GitHub secrets updated"
fi
# --- Step 9: Write env file for CI (if requested) ---
ENV_FILE="${COOLIFY_ENV_FILE:-}"
if [[ -n "$ENV_FILE" ]]; then
log "Writing env file to $ENV_FILE"
cat > "$ENV_FILE" <<EOF
COOLIFY_ENDPOINT='$COOLIFY_ENDPOINT'
COOLIFY_TOKEN='$API_TOKEN'
COOLIFY_SERVER_UUID='$SERVER_UUID'
COOLIFY_PRIVATE_KEY_UUID='$PRIVATE_KEY_UUID'
COOLIFY_S3_STORAGE_UUID='${S3_UUID:-}'
EOF
fi
# --- Summary ---
echo ""
echo "========================================="
echo " Coolify Test Instance Setup Complete"
echo "========================================="
echo " Endpoint: $COOLIFY_ENDPOINT"
echo " API Token: ${API_TOKEN:0:10}..."
echo " Server UUID: $SERVER_UUID"
echo " Private Key UUID: $PRIVATE_KEY_UUID"
echo " S3 Storage UUID: ${S3_UUID:-not configured}"
echo ""
echo " To run acceptance tests locally:"
echo " export COOLIFY_ENDPOINT=$COOLIFY_ENDPOINT"
echo " export COOLIFY_TOKEN=$API_TOKEN"
echo " export COOLIFY_SERVER_UUID=$SERVER_UUID"
echo " export COOLIFY_PRIVATE_KEY_UUID=$PRIVATE_KEY_UUID"
echo " export COOLIFY_S3_STORAGE_UUID=${S3_UUID:-}"
echo " make testacc"
echo "========================================="