vibed-infra 0.4.0 → 0.6.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.
Files changed (37) hide show
  1. package/README.md +37 -60
  2. package/install-auto-update.sh +17 -1
  3. package/install.sh +145 -13
  4. package/lib/__pycache__/load_config.cpython-312.pyc +0 -0
  5. package/lib/__pycache__/product_config.cpython-312.pyc +0 -0
  6. package/lib/env.sh +29 -0
  7. package/lib/generate.py +34 -3
  8. package/lib/host_gateway.sh +115 -0
  9. package/lib/package.py +48 -2
  10. package/lib/persistlog/__init__.py +123 -0
  11. package/lib/persistlog/__pycache__/__init__.cpython-312.pyc +0 -0
  12. package/lib/persistlog/ship.py +69 -0
  13. package/lib/product_config.py +5 -3
  14. package/lib/update_queue.sh +53 -0
  15. package/package.json +1 -1
  16. package/schema/packageconfig.md +23 -39
  17. package/skills/dns-configure/SKILL.md +16 -0
  18. package/skills/infra-update-agent/SKILL.md +40 -0
  19. package/skills/persist-logs/SKILL.md +64 -0
  20. package/skills/system-gateway/SKILL.md +30 -27
  21. package/templates/generic/DNS-SKILL.md.tmpl +40 -0
  22. package/templates/generic/start-api.sh +8 -0
  23. package/templates/generic/update-api.sh +6 -5
  24. package/templates/generic/update-gateway.sh +6 -5
  25. package/templates/generic/update-nodes.sh +5 -4
  26. package/templates/generic/update-ui.sh +6 -5
  27. package/templates/host-gateway/.env.example +15 -0
  28. package/templates/host-gateway/gateway/conf.d/00-default.conf +16 -0
  29. package/templates/host-gateway/gateway/nginx.conf +25 -0
  30. package/templates/host-gateway/reload-gateway.sh +7 -0
  31. package/templates/host-gateway/start-gateway.sh +108 -0
  32. package/templates/host-gateway/update-gateway.sh +41 -0
  33. package/templates/persist-logs/install-persist-logs.sh +38 -0
  34. package/templates/update-agent/agent.sh +87 -0
  35. package/templates/update-agent/enqueue.sh +54 -0
  36. package/templates/update-agent/install-agent.sh +60 -0
  37. package/templates/update-agent/webhook_server.py +132 -0
package/README.md CHANGED
@@ -1,98 +1,75 @@
1
1
  # vibed-infra
2
2
 
3
- Product-agnostic VPS deployment packager: **`package.sh`** turns four YAML configs into a committed **`dist/`** tree operators wget on the box install scripts, generic start/update helpers, env examples, and gateway nginx.
3
+ Product-agnostic VPS packager: **`package.sh`** builds committed **`dist/`** for wget install. One **host gateway** serves many apps; a **serial update agent** pulls images without overload; **persist-logs** enable event-sourced recovery; **`dist/DNS-SKILL.md`** configures DNS via an AU browser agent.
4
4
 
5
5
  Published on npm as **`vibed-infra`**.
6
6
 
7
7
  ## Product maintainer flow
8
8
 
9
- Author **`templates/`** in your product repo:
10
-
11
9
  | File | Purpose |
12
10
  |------|---------|
13
- | `vibed-infra-config.yml` | Product name, template refs, edge network, gateway `sites[]`, auto-update |
14
- | `api-config.yaml` | API image, port, opaque `config:` |
15
- | `ui-config.yaml` | UI image, port |
16
- | `nodes-config.yaml` | Worker image, opaque `config:` |
17
-
18
- Then package and commit **`dist/`**:
11
+ | `vibed-infra-config.yml` | Name, templates, `network.edge` (default `vps-edge`), `gateway.sites[]`, auto-update |
12
+ | `api-config.yaml` / `ui-config.yaml` / `nodes-config.yaml` | Images + opaque config |
19
13
 
20
14
  ```bash
21
- ./package.sh # exec vibed-infra packager → writes dist/
15
+ ./package.sh
22
16
  git add dist && git commit && git push
17
+ # Copy dist/DNS-SKILL.md into AU agent browser extension; give it the VPS IP
23
18
  ```
24
19
 
25
- Resolve the packager from npm:
20
+ ## Operator flow (VPS)
26
21
 
