vibed-infra 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/github/__pycache__/notify-vps-pull.cpython-312.pyc +0 -0
- package/install.sh +52 -3
- package/lib/__pycache__/generate.cpython-312.pyc +0 -0
- package/lib/__pycache__/github_oidc.cpython-312.pyc +0 -0
- package/lib/__pycache__/load_config.cpython-312.pyc +0 -0
- package/lib/__pycache__/product_config.cpython-312.pyc +0 -0
- package/lib/__pycache__/webhook.cpython-312.pyc +0 -0
- package/lib/env.sh +141 -0
- package/lib/package.py +36 -5
- package/lib/persistlog/__pycache__/__init__.cpython-312.pyc +0 -0
- package/lib/persistlog/ship.py +99 -7
- package/package.json +1 -1
- package/schema/packageconfig.md +7 -3
- package/skills/agent-vps-prep/SKILL.md +25 -24
- package/skills/persist-logs/SKILL.md +13 -3
- package/templates/docker-compose.backend.yml +17 -2
- package/templates/docker-compose.workers.yml +16 -1
- package/templates/generic/start-api.sh +45 -11
- package/templates/generic/start-gateway.sh +2 -0
- package/templates/generic/start-nodes.sh +86 -2
- package/templates/generic/start-ui.sh +2 -0
- package/templates/host-gateway/start-gateway.sh +2 -0
- package/templates/monitor/install-monitor.sh +15 -0
- package/templates/monitor/monitor-vibed.sh +340 -0
- package/templates/persist-logs/start-persist-sidecar.sh +68 -0
- package/templates/update-agent/__pycache__/webhook_server.cpython-312.pyc +0 -0
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ git add dist && git commit && git push
|
|
|
19
19
|
|
|
20
20
|
## Operator flow (VPS)
|
|
21
21
|
|
|
22
|
-
Non-root agent prep (SSH, Docker group
|
|
22
|
+
Non-root agent prep (SSH, Docker group): [`skills/agent-vps-prep/SKILL.md`](skills/agent-vps-prep/SKILL.md).
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
# 1) DNS — paste dist/DNS-SKILL.md into AU browser agent
|
|
@@ -44,7 +44,10 @@ Multi-app: further products’ `install-gateway.sh` only add `apps/{other}/sites
|
|
|
44
44
|
|------|---------|
|
|
45
45
|
| `~/services/gateway` | Host nginx (`GATEWAY_HOME`) |
|
|
46
46
|
| `~/services/vibed-infra/update-agent` | Serial pull queue; GHCR notify via GitHub Actions OIDC |
|
|
47
|
-
| `~/services/vibed-infra/persist-logs` |
|
|
47
|
+
| `~/services/vibed-infra/persist-logs` | Ship credentials + host cron leftover; per-app WALs live in named volumes |
|
|
48
|
+
| `~/services/vibed-infra/monitor-vibed.sh` | TUI: list deployments, summary, tail docker/volume logs |
|
|
49
|
+
|
|
50
|
+
App durable data (SQLite, persist WALs, worker logs) uses **Docker named volumes** by default — not bind mounts under `~/services`. Digests recreate containers; volumes keep the DB.
|
|
48
51
|
|
|
49
52
|
## Image updates (docker pull)
|
|
50
53
|
|
|
@@ -62,7 +65,8 @@ See [`skills/infra-update-agent/SKILL.md`](skills/infra-update-agent/SKILL.md).
|
|
|
62
65
|
| `package.sh` / `lib/package.py` | Build `dist/` |
|
|
63
66
|
| `templates/host-gateway/` | Shared gateway skeleton |
|
|
64
67
|
| `templates/update-agent/` | Queue agent + webhook |
|
|
65
|
-
| `templates/persist-logs/` | Shipper install |
|
|
68
|
+
| `templates/persist-logs/` | Shipper install + persist sidecar |
|
|
69
|
+
| `templates/monitor/` | Machine-wide `monitor-vibed.sh` |
|
|
66
70
|
| `lib/persistlog/` | Python append / seal / replay |
|
|
67
71
|
| `skills/` | system-gateway, agent-vps-prep, infra-update-agent, infra-cicd, infra-packager, persist-logs, dns-configure |
|
|
68
72
|
|
|
@@ -75,7 +79,9 @@ See [`skills/infra-update-agent/SKILL.md`](skills/infra-update-agent/SKILL.md).
|
|
|
75
79
|
| `VIBED_UPDATE_AGENT` | Override update-agent dir (default `$VIBED_HOME/update-agent`) |
|
|
76
80
|
| `GATEWAY_PUBLIC_IP` | VPS IPv4 (from `gateway.publicIp`) |
|
|
77
81
|
| `TLS_EMAIL` / `TLS_MODE` | Let’s Encrypt email; `lab` or `letsencrypt` (docker certbot → `./certs` PEMs; optional `CERTBOT_IMAGE` / `LETSENCRYPT_HOME`) |
|
|
78
|
-
| `
|
|
82
|
+
| `DATA_VOLUME` / `PERSIST_LOG_VOLUME` / `WORKER_LOG_VOLUME` | Named volume overrides (default `{container}-{data,persist,logs}`) |
|
|
83
|
+
| `PERSIST_LOGS` | Set `0` to disable persist volume + sidecar |
|
|
84
|
+
| `DATA_BIND` / `PERSIST_LOG_BIND` / `LOGS_BIND` | Force host bind mounts (debug) |
|
|
79
85
|
| `DOCKER_AUTO_PRUNE` | Prune dangling images after update (default on) |
|
|
80
86
|
|
|
81
87
|
## Tests
|
|
Binary file
|
package/install.sh
CHANGED
|
@@ -397,9 +397,58 @@ else:
|
|
|
397
397
|
echo "webhook token registered for ${PRODUCT_NAME}"
|
|
398
398
|
fi
|
|
399
399
|
fi
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
400
|
+
# Machine-wide persist-logs + monitor (idempotent; works for path or http PACKAGER_RAW)
|
|
401
|
+
_install_machine_helpers() {
|
|
402
|
+
local helper_packager="$PACKAGER_RAW"
|
|
403
|
+
if [[ "$LOCAL_PACKAGER" != "1" ]]; then
|
|
404
|
+
helper_packager="${DEST}/.infra-packager"
|
|
405
|
+
mkdir -p "$helper_packager/templates/persist-logs/lib" \
|
|
406
|
+
"$helper_packager/templates/monitor" \
|
|
407
|
+
"$helper_packager/lib"
|
|
408
|
+
_fetch "${PACKAGER_RAW}/templates/persist-logs/install-persist-logs.sh" \
|
|
409
|
+
"$helper_packager/templates/persist-logs/install-persist-logs.sh" || true
|
|
410
|
+
_fetch "${PACKAGER_RAW}/lib/persistlog/ship.py" \
|
|
411
|
+
"$helper_packager/lib/persistlog/ship.py" || true
|
|
412
|
+
# Minimal persistlog package for host cron / sidecar copy
|
|
413
|
+
mkdir -p "$helper_packager/lib/persistlog"
|
|
414
|
+
for f in __init__.py ship.py; do
|
|
415
|
+
_fetch "${PACKAGER_RAW}/lib/persistlog/${f}" \
|
|
416
|
+
"$helper_packager/lib/persistlog/${f}" || true
|
|
417
|
+
done
|
|
418
|
+
_fetch "${PACKAGER_RAW}/templates/monitor/install-monitor.sh" \
|
|
419
|
+
"$helper_packager/templates/monitor/install-monitor.sh" || true
|
|
420
|
+
_fetch "${PACKAGER_RAW}/templates/monitor/monitor-vibed.sh" \
|
|
421
|
+
"$helper_packager/templates/monitor/monitor-vibed.sh" || true
|
|
422
|
+
_fetch "${PACKAGER_RAW}/templates/persist-logs/start-persist-sidecar.sh" \
|
|
423
|
+
"$helper_packager/templates/persist-logs/start-persist-sidecar.sh" || true
|
|
424
|
+
fi
|
|
425
|
+
if [[ -f "${helper_packager}/templates/persist-logs/install-persist-logs.sh" ]]; then
|
|
426
|
+
chmod +x "${helper_packager}/templates/persist-logs/"*.sh 2>/dev/null || true
|
|
427
|
+
PACKAGER_RAW="$helper_packager" VIBED_HOME="${VIBED_HOME:-}" \
|
|
428
|
+
bash "${helper_packager}/templates/persist-logs/install-persist-logs.sh" || true
|
|
429
|
+
fi
|
|
430
|
+
if [[ -f "${helper_packager}/templates/monitor/install-monitor.sh" ]]; then
|
|
431
|
+
chmod +x "${helper_packager}/templates/monitor/"*.sh 2>/dev/null || true
|
|
432
|
+
PACKAGER_RAW="$helper_packager" VIBED_HOME="${VIBED_HOME:-}" \
|
|
433
|
+
bash "${helper_packager}/templates/monitor/install-monitor.sh" || true
|
|
434
|
+
fi
|
|
435
|
+
if [[ "$ROLE" == "backend" || "$ROLE" == "workers" ]]; then
|
|
436
|
+
# Prefer product dist copy when present; else packager template.
|
|
437
|
+
if [[ -f "${PRODUCT_RAW}/start-persist-sidecar.sh" ]]; then
|
|
438
|
+
cp -f "${PRODUCT_RAW}/start-persist-sidecar.sh" "$DEST/start-persist-sidecar.sh"
|
|
439
|
+
elif [[ "${PRODUCT_RAW}" =~ ^https?:// ]]; then
|
|
440
|
+
_fetch "${PRODUCT_RAW}/start-persist-sidecar.sh" "$DEST/start-persist-sidecar.sh" || true
|
|
441
|
+
elif [[ -f "${helper_packager}/templates/persist-logs/start-persist-sidecar.sh" ]]; then
|
|
442
|
+
cp -f "${helper_packager}/templates/persist-logs/start-persist-sidecar.sh" \
|
|
443
|
+
"$DEST/start-persist-sidecar.sh"
|
|
444
|
+
elif [[ -f "${PACKAGER_RAW}/templates/persist-logs/start-persist-sidecar.sh" ]]; then
|
|
445
|
+
cp -f "${PACKAGER_RAW}/templates/persist-logs/start-persist-sidecar.sh" \
|
|
446
|
+
"$DEST/start-persist-sidecar.sh"
|
|
447
|
+
fi
|
|
448
|
+
chmod +x "$DEST/start-persist-sidecar.sh" 2>/dev/null || true
|
|
449
|
+
fi
|
|
450
|
+
}
|
|
451
|
+
_install_machine_helpers
|
|
403
452
|
|
|
404
453
|
INFRA_PROFILE="$PROFILE" ./install-auto-update.sh || true
|
|
405
454
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/env.sh
CHANGED
|
@@ -209,6 +209,7 @@ memory_args() {
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
chown_data_dir() {
|
|
212
|
+
# Legacy bind-mount helper (DATA_BIND=1). Named volumes do not use this.
|
|
212
213
|
local dir="$1"
|
|
213
214
|
local uid="${2:-1000}"
|
|
214
215
|
mkdir -p "$dir"
|
|
@@ -223,6 +224,146 @@ chown_data_dir() {
|
|
|
223
224
|
chmod 755 "$dir" 2>/dev/null || true
|
|
224
225
|
}
|
|
225
226
|
|
|
227
|
+
# Create a durable named volume (idempotent). Never removes existing volumes.
|
|
228
|
+
# Sets VIBED_VOLUME_CREATED=1 when a new volume was created, else 0.
|
|
229
|
+
# Extra label key=value pairs after role/container are optional.
|
|
230
|
+
vibed_ensure_volume() {
|
|
231
|
+
local name="$1"
|
|
232
|
+
local role="${2:-}"
|
|
233
|
+
local container="${3:-}"
|
|
234
|
+
shift 3 || true
|
|
235
|
+
VIBED_VOLUME_CREATED=0
|
|
236
|
+
local -a labels=(--label "vibed.managed=1")
|
|
237
|
+
[[ -n "$role" ]] && labels+=(--label "vibed.role=${role}")
|
|
238
|
+
[[ -n "$container" ]] && labels+=(--label "vibed.container=${container}")
|
|
239
|
+
local kv
|
|
240
|
+
for kv in "$@"; do
|
|
241
|
+
[[ -n "$kv" ]] && labels+=(--label "$kv")
|
|
242
|
+
done
|
|
243
|
+
if docker volume inspect "$name" >/dev/null 2>&1; then
|
|
244
|
+
return 0
|
|
245
|
+
fi
|
|
246
|
+
docker volume create "${labels[@]}" "$name" >/dev/null
|
|
247
|
+
VIBED_VOLUME_CREATED=1
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# True if dir exists and has any entries (including hidden except . and ..).
|
|
251
|
+
_vibed_host_dir_has_files() {
|
|
252
|
+
local dir="$1"
|
|
253
|
+
[[ -d "$dir" ]] || return 1
|
|
254
|
+
local f
|
|
255
|
+
for f in "$dir"/* "$dir"/.[!.]* "$dir"/..?*; do
|
|
256
|
+
[[ -e "$f" ]] && return 0
|
|
257
|
+
done
|
|
258
|
+
return 1
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# One-time copy from a legacy host bind dir into a newly created empty named volume.
|
|
262
|
+
# Do NOT mount the volume before the app unless migrating — any first mount can
|
|
263
|
+
# initialize ownership from that helper image instead of the app image.
|
|
264
|
+
vibed_migrate_host_dir() {
|
|
265
|
+
local vol="$1"
|
|
266
|
+
local host_dir="$2"
|
|
267
|
+
local newly_created="${3:-0}"
|
|
268
|
+
[[ -n "$vol" && -n "$host_dir" ]] || return 0
|
|
269
|
+
[[ "$newly_created" == "1" ]] || return 0
|
|
270
|
+
_vibed_host_dir_has_files "$host_dir" || return 0
|
|
271
|
+
local abs
|
|
272
|
+
abs="$(cd "$host_dir" && pwd)"
|
|
273
|
+
echo "Migrating $abs → volume $vol (keeping host copy) ..."
|
|
274
|
+
docker run --rm \
|
|
275
|
+
-v "${vol}:/dest" \
|
|
276
|
+
-v "${abs}:/src:ro" \
|
|
277
|
+
alpine sh -c 'cp -a /src/. /dest/' >/dev/null
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# Resolve data storage: named volume (default) vs host bind (DATA_BIND=1 or non-default DATA_DIR).
|
|
281
|
+
# Sets: VIBED_DATA_MODE=volume|bind, VIBED_DATA_VOLUME, VIBED_DATA_BIND_PATH
|
|
282
|
+
vibed_resolve_data_storage() {
|
|
283
|
+
local container_name="$1"
|
|
284
|
+
local role="${2:-api}"
|
|
285
|
+
VIBED_DATA_MODE=volume
|
|
286
|
+
VIBED_DATA_VOLUME="${DATA_VOLUME:-${container_name}-data}"
|
|
287
|
+
VIBED_DATA_BIND_PATH=""
|
|
288
|
+
if env_flag_on DATA_BIND; then
|
|
289
|
+
VIBED_DATA_MODE=bind
|
|
290
|
+
VIBED_DATA_BIND_PATH="${DATA_DIR:-./data}"
|
|
291
|
+
return 0
|
|
292
|
+
fi
|
|
293
|
+
local dd="${DATA_DIR-}"
|
|
294
|
+
if [[ -n "$dd" && "$dd" != "./data" && "$dd" != "data" ]]; then
|
|
295
|
+
VIBED_DATA_MODE=bind
|
|
296
|
+
VIBED_DATA_BIND_PATH="$dd"
|
|
297
|
+
return 0
|
|
298
|
+
fi
|
|
299
|
+
vibed_ensure_volume "$VIBED_DATA_VOLUME" "$role" "$container_name" "vibed.kind=data"
|
|
300
|
+
if [[ -z "$dd" || "$dd" == "./data" || "$dd" == "data" ]]; then
|
|
301
|
+
vibed_migrate_host_dir "$VIBED_DATA_VOLUME" "./data" "${VIBED_VOLUME_CREATED:-0}"
|
|
302
|
+
fi
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# Persist logs: default named volume + sidecar. PERSIST_LOGS=0 disables.
|
|
306
|
+
# Sets: VIBED_PERSIST_MODE=volume|bind|off, VIBED_PERSIST_VOLUME, VIBED_PERSIST_BIND_PATH
|
|
307
|
+
vibed_resolve_persist_storage() {
|
|
308
|
+
local container_name="$1"
|
|
309
|
+
local role="${2:-api}"
|
|
310
|
+
VIBED_PERSIST_MODE=volume
|
|
311
|
+
VIBED_PERSIST_VOLUME="${PERSIST_LOG_VOLUME:-${container_name}-persist}"
|
|
312
|
+
VIBED_PERSIST_BIND_PATH=""
|
|
313
|
+
if [[ -n "${PERSIST_LOGS+x}" ]] && ! env_flag_on PERSIST_LOGS; then
|
|
314
|
+
VIBED_PERSIST_MODE=off
|
|
315
|
+
return 0
|
|
316
|
+
fi
|
|
317
|
+
if env_flag_on PERSIST_LOG_BIND; then
|
|
318
|
+
VIBED_PERSIST_MODE=bind
|
|
319
|
+
VIBED_PERSIST_BIND_PATH="${PERSIST_LOG_DIR:-./persist-logs}"
|
|
320
|
+
return 0
|
|
321
|
+
fi
|
|
322
|
+
local pd="${PERSIST_LOG_DIR-}"
|
|
323
|
+
if [[ -n "$pd" && "$pd" != "./persist-logs" && "$pd" != "persist-logs" ]]; then
|
|
324
|
+
VIBED_PERSIST_MODE=bind
|
|
325
|
+
VIBED_PERSIST_BIND_PATH="$pd"
|
|
326
|
+
return 0
|
|
327
|
+
fi
|
|
328
|
+
vibed_ensure_volume "$VIBED_PERSIST_VOLUME" "$role" "$container_name" "vibed.kind=persist"
|
|
329
|
+
if [[ -z "$pd" || "$pd" == "./persist-logs" || "$pd" == "persist-logs" ]]; then
|
|
330
|
+
vibed_migrate_host_dir "$VIBED_PERSIST_VOLUME" "./persist-logs" "${VIBED_VOLUME_CREATED:-0}"
|
|
331
|
+
fi
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
# Worker logs volume (default) or bind.
|
|
335
|
+
# Sets: VIBED_LOGS_MODE=volume|bind, VIBED_LOGS_VOLUME, VIBED_LOGS_BIND_PATH
|
|
336
|
+
vibed_resolve_logs_storage() {
|
|
337
|
+
local container_name="$1"
|
|
338
|
+
local role="${2:-nodes}"
|
|
339
|
+
VIBED_LOGS_MODE=volume
|
|
340
|
+
VIBED_LOGS_VOLUME="${WORKER_LOG_VOLUME:-${container_name}-logs}"
|
|
341
|
+
VIBED_LOGS_BIND_PATH=""
|
|
342
|
+
if env_flag_on LOGS_BIND; then
|
|
343
|
+
VIBED_LOGS_MODE=bind
|
|
344
|
+
VIBED_LOGS_BIND_PATH="${LOGS_DIR:-./logs}"
|
|
345
|
+
return 0
|
|
346
|
+
fi
|
|
347
|
+
local ld="${LOGS_DIR-}"
|
|
348
|
+
if [[ -n "$ld" && "$ld" != "./logs" && "$ld" != "logs" ]]; then
|
|
349
|
+
VIBED_LOGS_MODE=bind
|
|
350
|
+
VIBED_LOGS_BIND_PATH="$ld"
|
|
351
|
+
return 0
|
|
352
|
+
fi
|
|
353
|
+
vibed_ensure_volume "$VIBED_LOGS_VOLUME" "$role" "$container_name" "vibed.kind=logs"
|
|
354
|
+
if [[ -z "$ld" || "$ld" == "./logs" || "$ld" == "logs" ]]; then
|
|
355
|
+
vibed_migrate_host_dir "$VIBED_LOGS_VOLUME" "./logs" "${VIBED_VOLUME_CREATED:-0}"
|
|
356
|
+
fi
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
vibed_home_dir() {
|
|
360
|
+
if [[ -n "${VIBED_HOME:-}" ]]; then
|
|
361
|
+
echo "${VIBED_HOME/#\~/$HOME}"
|
|
362
|
+
return 0
|
|
363
|
+
fi
|
|
364
|
+
echo "${HOME}/services/vibed-infra"
|
|
365
|
+
}
|
|
366
|
+
|
|
226
367
|
# Default on during auto-update. Set DOCKER_AUTO_PRUNE=0 to disable.
|
|
227
368
|
docker_auto_prune_on() {
|
|
228
369
|
if [[ -n "${DOCKER_AUTO_PRUNE+x}" ]]; then
|
package/lib/package.py
CHANGED
|
@@ -45,10 +45,17 @@ DOCKER_NETWORK={meta['network']}
|
|
|
45
45
|
DOCKER_NAME={meta['apiContainer']}
|
|
46
46
|
BACKEND_IMAGE={conf['images']['backend']}
|
|
47
47
|
HOST_PORT={meta['apiPort']}
|
|
48
|
-
|
|
48
|
+
# Durable data uses Docker named volume ${{DOCKER_NAME}}-data by default.
|
|
49
|
+
# DATA_VOLUME=custom-data-vol
|
|
50
|
+
# DATA_BIND=1
|
|
51
|
+
# DATA_DIR=./data
|
|
49
52
|
CONFIG_FILE=api-app.yaml
|
|
50
53
|
API_MEMORY_LIMIT=256m
|
|
51
|
-
|
|
54
|
+
# Persist logs: named volume ${{DOCKER_NAME}}-persist + ship sidecar (default on).
|
|
55
|
+
# PERSIST_LOG_VOLUME=custom-persist-vol
|
|
56
|
+
# PERSIST_LOGS=0
|
|
57
|
+
# PERSIST_LOG_BIND=1
|
|
58
|
+
# PERSIST_LOG_DIR=./persist-logs
|
|
52
59
|
{au['flag']}=0
|
|
53
60
|
{au['intervalEnv']}=30
|
|
54
61
|
{au['stopTimeoutEnv']}=60
|
|
@@ -65,7 +72,6 @@ DOCKER_NETWORK={meta['network']}
|
|
|
65
72
|
UI_NAME={meta['uiContainer']}
|
|
66
73
|
UI_IMAGE={conf['images']['ui']}
|
|
67
74
|
UI_MEMORY_LIMIT=32m
|
|
68
|
-
PERSIST_LOG_DIR=./persist-logs
|
|
69
75
|
{au['flag']}=0
|
|
70
76
|
{au['intervalEnv']}=20
|
|
71
77
|
{au['stopTimeoutEnv']}=30
|
|
@@ -87,7 +93,12 @@ WORKER_CONTAINER_NAME={meta['workerContainer']}
|
|
|
87
93
|
WORKER_ROLE=heartbeat
|
|
88
94
|
INTERVAL_SEC=60
|
|
89
95
|
WORKER_MEMORY_LIMIT=64m
|
|
90
|
-
|
|
96
|
+
# Logs + persist use named volumes by default (${{WORKER_CONTAINER_NAME}}-logs / -persist).
|
|
97
|
+
# WORKER_LOG_VOLUME=
|
|
98
|
+
# PERSIST_LOG_VOLUME=
|
|
99
|
+
# PERSIST_LOGS=0
|
|
100
|
+
# LOGS_BIND=1
|
|
101
|
+
# LOGS_DIR=./logs
|
|
91
102
|
{au['flag']}=0
|
|
92
103
|
{au['intervalEnv']}=30
|
|
93
104
|
{au['stopTimeoutEnv']}=30
|
|
@@ -186,6 +197,14 @@ networks:
|
|
|
186
197
|
external: true
|
|
187
198
|
name: ${{DOCKER_NETWORK:-{meta['network']}}}
|
|
188
199
|
|
|
200
|
+
volumes:
|
|
201
|
+
worker_logs:
|
|
202
|
+
external: true
|
|
203
|
+
name: ${{WORKER_LOG_VOLUME:-{meta['workerContainer']}-logs}}
|
|
204
|
+
worker_persist:
|
|
205
|
+
external: true
|
|
206
|
+
name: ${{PERSIST_LOG_VOLUME:-{meta['workerContainer']}-persist}}
|
|
207
|
+
|
|
189
208
|
services:
|
|
190
209
|
worker:
|
|
191
210
|
image: ${{WORKER_IMAGE:-{conf['images']['worker']}}}
|
|
@@ -193,13 +212,20 @@ services:
|
|
|
193
212
|
restart: unless-stopped
|
|
194
213
|
mem_limit: ${{WORKER_MEMORY_LIMIT:-64m}}
|
|
195
214
|
env_file: .env
|
|
215
|
+
labels:
|
|
216
|
+
vibed.managed: "1"
|
|
217
|
+
vibed.role: nodes
|
|
218
|
+
vibed.logs-volume: ${{WORKER_LOG_VOLUME:-{meta['workerContainer']}-logs}}
|
|
219
|
+
vibed.persist-volume: ${{PERSIST_LOG_VOLUME:-{meta['workerContainer']}-persist}}
|
|
196
220
|
environment:
|
|
197
221
|
WORKER_ROLE: ${{WORKER_ROLE:-heartbeat}}
|
|
198
222
|
WORKER_CONFIG: /config/workers.yaml
|
|
199
223
|
API_URL: ${{API_URL:-http://{meta['apiContainer']}:{meta['apiPort']}}}
|
|
200
224
|
INTERVAL_SEC: ${{INTERVAL_SEC:-60}}
|
|
225
|
+
PERSIST_LOG_DIR: /persist-logs
|
|
201
226
|
volumes:
|
|
202
|
-
-
|
|
227
|
+
- worker_logs:/data/logs
|
|
228
|
+
- worker_persist:/persist-logs
|
|
203
229
|
- ./nodes-workers.yaml:/config/workers.yaml:ro
|
|
204
230
|
extra_hosts:
|
|
205
231
|
- "host.docker.internal:host-gateway"
|
|
@@ -308,6 +334,11 @@ def build_dist(
|
|
|
308
334
|
):
|
|
309
335
|
_copy_generic(packager_root, out_dir, script)
|
|
310
336
|
|
|
337
|
+
sidecar = packager_root / "templates" / "persist-logs" / "start-persist-sidecar.sh"
|
|
338
|
+
if sidecar.is_file():
|
|
339
|
+
shutil.copy2(sidecar, out_dir / "start-persist-sidecar.sh")
|
|
340
|
+
_chmod_x(out_dir / "start-persist-sidecar.sh")
|
|
341
|
+
|
|
311
342
|
# Bake HTTP URLs into install wrappers when packaging for wget serve; else GitHub default.
|
|
312
343
|
if packager_raw_val.startswith("http://") or packager_raw_val.startswith("https://"):
|
|
313
344
|
wrapper_packager = packager_raw_val
|
|
Binary file
|
package/lib/persistlog/ship.py
CHANGED
|
@@ -2,18 +2,105 @@
|
|
|
2
2
|
"""Ship sealed persist-log segments to R2/S3-compatible storage (batched PUTs)."""
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
5
7
|
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
6
10
|
import sys
|
|
7
11
|
import urllib.error
|
|
8
12
|
import urllib.request
|
|
13
|
+
from datetime import datetime, timezone
|
|
9
14
|
from pathlib import Path
|
|
15
|
+
from urllib.parse import quote, urlsplit
|
|
10
16
|
|
|
11
|
-
# Optional: use aws CLI if present (works with R2 endpoint)
|
|
12
|
-
def ship_file(local: Path, key: str, endpoint: str, bucket: str, access: str, secret: str) -> None:
|
|
13
|
-
# Prefer aws CLI for SigV4
|
|
14
|
-
import shutil
|
|
15
|
-
import subprocess
|
|
16
17
|
|
|
18
|
+
def _sigv4_put(
|
|
19
|
+
local: Path,
|
|
20
|
+
key: str,
|
|
21
|
+
endpoint: str,
|
|
22
|
+
bucket: str,
|
|
23
|
+
access: str,
|
|
24
|
+
secret: str,
|
|
25
|
+
region: str = "auto",
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Minimal SigV4 PUT for S3/R2-compatible endpoints (stdlib only)."""
|
|
28
|
+
body = local.read_bytes()
|
|
29
|
+
payload_hash = hashlib.sha256(body).hexdigest()
|
|
30
|
+
now = datetime.now(timezone.utc)
|
|
31
|
+
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
|
32
|
+
date_stamp = now.strftime("%Y%m%d")
|
|
33
|
+
|
|
34
|
+
parts = urlsplit(endpoint.rstrip("/"))
|
|
35
|
+
host = parts.netloc
|
|
36
|
+
# Virtual-hosted style preferred for R2: https://bucket.account.r2.cloudflarestorage.com/key
|
|
37
|
+
# Path style: https://endpoint/bucket/key
|
|
38
|
+
if host.startswith(f"{bucket}."):
|
|
39
|
+
url_path = "/" + quote(key, safe="/")
|
|
40
|
+
canonical_uri = url_path
|
|
41
|
+
host_header = host
|
|
42
|
+
else:
|
|
43
|
+
url_path = f"/{bucket}/{quote(key, safe='/')}"
|
|
44
|
+
canonical_uri = f"/{bucket}/{quote(key, safe='/')}"
|
|
45
|
+
host_header = host
|
|
46
|
+
|
|
47
|
+
service = "s3"
|
|
48
|
+
credential_scope = f"{date_stamp}/{region}/{service}/aws4_request"
|
|
49
|
+
canonical_headers = (
|
|
50
|
+
f"host:{host_header}\n"
|
|
51
|
+
f"x-amz-content-sha256:{payload_hash}\n"
|
|
52
|
+
f"x-amz-date:{amz_date}\n"
|
|
53
|
+
)
|
|
54
|
+
signed_headers = "host;x-amz-content-sha256;x-amz-date"
|
|
55
|
+
canonical_request = "\n".join(
|
|
56
|
+
[
|
|
57
|
+
"PUT",
|
|
58
|
+
canonical_uri,
|
|
59
|
+
"",
|
|
60
|
+
canonical_headers,
|
|
61
|
+
signed_headers,
|
|
62
|
+
payload_hash,
|
|
63
|
+
]
|
|
64
|
+
)
|
|
65
|
+
string_to_sign = "\n".join(
|
|
66
|
+
[
|
|
67
|
+
"AWS4-HMAC-SHA256",
|
|
68
|
+
amz_date,
|
|
69
|
+
credential_scope,
|
|
70
|
+
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest(),
|
|
71
|
+
]
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def _sign(key: bytes, msg: str) -> bytes:
|
|
75
|
+
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
|
|
76
|
+
|
|
77
|
+
k_date = _sign(("AWS4" + secret).encode("utf-8"), date_stamp)
|
|
78
|
+
k_region = _sign(k_date, region)
|
|
79
|
+
k_service = _sign(k_region, service)
|
|
80
|
+
k_signing = _sign(k_service, "aws4_request")
|
|
81
|
+
signature = hmac.new(k_signing, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
|
82
|
+
authorization = (
|
|
83
|
+
f"AWS4-HMAC-SHA256 Credential={access}/{credential_scope}, "
|
|
84
|
+
f"SignedHeaders={signed_headers}, Signature={signature}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
url = f"{parts.scheme}://{host_header}{url_path}"
|
|
88
|
+
req = urllib.request.Request(url, data=body, method="PUT")
|
|
89
|
+
req.add_header("Host", host_header)
|
|
90
|
+
req.add_header("x-amz-content-sha256", payload_hash)
|
|
91
|
+
req.add_header("x-amz-date", amz_date)
|
|
92
|
+
req.add_header("Authorization", authorization)
|
|
93
|
+
req.add_header("Content-Length", str(len(body)))
|
|
94
|
+
try:
|
|
95
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
96
|
+
if resp.status not in (200, 201, 204):
|
|
97
|
+
raise RuntimeError(f"PUT {url} returned {resp.status}")
|
|
98
|
+
except urllib.error.HTTPError as exc:
|
|
99
|
+
detail = exc.read().decode("utf-8", errors="replace")[:500]
|
|
100
|
+
raise RuntimeError(f"PUT {url} failed: {exc.code} {detail}") from exc
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def ship_file(local: Path, key: str, endpoint: str, bucket: str, access: str, secret: str) -> None:
|
|
17
104
|
if shutil.which("aws"):
|
|
18
105
|
env = os.environ.copy()
|
|
19
106
|
env["AWS_ACCESS_KEY_ID"] = access
|
|
@@ -30,7 +117,8 @@ def ship_file(local: Path, key: str, endpoint: str, bucket: str, access: str, se
|
|
|
30
117
|
]
|
|
31
118
|
subprocess.run(cmd, check=True, env=env)
|
|
32
119
|
return
|
|
33
|
-
|
|
120
|
+
region = os.environ.get("R2_REGION") or os.environ.get("AWS_DEFAULT_REGION") or "auto"
|
|
121
|
+
_sigv4_put(local, key, endpoint, bucket, access, secret, region=region)
|
|
34
122
|
|
|
35
123
|
|
|
36
124
|
def main() -> int:
|
|
@@ -43,6 +131,7 @@ def main() -> int:
|
|
|
43
131
|
access = os.environ.get("R2_ACCESS_KEY_ID") or os.environ.get("AWS_ACCESS_KEY_ID") or ""
|
|
44
132
|
secret = os.environ.get("R2_SECRET_ACCESS_KEY") or os.environ.get("AWS_SECRET_ACCESS_KEY") or ""
|
|
45
133
|
machine = os.environ.get("PERSIST_MACHINE_ID") or os.uname().nodename
|
|
134
|
+
prefix = (os.environ.get("PERSIST_SHIP_PREFIX") or "").strip().strip("/")
|
|
46
135
|
if not endpoint or not bucket or not access or not secret:
|
|
47
136
|
print("missing R2/S3 credentials — skip ship", file=sys.stderr)
|
|
48
137
|
return 0
|
|
@@ -53,7 +142,10 @@ def main() -> int:
|
|
|
53
142
|
if marker.is_file():
|
|
54
143
|
continue
|
|
55
144
|
rel = seg.relative_to(base).as_posix()
|
|
56
|
-
|
|
145
|
+
if prefix:
|
|
146
|
+
key = f"{machine}/{prefix}/{rel}"
|
|
147
|
+
else:
|
|
148
|
+
key = f"{machine}/{rel}"
|
|
57
149
|
try:
|
|
58
150
|
ship_file(seg, key, endpoint, bucket, access, secret)
|
|
59
151
|
marker.write_text("ok\n", encoding="utf-8")
|
package/package.json
CHANGED
package/schema/packageconfig.md
CHANGED
|
@@ -43,7 +43,7 @@ Compiled `packageconfig.yaml` also includes `webhook.url` / `fallbackUrl` (from
|
|
|
43
43
|
| `DNS-SKILL.md` | Paste into AU browser agent; includes domains + `publicIp` when set |
|
|
44
44
|
| `packageconfig.yaml` | compiled config |
|
|
45
45
|
| `start-*.sh` / `update-*.sh` | lifecycle |
|
|
46
|
-
| `.env.*.example` | includes
|
|
46
|
+
| `.env.*.example` | includes named-volume / persist / prune flags |
|
|
47
47
|
|
|
48
48
|
Gateway install writes `$GATEWAY_HOME/apps/{name}/sites.conf` (host-extension mode).
|
|
49
49
|
|
|
@@ -57,9 +57,13 @@ Install registers each role with the machine **update-agent**. Pulls are serial
|
|
|
57
57
|
|
|
58
58
|
`webhook.token` in compiled packageconfig is only for local curl; CI does not need `VIBED_WEBHOOK_SECRET`.
|
|
59
59
|
|
|
60
|
-
## Persist logs
|
|
60
|
+
## Persist logs + named volumes
|
|
61
61
|
|
|
62
|
-
`
|
|
62
|
+
By default API `/data`, persist WALs, and worker `/data/logs` use Docker **named volumes** (stable across digest-gated recreates). A vibed-prepared **persist sidecar** ships sealed segments using machine-wide R2/S3 config. See [`skills/persist-logs/SKILL.md`](../skills/persist-logs/SKILL.md).
|
|
63
|
+
|
|
64
|
+
`.env.*.example` documents `DATA_VOLUME`, `PERSIST_LOG_VOLUME`, `PERSIST_LOGS=0`, and bind escape hatches.
|
|
65
|
+
|
|
66
|
+
Inspect: `~/services/vibed-infra/monitor-vibed.sh`.
|
|
63
67
|
|
|
64
68
|
## Environment (install)
|
|
65
69
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: agent-vps-prep
|
|
3
3
|
description: >-
|
|
4
4
|
Prepare a fresh VPS for vibed-infra installs run by an automation agent
|
|
5
|
-
(cloud_agent): SSH key, sudoers, Docker group,
|
|
5
|
+
(cloud_agent): SSH key, sudoers, Docker group, firewall; app data uses named volumes.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Prepare a VPS for vibed-infra (agent operator)
|
|
@@ -67,42 +67,43 @@ sudo chown -R cloud_agent:cloud_agent /home/cloud_agent
|
|
|
67
67
|
|
|
68
68
|
vibed-infra defaults: `~/services/gateway`, `~/services/vibed-infra`, product dirs under `~/services/<app>/`.
|
|
69
69
|
|
|
70
|
-
## 5.
|
|
70
|
+
## 5. App data ownership — named volumes (default)
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
API data, persist logs, and worker logs use **Docker named volumes** by default (`{container}-data`, `{container}-persist`, `{container}-logs`). They are not under `~/services`, so the operator does **not** chown them to uid 1000 and does not need a `u:1000` ACL for SQLite/WALs.
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
In-volume ownership comes from the **app image** on first empty mount. Digest-gated recreates remount the same volume — the database is not wiped.
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
### Inspect data / logs over SSH
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
```bash
|
|
79
|
+
~/services/vibed-infra/monitor-vibed.sh
|
|
80
|
+
# non-interactive:
|
|
81
|
+
~/services/vibed-infra/monitor-vibed.sh --list
|
|
82
|
+
~/services/vibed-infra/monitor-vibed.sh --summary myapp-api
|
|
83
|
+
```
|
|
79
84
|
|
|
80
|
-
|
|
85
|
+
Or:
|
|
81
86
|
|
|
82
87
|
```bash
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
install -d -o cloud_agent -g cloud_agent /home/cloud_agent/services
|
|
86
|
-
setfacl -m u:1000:rwx /home/cloud_agent/services
|
|
87
|
-
setfacl -d -m u:1000:rwx /home/cloud_agent/services
|
|
88
|
-
# also grant the operator full access via ACL defaults
|
|
89
|
-
setfacl -m u:cloud_agent:rwx /home/cloud_agent/services
|
|
90
|
-
setfacl -d -m u:cloud_agent:rwx /home/cloud_agent/services
|
|
88
|
+
docker run --rm -v myapp-api-data:/data:ro alpine ls -la /data
|
|
89
|
+
docker run --rm -v myapp-api-persist:/persist-logs:ro alpine ls -laR /persist-logs
|
|
91
90
|
```
|
|
92
91
|
|
|
93
|
-
###
|
|
92
|
+
### Optional: bind-mount escape hatch
|
|
93
|
+
|
|
94
|
+
`DATA_BIND=1` / `PERSIST_LOG_BIND=1` / `LOGS_BIND=1` with a host path. Only then does host uid matter for those dirs. Prefer named volumes.
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
### Install-dir scripts under `~/services`
|
|
97
|
+
|
|
98
|
+
Gateway certs, product `.env`, and yaml configs still live under `~/services`. Prefer operator uid 1000 **or** a one-time ACL on `~/services` so the agent can write install trees — not for app SQLite.
|
|
96
99
|
|
|
97
100
|
```bash
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
# optional ACL for install scripts only (operator not uid 1000)
|
|
102
|
+
apt-get install -y acl
|
|
103
|
+
install -d -o cloud_agent -g cloud_agent /home/cloud_agent/services
|
|
104
|
+
setfacl -m u:cloud_agent:rwx -d -m u:cloud_agent:rwx /home/cloud_agent/services
|
|
102
105
|
```
|
|
103
106
|
|
|
104
|
-
Do not rely on per-product `chown` as ongoing ops. Prefer fixing the host model above.
|
|
105
|
-
|
|
106
107
|
## 6. Ports + DNS
|
|
107
108
|
|
|
108
109
|
- Free **80/443** for the host gateway (only vps-gateway binds them)
|
|
@@ -129,6 +130,6 @@ wget -qO- https://raw.githubusercontent.com/ORG/REPO/main/deploy/.../dist/instal
|
|
|
129
130
|
- [ ] `ssh cloud_agent@VPS` works with key only
|
|
130
131
|
- [ ] `docker ps` works without sudo
|
|
131
132
|
- [ ] `$HOME` owned by `cloud_agent`
|
|
132
|
-
- [ ] operator uid is 1000 **OR** `~/services` has default ACL for `u:1000` (and `u:cloud_agent` if needed)
|
|
133
133
|
- [ ] DNS → VPS IP
|
|
134
134
|
- [ ] install-gateway + setup-tls + start-gateway
|
|
135
|
+
- [ ] `~/services/vibed-infra/monitor-vibed.sh --list` shows deployments after start
|
|
@@ -14,18 +14,28 @@ Each service writes **domain events** to a durable local log. After DB loss (or
|
|
|
14
14
|
|
|
15
15
|
## Paths
|
|
16
16
|
|
|
17
|
-
- Machine
|
|
18
|
-
- Per
|
|
17
|
+
- Machine config (credentials + host cron leftover): `~/services/vibed-infra/persist-logs/` (one-time `install-persist-logs.sh`)
|
|
18
|
+
- Per deployment: Docker **named volume** `${DOCKER_NAME}-persist` mounted at `/persist-logs` (default). Set `PERSIST_LOGS=0` to disable. Bind escape: `PERSIST_LOG_BIND=1` + `PERSIST_LOG_DIR=…`.
|
|
19
|
+
- **Ship sidecar** `${DOCKER_NAME}-persist-ship` shares only that persist volume (no Docker socket, no app `/data`). Uses machine `.env` for R2/S3; app container does not get those secrets.
|
|
20
|
+
- Object keys: `{machine}/{PERSIST_SHIP_PREFIX}/{rel}` (prefix defaults to the app container name).
|
|
19
21
|
|
|
20
22
|
## Algorithm
|
|
21
23
|
|
|
22
24
|
1. **Append** NDJSON to `wal.ndjson` with fsync (fast, free).
|
|
23
25
|
2. **Seal** → `seg-NNNN.ndjson.gz` at ~8 MiB or ~60s.
|
|
24
|
-
3. **Ship** sealed segments with one PUT each (`PERSIST_SHIP=1` + R2/S3 env). Default `PERSIST_SHIP=0` (local only).
|
|
26
|
+
3. **Ship** sealed segments with one PUT each (`PERSIST_SHIP=1` + R2/S3 env). Default `PERSIST_SHIP=0` (local only). Sidecar prefers stdlib SigV4; host cron may use `aws` CLI.
|
|
25
27
|
4. **Replay** sealed + wal in order.
|
|
26
28
|
|
|
27
29
|
Python: `lib/persistlog` — `append`, `seal`, `iter_events`, `replay`.
|
|
28
30
|
|
|
31
|
+
## Inspect on the VPS
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
~/services/vibed-infra/monitor-vibed.sh
|
|
35
|
+
# or:
|
|
36
|
+
docker run --rm -v "${DOCKER_NAME}-persist:/persist-logs:ro" alpine ls -laR /persist-logs
|
|
37
|
+
```
|
|
38
|
+
|
|
29
39
|
## Event schema
|
|
30
40
|
|
|
31
41
|
```json
|