27
22
  ```bash
28
- node -e "console.log(require('path').dirname(require.resolve('vibed-infra/package.json')))"
29
- ```
30
-
31
- Example product: [`examples/vps-hello`](examples/vps-hello) (`app/`, `build-images.sh`, four YAMLs, committed `dist/`).
32
-
33
- ## Operator flow (wget on VPS)
34
-
35
- ```bash
36
- wget -qO- https://raw.githubusercontent.com/ORG/REPO/main/dist/install-api.sh | bash
37
- wget -qO- .../dist/install-nodes.sh | bash
23
+ wget -qO- .../dist/install-api.sh | bash
38
24
  wget -qO- .../dist/install-ui.sh | bash
39
- wget -qO- .../dist/install-gateway.sh | bash
40
- # edit each install dir .env, then ./start-*.sh
25
+ wget -qO- .../dist/install-nodes.sh | bash
26
+ wget -qO- .../dist/install-gateway.sh | bash # bootstraps ~/services/gateway once + apps/{name}/
41
27
  ```
42
28
 
43
29
  | Profile | Role |
44
30
  |---------|------|
45
- | `api` | Backend on shared edge network |
46
- | `ui` | UI container (separate install) |
47
- | `nodes` | Worker compose on edge network |
48
- | `gateway` | HTTPS nginx only (API + UI must already run) |
31
+ | `api` / `ui` / `nodes` | Join shared `vps-edge` |
32
+ | `gateway` | Host nginx + this app’s `sites.conf` under `apps/` |
33
+
34
+ Multi-app: further products’ `install-gateway.sh` only add `apps/{other}/sites.conf` and reload no second 80/443 bind.
35
+
36
+ ## Machine services (installed once)
37
+
38
+ | Path | Purpose |
39
+ |------|---------|
40
+ | `~/services/gateway` | Host nginx (`GATEWAY_HOME`) |
41
+ | `~/services/vibed-infra/update-agent` | Serial pull queue + optional GHCR webhook |
42
+ | `~/services/vibed-infra/persist-logs` | Per-app WALs + optional R2/S3 ship |
43
+
44
+ Auto-update cron **enqueues** work; the agent processes one job at a time. Updates prune dangling images (`DOCKER_AUTO_PRUNE=1`).
49
45
 
50
46
  ## Layout
51
47
 
52
48
  | Path | Purpose |
53
49
  |------|---------|
54
- | `package.sh` / `lib/package.py` | Build product `dist/` from four YAMLs + generic templates |
55
- | `install.sh` | Single entrypoint (`INFRA_PROFILE` or `--profile`) |
56
- | `install-auto-update.sh` | Cron from profile flags |
57
- | `lib/` | fetch, env, tls, prompt, generate, product_config |
58
- | `templates/generic/` | Generic start/update/env/certs/gateway |
59
- | `schema/packageconfig.md` | Four-file product schema + compiled packageconfig |
60
- | `examples/vps-hello/` | Full example with dist e2e tests |
61
- | `skills/` | Cursor skills |
62
- | `github/workflows/` | Reusable GHCR build workflow |
50
+ | `package.sh` / `lib/package.py` | Build `dist/` |
51
+ | `templates/host-gateway/` | Shared gateway skeleton |
52
+ | `templates/update-agent/` | Queue agent + webhook |
53
+ | `templates/persist-logs/` | Shipper install |
54
+ | `lib/persistlog/` | Python append / seal / replay |
55
+ | `skills/` | system-gateway, infra-update-agent, persist-logs, dns-configure |
63
56
 
64
57
  ## Environment
65
58
 
66
59
  | Variable | Meaning |
67
60
  |----------|---------|
68
- | `PACKAGER_RAW` | Base URL or local path to this package root |
69
- | `PACKAGECONFIG_URL` | Product `packageconfig.yaml` or `dist/` URL (URL or path) |
70
- | `PRODUCT_RAW` | Product `dist/` base (templates + install scripts) |
71
- | `INFRA_PROFILE` | `api`, `ui`, `nodes`, or `gateway` |
72
- | `INSTALL_DIR` | Target directory (default `.`) |
61
+ | `GATEWAY_HOME` | Host gateway dir (default `~/services/gateway`) |
62
+ | `VIBED_HOME` | Machine vibed root (default `~/services/vibed-infra`) |
63
+ | `PERSIST_LOG_DIR` | Per-service event log dir |
64
+ | `DOCKER_AUTO_PRUNE` | Prune dangling images after update (default on) |
73
65
 
74
66
  ## Tests
75
67
 
76
68
  ```bash
77
- npm test # package + dry-run all dist/install-*.sh
78
- npm run test:dist # build images + e2e api, ui, nodes (sequential)
79
- ./examples/vps-hello/test-dist.sh --profile api # single profile
69
+ npm test
70
+ npm run test:dist
80
71
  ```
81
72
 
82
- CI: `validate` job then parallel `dist-e2e` matrix (`api`, `ui`, `nodes`).
83
-
84
- ## Publishing (npm)
85
-
86
- Every **merge to `main`** runs [Publish npm](.github/workflows/publish.yml):
87
-
88
- 1. Bumps **minor** (`0.1.0` → `0.2.0`)
89
- 2. Commits `chore: release vibed-infra v…`, tags `v…`, pushes
90
- 3. Runs `npm publish` (needs repo secret `NPM_TOKEN`)
91
-
92
- **Major versions** — set `version` in `package.json` yourself (e.g. `1.0.0`) and include **`[major]`** in the commit message. That publishes the pinned version with **no** automatic minor bump.
93
-
94
- Set repository secret **`NPM_TOKEN`** before the first merge to `main`.
95
-
96
- Ordinary PRs only run CI (`npm test` + dist e2e); they do **not** publish.
73
+ ## Publishing
97
74
 
98
- Runbook: [`examples/vps-hello/README.md`](examples/vps-hello/README.md).
75
+ Merge to `main` runs Publish npm (minor bump). Set `NPM_TOKEN`. Ordinary PRs run CI only.
@@ -88,7 +88,23 @@ build_cron_minutes() {
88
88
  }
89
89
  CRON_MINUTES="$(build_cron_minutes "${CRON_OFFSET:-0}" "${INTERVAL_MIN:-30}")"
90
90
  CRON_SCHED="${CRON_MINUTES} * * * *"
91
- CRON_CMD="cd ${SCRIPT_DIR} && /bin/bash ${SCRIPT_DIR}/${UPDATE_SCRIPT} >/dev/null 2>&1"
91
+ # Prefer shared update-agent queue when available; else direct update script.
92
+ AGENT_ENQUEUE=""
93
+ for cand in \
94
+ "${VIBED_HOME:-}/update-agent/enqueue.sh" \
95
+ "${HOME:-}/services/vibed-infra/update-agent/enqueue.sh" \
96
+ "/var/lib/vibed/update-agent/enqueue.sh"
97
+ do
98
+ if [[ -n "$cand" && -x "$cand" ]]; then
99
+ AGENT_ENQUEUE="$cand"
100
+ break
101
+ fi
102
+ done
103
+ if [[ -n "$AGENT_ENQUEUE" ]]; then
104
+ CRON_CMD="cd ${SCRIPT_DIR} && UPDATE_REASON=cron /bin/bash ${AGENT_ENQUEUE} ${SCRIPT_DIR} >/dev/null 2>&1"
105
+ else
106
+ CRON_CMD="cd ${SCRIPT_DIR} && /bin/bash ${SCRIPT_DIR}/${UPDATE_SCRIPT} >/dev/null 2>&1"
107
+ fi
92
108
  dir_slug="$(basename "$SCRIPT_DIR" | tr -c 'A-Za-z0-9._-' '_')"
93
109
  CRON_D_FILE="/etc/cron.d/infra-${ROLE}-${dir_slug}"
94
110
 
package/install.sh CHANGED
@@ -57,7 +57,7 @@ LOCAL_PACKAGER=0
57
57
  if [[ "$PACKAGER_RAW" =~ ^/ ]] && [[ -d "$PACKAGER_RAW/lib" ]]; then
58
58
  LOCAL_PACKAGER=1
59
59
  fi
60
- for lib in fetch.sh env.sh prompt.sh tls.sh load_config.py generate.py; do
60
+ for lib in fetch.sh env.sh prompt.sh tls.sh load_config.py generate.py host_gateway.sh; do
61
61
  if [[ "$LOCAL_PACKAGER" == "1" ]]; then
62
62
  cp -f "${PACKAGER_RAW}/lib/${lib}" "${INFRA_LIB}/${lib}"
63
63
  else
@@ -79,6 +79,8 @@ source "${INFRA_LIB}/fetch.sh"
79
79
  source "${INFRA_LIB}/prompt.sh"
80
80
  # shellcheck source=/dev/null
81
81
  source "${INFRA_LIB}/tls.sh"
82
+ # shellcheck source=/dev/null
83
+ source "${INFRA_LIB}/host_gateway.sh"
82
84
 
83
85
  cd "$DEST"
84
86
 
@@ -203,15 +205,85 @@ for e in extras:
203
205
  done
204
206
 
205
207
  if [[ "$ROLE" == "gateway" ]]; then
206
- mkdir -p "$DEST/gateway/conf.d"
207
- infra_write_if_missing "${PRODUCT_RAW}/gateway/nginx.conf" "$DEST/gateway/nginx.conf" 0
208
- infra_write_template "${PRODUCT_RAW}/gateway/nginx.conf" "$DEST/gateway/nginx.conf" 0
209
- if python3 "${INFRA_LIB}/generate.py" "$PC_LOCAL" --profile "$PROFILE" -o "$DEST/gateway/conf.d/domains.conf" 2>/dev/null; then
210
- echo "generated: gateway/conf.d/domains.conf"
211
- else
212
- infra_write_if_missing "${PRODUCT_RAW}/gateway/conf.d/domains.conf" "$DEST/gateway/conf.d/domains.conf" 0
213
- infra_write_template "${PRODUCT_RAW}/gateway/conf.d/domains.conf" "$DEST/gateway/conf.d/domains.conf" 0
208
+ PRODUCT_NAME="$(
209
+ python3 -c "
210
+ import sys
211
+ sys.path.insert(0, '${INFRA_LIB}')
212
+ from load_config import load_packageconfig
213
+ from pathlib import Path
214
+ c = load_packageconfig(Path('${PC_LOCAL}'))
215
+ print(c.get('name') or 'app')
216
+ "
217
+ )"
218
+ GATEWAY_HOME="${GATEWAY_HOME:-$(vibed_gateway_home)}"
219
+ export GATEWAY_HOME
220
+ mkdir -p "$DEST/gateway/conf.d" "$DEST/lib"
221
+
222
+ # Phase 1: bootstrap shared host gateway once
223
+ vibed_bootstrap_host_gateway "$PACKAGER_RAW"
224
+ if [[ "$LOCAL_PACKAGER" == "1" ]]; then
225
+ cp -f "${PACKAGER_RAW}/lib/env.sh" "${GATEWAY_HOME}/lib-env.sh"
226
+ cp -f "${PACKAGER_RAW}/lib/host_gateway.sh" "${GATEWAY_HOME}/lib/host_gateway.sh" 2>/dev/null || \
227
+ mkdir -p "${GATEWAY_HOME}/lib" && cp -f "${PACKAGER_RAW}/lib/host_gateway.sh" "${GATEWAY_HOME}/lib/host_gateway.sh"
214
228
  fi
229
+
230
+ # Phase 2: app extension — sites.conf under host apps/{name}/
231
+ SITES_TMP="$DEST/gateway/sites.conf"
232
+ python3 "${INFRA_LIB}/generate.py" "$PC_LOCAL" --profile "$PROFILE" --mode app -o "$SITES_TMP"
233
+ META_TMP="$DEST/gateway/meta.json"
234
+ python3 -c "
235
+ import json, sys
236
+ from pathlib import Path
237
+ sys.path.insert(0, '${INFRA_LIB}')
238
+ from load_config import load_packageconfig, get_profile
239
+ c = load_packageconfig(Path('${PC_LOCAL}'))
240
+ p = get_profile(c, '${PROFILE}')
241
+ meta = {
242
+ 'name': c.get('name'),
243
+ 'installDir': '${DEST}',
244
+ 'sites': p.get('sites') or [],
245
+ 'images': c.get('images') or {},
246
+ }
247
+ Path('${META_TMP}').write_text(json.dumps(meta, indent=2) + '\n')
248
+ "
249
+ vibed_install_app_sites "$PRODUCT_NAME" "$SITES_TMP" "$META_TMP"
250
+
251
+ # Product install dir keeps thin wrappers that point at host gateway
252
+ infra_write_if_missing "${PRODUCT_RAW}/gateway/nginx.conf" "$DEST/gateway/nginx.conf" 0
253
+ cat >"$DEST/start-gateway.sh" <<EOF
254
+ #!/usr/bin/env bash
255
+ set -euo pipefail
256
+ export GATEWAY_HOME="${GATEWAY_HOME}"
257
+ # shellcheck source=/dev/null
258
+ source "\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)/.infra-lib/host_gateway.sh" 2>/dev/null \\
259
+ || source "${GATEWAY_HOME}/lib/host_gateway.sh"
260
+ vibed_reload_or_start_gateway
261
+ EOF
262
+ cat >"$DEST/update-gateway.sh" <<EOF
263
+ #!/usr/bin/env bash
264
+ set -euo pipefail
265
+ SCRIPT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
266
+ cd "\$SCRIPT_DIR"
267
+ # shellcheck source=lib-env.sh
268
+ [[ -f lib-env.sh ]] && source lib-env.sh && load_dotenv .env
269
+ export GATEWAY_HOME="${GATEWAY_HOME}"
270
+ # Regenerate this app's sites and reload host (image update is host's update-gateway.sh)
271
+ if [[ -f .packageconfig.yaml ]]; then
272
+ python3 "\${SCRIPT_DIR}/.infra-lib/generate.py" .packageconfig.yaml --profile gateway --mode app \\
273
+ -o "\${GATEWAY_HOME}/apps/${PRODUCT_NAME}/sites.conf" 2>/dev/null \\
274
+ || python3 "\${GATEWAY_HOME}/../vibed-infra/lib/generate.py" .packageconfig.yaml --profile gateway --mode app \\
275
+ -o "\${GATEWAY_HOME}/apps/${PRODUCT_NAME}/sites.conf"
276
+ fi
277
+ # shellcheck source=/dev/null
278
+ source "\${SCRIPT_DIR}/.infra-lib/host_gateway.sh" 2>/dev/null || source "${GATEWAY_HOME}/lib/host_gateway.sh"
279
+ vibed_reload_or_start_gateway
280
+ EOF
281
+ chmod +x "$DEST/start-gateway.sh" "$DEST/update-gateway.sh"
282
+ # generate.py / host_gateway.sh already live in .infra-lib from bootstrap
283
+ cp -f "${INFRA_LIB}/host_gateway.sh" "$DEST/.infra-lib/host_gateway.sh" 2>/dev/null || true
284
+
285
+ # Ensure host network name is in product .env example path after copy
286
+ echo "GATEWAY_HOME=${GATEWAY_HOME}" >>"$DEST/.env.example" 2>/dev/null || true
215
287
  fi
216
288
 
217
289
  if [[ ! -f "$DEST/.env" ]]; then
@@ -221,6 +293,22 @@ else
221
293
  echo "exists: $DEST/.env"
222
294
  fi
223
295
 
296
+ # Align product DOCKER_NETWORK with host edge when gateway/host exists
297
+ if vibed_host_gateway_ready 2>/dev/null; then
298
+ HG="$(vibed_gateway_home)"
299
+ if [[ -f "$HG/.env" ]]; then
300
+ HOST_NET="$(grep -E '^DOCKER_NETWORK=' "$HG/.env" | head -1 | cut -d= -f2- || true)"
301
+ if [[ -n "$HOST_NET" && -f "$DEST/.env" ]]; then
302
+ if grep -q '^DOCKER_NETWORK=' "$DEST/.env"; then
303
+ sed -i "s|^DOCKER_NETWORK=.*|DOCKER_NETWORK=${HOST_NET}|" "$DEST/.env"
304
+ else
305
+ echo "DOCKER_NETWORK=${HOST_NET}" >>"$DEST/.env"
306
+ fi
307
+ echo "joined host edge network: $HOST_NET"
308
+ fi
309
+ fi
310
+ fi
311
+
224
312
  cat >"$DEST/.infra-profile" <<EOF
225
313
  PROFILE=${PROFILE}
226
314
  ROLE=${ROLE}
@@ -229,12 +317,26 @@ UPDATE_SCRIPT=${UPDATE_SCRIPT}
229
317
  EOF
230
318
 
231
319
  chmod +x "$DEST/${START_SCRIPT}" 2>/dev/null || true
320
+
321
+ # Ensure update-agent + persist-logs on first install (idempotent)
322
+ if [[ -f "${PACKAGER_RAW}/templates/update-agent/install-agent.sh" ]]; then
323
+ PACKAGER_RAW="$PACKAGER_RAW" VIBED_HOME="${VIBED_HOME:-}" bash "${PACKAGER_RAW}/templates/update-agent/install-agent.sh" || true
324
+ fi
325
+ if [[ -f "${PACKAGER_RAW}/templates/persist-logs/install-persist-logs.sh" ]]; then
326
+ PACKAGER_RAW="$PACKAGER_RAW" VIBED_HOME="${VIBED_HOME:-}" bash "${PACKAGER_RAW}/templates/persist-logs/install-persist-logs.sh" || true
327
+ fi
328
+
232
329
  INFRA_PROFILE="$PROFILE" ./install-auto-update.sh || true
233
330
 
234
331
  if [[ "$ROLE" == "gateway" ]]; then
235
332
  # shellcheck source=/dev/null
236
333
  source "$DEST/lib-env.sh"
237
334
  load_dotenv "$DEST/.env"
335
+ HG="$(vibed_gateway_home)"
336
+ # Prefer host certs
337
+ if [[ -f "$HG/.env" ]]; then
338
+ load_dotenv "$HG/.env"
339
+ fi
238
340
  CERT_DIR="$(python3 -c "
239
341
  import sys
240
342
  sys.path.insert(0, '${INFRA_LIB}')
@@ -243,13 +345,37 @@ from pathlib import Path
243
345
  p = get_profile(load_packageconfig(Path('${PC_LOCAL}')), '${PROFILE}')
244
346
  sites = p.get('sites') or []
245
347
  if not sites:
246
- print('/etc/letsencrypt/live/example.com')
348
+ print('${HG}/certs')
247
349
  else:
248
350
  s = sites[0]
249
- print(s.get('tlsCertDir') or ('/etc/letsencrypt/live/' + s['host']))
351
+ print(s.get('tlsCertDir') or ('${HG}/certs'))
250
352
  ")"
251
353
  TLS_FULLCHAIN="${TLS_FULLCHAIN:-${CERT_DIR}/fullchain.pem}"
252
354
  TLS_PRIVKEY="${TLS_PRIVKEY:-${CERT_DIR}/privkey.pem}"
355
+ # Prefer writable host certs when packaged tlsCertDir is not writable (e.g. /etc/letsencrypt)
356
+ if [[ ! -f "$TLS_FULLCHAIN" ]]; then
357
+ HOST_CERT_DIR="${HG}/certs"
358
+ mkdir -p "$HOST_CERT_DIR" 2>/dev/null || true
359
+ if [[ -x "${PRODUCT_RAW}/gen-dev-certs.sh" ]]; then
360
+ APP_HOST="$(python3 -c "
361
+ import sys
362
+ sys.path.insert(0, '${INFRA_LIB}')
363
+ from load_config import load_packageconfig, get_profile
364
+ from pathlib import Path
365
+ p = get_profile(load_packageconfig(Path('${PC_LOCAL}')), '${PROFILE}')
366
+ sites = p.get('sites') or []
367
+ print(sites[0]['host'] if sites else 'localhost')
368
+ ")"
369
+ if APP_HOST="$APP_HOST" bash "${PRODUCT_RAW}/gen-dev-certs.sh" "$HOST_CERT_DIR" 2>/dev/null; then
370
+ TLS_FULLCHAIN="${HOST_CERT_DIR}/fullchain.pem"
371
+ TLS_PRIVKEY="${HOST_CERT_DIR}/privkey.pem"
372
+ fi
373
+ fi
374
+ fi
375
+ if [[ -f "$HG/.env" ]]; then
376
+ sed -i "s|^TLS_FULLCHAIN=.*|TLS_FULLCHAIN=${TLS_FULLCHAIN}|" "$HG/.env" 2>/dev/null || true
377
+ sed -i "s|^TLS_PRIVKEY=.*|TLS_PRIVKEY=${TLS_PRIVKEY}|" "$HG/.env" 2>/dev/null || true
378
+ fi
253
379
  mapfile -t DOMAIN_ARR < <(python3 -c "
254
380
  import sys
255
381
  sys.path.insert(0, '${INFRA_LIB}')
@@ -261,8 +387,14 @@ for s in p.get('sites') or []:
261
387
  for a in s.get('aliases') or []:
262
388
  print(a)
263
389
  ")
264
- infra_tls_offer_interactive "$TLS_FULLCHAIN" "$TLS_PRIVKEY" "${DOMAIN_ARR[@]}"
390
+ infra_tls_offer_interactive "$TLS_FULLCHAIN" "$TLS_PRIVKEY" "${DOMAIN_ARR[@]}" || true
265
391
  fi
266
392
 
267
393
  echo ""
268
- echo "Install complete. Start: cd $DEST && ./${START_SCRIPT}"
394
+ if [[ "$ROLE" == "gateway" ]]; then
395
+ echo "Install complete. Host gateway: $(vibed_gateway_home)"
396
+ echo "App sites: $(vibed_gateway_home)/apps/${PRODUCT_NAME:-app}/sites.conf"
397
+ echo "Start: cd $DEST && ./start-gateway.sh"
398
+ else
399
+ echo "Install complete. Start: cd $DEST && ./${START_SCRIPT}"
400
+ fi
package/lib/env.sh CHANGED
@@ -222,3 +222,32 @@ chown_data_dir() {
222
222
  fi
223
223
  chmod 755 "$dir" 2>/dev/null || true
224
224
  }
225
+
226
+ # Default on during auto-update. Set DOCKER_AUTO_PRUNE=0 to disable.
227
+ docker_auto_prune_on() {
228
+ if [[ -n "${DOCKER_AUTO_PRUNE+x}" ]]; then
229
+ env_flag_on DOCKER_AUTO_PRUNE
230
+ return
231
+ fi
232
+ return 0
233
+ }
234
+
235
+ # Drop dangling images left after retagged pulls (e.g. :main). Optionally also
236
+ # remove unused tagged images older than DOCKER_PRUNE_UNTIL when DOCKER_PRUNE_UNUSED=1.
237
+ prune_docker_images() {
238
+ local log_dir="${1:-.}"
239
+ local role_label="${2:-infra}"
240
+ docker_auto_prune_on || return 0
241
+ if ! command -v docker >/dev/null 2>&1; then
242
+ return 0
243
+ fi
244
+ local summary
245
+ summary="$(docker image prune -f 2>/dev/null | awk '/Total reclaimed space/{print; found=1} END{if(!found) print "done"}' || echo done)"
246
+ log_update "$log_dir" "${role_label}: prune dangling images — ${summary}"
247
+ if env_flag_on DOCKER_PRUNE_UNUSED; then
248
+ local until="${DOCKER_PRUNE_UNTIL:-72h}"
249
+ summary="$(docker image prune -af --filter "until=${until}" 2>/dev/null \
250
+ | awk '/Total reclaimed space/{print; found=1} END{if(!found) print "done"}' || echo done)"
251
+ log_update "$log_dir" "${role_label}: prune unused images (until=${until}) — ${summary}"
252
+ fi
253
+ }
package/lib/generate.py CHANGED
@@ -22,6 +22,8 @@ def render_site(site: dict) -> str:
22
22
  create = site.get("createPath") or ""
23
23
  be_var = backend.replace("-", "_")
24
24
  ui_var = ui.replace("-", "_")
25
+ cert = site.get("sslCertificate") or "/etc/nginx/certs/fullchain.pem"
26
+ key = site.get("sslCertificateKey") or "/etc/nginx/certs/privkey.pem"
25
27
  proxy_headers = f""" proxy_pass http://${be_var}_upstream;
26
28
  proxy_set_header Host $host;
27
29
  proxy_set_header X-Real-IP $remote_addr;
@@ -42,8 +44,8 @@ server {{
42
44
  http2 on;
43
45
  server_name {server_names};
44
46
 
45
- ssl_certificate /etc/nginx/certs/fullchain.pem;
46
- ssl_certificate_key /etc/nginx/certs/privkey.pem;
47
+ ssl_certificate {cert};
48
+ ssl_certificate_key {key};
47
49
  ssl_protocols TLSv1.2 TLSv1.3;
48
50
  ssl_prefer_server_ciphers on;
49
51
 
@@ -78,7 +80,23 @@ server {{
78
80
  """
79
81
 
80
82
 
83
+ def render_app_sites(prof: dict) -> str:
84
+ """Per-app sites.conf (no ACME block — host 00-default.conf owns that)."""
85
+ sites = prof.get("sites") or []
86
+ parts = [
87
+ "# Generated by lib/generate.py — app sites for host gateway apps/{name}/sites.conf",
88
+ "",
89
+ ]
90
+ for site in sites:
91
+ parts.append(render_site(site))
92
+ parts.append("")
93
+ return "\n".join(parts)
94
+
95
+
81
96
  def render_gateway(conf: dict, profile: str, prof: dict, output_name: str = "domains.conf") -> str:
97
+ """Full standalone conf (legacy) or app-only when output is sites.conf."""
98
+ if Path(output_name).name == "sites.conf":
99
+ return render_app_sites(prof)
82
100
  sites = prof.get("sites") or []
83
101
  parts = [
84
102
  "# Generated by lib/generate.py — edit packageconfig sites[] and re-install",
@@ -106,10 +124,23 @@ def main() -> int:
106
124
  ap.add_argument("packageconfig")
107
125
  ap.add_argument("--profile", default="gateway")
108
126
  ap.add_argument("-o", "--output", required=True)
127
+ ap.add_argument(
128
+ "--mode",
129
+ choices=("full", "app"),
130
+ default=None,
131
+ help="full=standalone domains.conf; app=sites.conf for host gateway",
132
+ )
109
133
  args = ap.parse_args()
110
134
  conf = load_packageconfig(Path(args.packageconfig))
111
135
  prof = get_profile(conf, args.profile)
112
- out = render_gateway(conf, args.profile, prof, Path(args.output).name)
136
+ out_name = Path(args.output).name
137
+ mode = args.mode
138
+ if mode is None:
139
+ mode = "app" if out_name == "sites.conf" else "full"
140
+ if mode == "app":
141
+ out = render_app_sites(prof)
142
+ else:
143
+ out = render_gateway(conf, args.profile, prof, out_name)
113
144
  Path(args.output).write_text(out, encoding="utf-8")
114
145
  print(f"wrote {args.output}")
115
146
  return 0
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env bash
2
+ # Host / shared gateway helpers. Sourced by install.sh and start scripts.
3
+ # shellcheck shell=bash
4
+
5
+ vibed_gateway_home() {
6
+ if [[ -n "${GATEWAY_HOME:-}" ]]; then
7
+ echo "$GATEWAY_HOME"
8
+ return 0
9
+ fi
10
+ if [[ -n "${HOME:-}" ]]; then
11
+ echo "${HOME}/services/gateway"
12
+ return 0
13
+ fi
14
+ echo "/var/lib/vibed/gateway"
15
+ }
16
+
17
+ vibed_home() {
18
+ if [[ -n "${VIBED_HOME:-}" ]]; then
19
+ echo "$VIBED_HOME"
20
+ return 0
21
+ fi
22
+ if [[ -n "${HOME:-}" ]]; then
23
+ echo "${HOME}/services/vibed-infra"
24
+ return 0
25
+ fi
26
+ echo "/var/lib/vibed"
27
+ }
28
+
29
+ vibed_host_gateway_ready() {
30
+ local home
31
+ home="$(vibed_gateway_home)"
32
+ [[ -f "${home}/.vibed-host-gateway" ]]
33
+ }
34
+
35
+ vibed_bootstrap_host_gateway() {
36
+ local packager="${1:?packager root or URL}"
37
+ local home
38
+ home="$(vibed_gateway_home)"
39
+ mkdir -p "$home/gateway/conf.d" "$home/apps" "$home/certs" "$home/certbot-www"
40
+
41
+ if [[ -f "${home}/.vibed-host-gateway" ]]; then
42
+ echo "host gateway already present: $home"
43
+ return 0
44
+ fi
45
+
46
+ echo "bootstrapping host gateway at $home"
47
+
48
+ _hg_fetch() {
49
+ local rel="$1" dest="$2"
50
+ mkdir -p "$(dirname "$dest")"
51
+ if [[ "$packager" =~ ^/ ]]; then
52
+ cp -f "${packager}/${rel}" "$dest"
53
+ else
54
+ if command -v curl >/dev/null 2>&1; then
55
+ curl -fsSL "${packager}/${rel}" -o "$dest"
56
+ else
57
+ wget -qO "$dest" "${packager}/${rel}"
58
+ fi
59
+ fi
60
+ }
61
+
62
+ _hg_fetch "templates/host-gateway/gateway/nginx.conf" "$home/gateway/nginx.conf"
63
+ _hg_fetch "templates/host-gateway/gateway/conf.d/00-default.conf" "$home/gateway/conf.d/00-default.conf"
64
+ _hg_fetch "templates/host-gateway/.env.example" "$home/.env.example"
65
+ _hg_fetch "templates/host-gateway/start-gateway.sh" "$home/start-gateway.sh"
66
+ _hg_fetch "templates/host-gateway/reload-gateway.sh" "$home/reload-gateway.sh"
67
+ _hg_fetch "templates/host-gateway/update-gateway.sh" "$home/update-gateway.sh"
68
+ chmod +x "$home/start-gateway.sh" "$home/reload-gateway.sh" "$home/update-gateway.sh"
69
+
70
+ if [[ ! -f "$home/.env" ]]; then
71
+ cp "$home/.env.example" "$home/.env"
72
+ fi
73
+ if [[ -f "${packager}/lib/env.sh" ]] || [[ "$packager" =~ ^/ ]]; then
74
+ if [[ "$packager" =~ ^/ ]]; then
75
+ cp -f "${packager}/lib/env.sh" "$home/lib-env.sh"
76
+ fi
77
+ fi
78
+
79
+ echo "1" >"${home}/.vibed-host-gateway"
80
+ echo "bootstrapped host gateway: $home"
81
+ }
82
+
83
+ vibed_install_app_sites() {
84
+ local app_name="$1"
85
+ local sites_conf_src="$2"
86
+ local meta_json="${3:-}"
87
+ local home
88
+ home="$(vibed_gateway_home)"
89
+ local app_dir="${home}/apps/${app_name}"
90
+ mkdir -p "$app_dir"
91
+ cp -f "$sites_conf_src" "${app_dir}/sites.conf"
92
+ if [[ -n "$meta_json" && -f "$meta_json" ]]; then
93
+ cp -f "$meta_json" "${app_dir}/meta.json"
94
+ fi
95
+ echo "installed app sites: ${app_dir}/sites.conf"
96
+ }
97
+
98
+ vibed_reload_or_start_gateway() {
99
+ local home
100
+ home="$(vibed_gateway_home)"
101
+ # shellcheck source=/dev/null
102
+ [[ -f "$home/lib-env.sh" ]] && source "$home/lib-env.sh"
103
+ # shellcheck disable=SC1091
104
+ [[ -f "$home/.env" ]] && load_dotenv "$home/.env" 2>/dev/null || true
105
+ local name="${GATEWAY_NAME:-vps-gateway}"
106
+ if docker inspect "$name" >/dev/null 2>&1; then
107
+ if [[ -x "$home/reload-gateway.sh" ]]; then
108
+ (cd "$home" && ./reload-gateway.sh)
109
+ else
110
+ (cd "$home" && ./start-gateway.sh)
111
+ fi
112
+ else
113
+ (cd "$home" && ./start-gateway.sh)
114
+ fi
115
+ }