vibed-infra 0.7.0 → 0.9.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 +13 -4
- package/github/__pycache__/notify-vps-pull.cpython-312.pyc +0 -0
- 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/host_gateway.sh +5 -5
- package/lib/package.py +11 -2
- package/lib/persistlog/__pycache__/__init__.cpython-312.pyc +0 -0
- package/lib/tls.sh +11 -20
- package/package.json +1 -1
- package/schema/packageconfig.md +10 -5
- package/skills/infra-cicd/SKILL.md +4 -4
- package/skills/infra-packager/SKILL.md +7 -6
- package/skills/infra-update-agent/SKILL.md +28 -21
- package/skills/system-gateway/SKILL.md +3 -3
- package/templates/host-gateway/.env.example +3 -0
- package/templates/host-gateway/setup-tls.sh +109 -18
- package/templates/host-gateway/start-gateway.sh +7 -7
- package/templates/update-agent/__pycache__/webhook_server.cpython-312.pyc +0 -0
package/README.md
CHANGED
|
@@ -41,10 +41,17 @@ Multi-app: further products’ `install-gateway.sh` only add `apps/{other}/sites
|
|
|
41
41
|
| Path | Purpose |
|
|
42
42
|
|------|---------|
|
|
43
43
|
| `~/services/gateway` | Host nginx (`GATEWAY_HOME`) |
|
|
44
|
-
| `~/services/vibed-infra/update-agent` | Serial pull queue
|
|
44
|
+
| `~/services/vibed-infra/update-agent` | Serial pull queue; GHCR notify via GitHub Actions OIDC |
|
|
45
45
|
| `~/services/vibed-infra/persist-logs` | Per-app WALs + optional R2/S3 ship |
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
## Image updates (docker pull)
|
|
48
|
+
|
|
49
|
+
Install registers each role in the machine **update-agent**. Two paths enqueue work; the agent runs **one pull at a time** (`update-*.sh` is digest-gated — no restart if the image digest is unchanged):
|
|
50
|
+
|
|
51
|
+
1. **Immediate** — push `:main` with the reusable GHCR workflow (`id-token: write`). CI mints an OIDC JWT and POSTs `https://{site host}/_vibed/hooks/ghcr` (fallback `http://{publicIp}/…`). No GitHub webhook UI or `VIBED_WEBHOOK_SECRET`.
|
|
52
|
+
2. **Cron** — if `*_AUTO_UPDATE=1`, periodic enqueue (backup if notify is missed).
|
|
53
|
+
|
|
54
|
+
See [`skills/infra-update-agent/SKILL.md`](skills/infra-update-agent/SKILL.md). After a successful update, dangling images are pruned (`DOCKER_AUTO_PRUNE=1`).
|
|
48
55
|
|
|
49
56
|
## Layout
|
|
50
57
|
|
|
@@ -55,7 +62,7 @@ Auto-update cron **enqueues** work; the agent processes one job at a time. After
|
|
|
55
62
|
| `templates/update-agent/` | Queue agent + webhook |
|
|
56
63
|
| `templates/persist-logs/` | Shipper install |
|
|
57
64
|
| `lib/persistlog/` | Python append / seal / replay |
|
|
58
|
-
| `skills/` | system-gateway, infra-update-agent, persist-logs, dns-configure |
|
|
65
|
+
| `skills/` | system-gateway, infra-update-agent, infra-cicd, infra-packager, persist-logs, dns-configure |
|
|
59
66
|
|
|
60
67
|
## Environment
|
|
61
68
|
|
|
@@ -63,8 +70,9 @@ Auto-update cron **enqueues** work; the agent processes one job at a time. After
|
|
|
63
70
|
|----------|---------|
|
|
64
71
|
| `GATEWAY_HOME` | Host gateway dir (default `~/services/gateway`) |
|
|
65
72
|
| `VIBED_HOME` | Machine vibed root (default `~/services/vibed-infra`) |
|
|
73
|
+
| `VIBED_UPDATE_AGENT` | Override update-agent dir (default `$VIBED_HOME/update-agent`) |
|
|
66
74
|
| `GATEWAY_PUBLIC_IP` | VPS IPv4 (from `gateway.publicIp`) |
|
|
67
|
-
| `TLS_EMAIL` / `TLS_MODE` | Let’s Encrypt email; `lab` or `letsencrypt` |
|
|
75
|
+
| `TLS_EMAIL` / `TLS_MODE` | Let’s Encrypt email; `lab` or `letsencrypt` (docker certbot → `./certs` PEMs; optional `CERTBOT_IMAGE` / `LETSENCRYPT_HOME`) |
|
|
68
76
|
| `PERSIST_LOG_DIR` | Per-service event log dir |
|
|
69
77
|
| `DOCKER_AUTO_PRUNE` | Prune dangling images after update (default on) |
|
|
70
78
|
|
|
@@ -72,6 +80,7 @@ Auto-update cron **enqueues** work; the agent processes one job at a time. After
|
|
|
72
80
|
|
|
73
81
|
```bash
|
|
74
82
|
npm test
|
|
83
|
+
npm run test:oidc # real GitHub OIDC mint; skips locally unless ACTIONS_ID_TOKEN_* set
|
|
75
84
|
npm run test:dist
|
|
76
85
|
npm run test:e2e-multi # two apps, localhost wget|bash, shared host gateway
|
|
77
86
|
```
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/host_gateway.sh
CHANGED
|
@@ -52,9 +52,11 @@ vibed_bootstrap_host_gateway() {
|
|
|
52
52
|
fi
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
# Always refresh
|
|
55
|
+
# Always refresh gateway scripts (idempotent re-install / multi-app)
|
|
56
56
|
_hg_fetch "templates/host-gateway/setup-tls.sh" "$home/setup-tls.sh"
|
|
57
|
-
|
|
57
|
+
_hg_fetch "templates/host-gateway/start-gateway.sh" "$home/start-gateway.sh"
|
|
58
|
+
_hg_fetch "templates/host-gateway/reload-gateway.sh" "$home/reload-gateway.sh"
|
|
59
|
+
chmod +x "$home/setup-tls.sh" "$home/start-gateway.sh" "$home/reload-gateway.sh"
|
|
58
60
|
|
|
59
61
|
if [[ -f "${home}/.vibed-host-gateway" ]]; then
|
|
60
62
|
echo "host gateway already present: $home"
|
|
@@ -66,11 +68,9 @@ vibed_bootstrap_host_gateway() {
|
|
|
66
68
|
_hg_fetch "templates/host-gateway/gateway/nginx.conf" "$home/gateway/nginx.conf"
|
|
67
69
|
_hg_fetch "templates/host-gateway/gateway/conf.d/00-default.conf" "$home/gateway/conf.d/00-default.conf"
|
|
68
70
|
_hg_fetch "templates/host-gateway/.env.example" "$home/.env.example"
|
|
69
|
-
_hg_fetch "templates/host-gateway/start-gateway.sh" "$home/start-gateway.sh"
|
|
70
|
-
_hg_fetch "templates/host-gateway/reload-gateway.sh" "$home/reload-gateway.sh"
|
|
71
71
|
_hg_fetch "templates/host-gateway/update-gateway.sh" "$home/update-gateway.sh"
|
|
72
72
|
_hg_fetch "lib/env.sh" "$home/lib-env.sh"
|
|
73
|
-
chmod +x "$home/
|
|
73
|
+
chmod +x "$home/update-gateway.sh"
|
|
74
74
|
|
|
75
75
|
if [[ ! -f "$home/.env" ]]; then
|
|
76
76
|
cp "$home/.env.example" "$home/.env"
|
package/lib/package.py
CHANGED
|
@@ -100,8 +100,13 @@ DOCKER_PRUNE_UNTIL=72h
|
|
|
100
100
|
def _env_gateway(meta: dict, conf: dict, sites: list) -> str:
|
|
101
101
|
au = conf["profiles"]["gateway"]["autoUpdate"]
|
|
102
102
|
gw = conf["profiles"]["gateway"]
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
# Host gateway stores PEMs under ./certs (setup-tls.sh). tlsCertDir is legacy/docs only
|
|
104
|
+
# unless it already points at a relative ./certs tree.
|
|
105
|
+
cert_dir = "./certs"
|
|
106
|
+
if sites:
|
|
107
|
+
raw = sites[0].get("tlsCertDir") or ""
|
|
108
|
+
if raw.startswith("./certs") or raw.rstrip("/") == "certs":
|
|
109
|
+
cert_dir = raw.rstrip("/")
|
|
105
110
|
flags = au.get("flags") or ["GATEWAY_AUTO_UPDATE"]
|
|
106
111
|
flag_lines = "\n".join(f"{f}=0" for f in flags)
|
|
107
112
|
public_ip = gw.get("publicIp") or meta.get("publicIp") or ""
|
|
@@ -351,6 +356,10 @@ wget -qO- https://raw.githubusercontent.com/ORG/REPO/main/dist/install-gateway.s
|
|
|
351
356
|
Network: `{meta['network']}` (shared host edge). API container: `{meta['apiContainer']}`.
|
|
352
357
|
Gateway install bootstraps `~/services/gateway` once, then drops `apps/{{product}}/sites.conf`.
|
|
353
358
|
|
|
359
|
+
## Image updates
|
|
360
|
+
|
|
361
|
+
wget install registers this product with the machine update-agent. A GHCR `:main` push (reusable workflow + `id-token: write`) POSTs `/_vibed/hooks/ghcr` so the VPS pulls immediately. Cron `*_AUTO_UPDATE` is a backup. See the packager’s infra-update-agent skill.
|
|
362
|
+
|
|
354
363
|
## DNS (AU agent)
|
|
355
364
|
|
|
356
365
|
Copy [`DNS-SKILL.md`](DNS-SKILL.md) into an AU agent browser extension. Domains and `gateway.publicIp` are baked in when configured; confirm the IP with the operator.
|
|
Binary file
|
package/lib/tls.sh
CHANGED
|
@@ -9,22 +9,16 @@ infra_tls_check() {
|
|
|
9
9
|
|
|
10
10
|
infra_tls_suggest_certbot() {
|
|
11
11
|
local domains=("$@")
|
|
12
|
-
local joined=""
|
|
13
|
-
local d
|
|
14
|
-
for d in "${domains[@]}"; do
|
|
15
|
-
joined+=" -d $d"
|
|
16
|
-
done
|
|
17
12
|
cat <<EOF
|
|
18
13
|
|
|
19
|
-
TLS certificates not found.
|
|
14
|
+
TLS certificates not found. From the host gateway directory, issue with:
|
|
20
15
|
|
|
21
|
-
|
|
16
|
+
cd ~/services/gateway && ./setup-tls.sh --force
|
|
22
17
|
|
|
23
|
-
|
|
18
|
+
(setup-tls.sh uses docker certbot when available — no host certbot/sudo required —
|
|
19
|
+
or host certbot when installed. PEMs land in ./certs/.)
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
Then set TLS_FULLCHAIN and TLS_PRIVKEY in .env and run ./start.sh
|
|
21
|
+
Or set TLS_MODE=lab for self-signed lab certs, then ./start.sh
|
|
28
22
|
|
|
29
23
|
EOF
|
|
30
24
|
}
|
|
@@ -41,19 +35,16 @@ infra_tls_offer_interactive() {
|
|
|
41
35
|
if [[ ! -t 0 ]]; then
|
|
42
36
|
return 0
|
|
43
37
|
fi
|
|
44
|
-
|
|
38
|
+
local gw="${GATEWAY_HOME:-${HOME:-}/services/gateway}"
|
|
39
|
+
read -r -p "Run setup-tls.sh in ${gw}? [y/N] " ans
|
|
45
40
|
case "$ans" in
|
|
46
41
|
y|Y|yes|YES)
|
|
47
|
-
if
|
|
48
|
-
|
|
42
|
+
if [[ -x "${gw}/setup-tls.sh" ]]; then
|
|
43
|
+
(cd "$gw" && ./setup-tls.sh --force)
|
|
44
|
+
else
|
|
45
|
+
echo "setup-tls.sh not found at ${gw} — bootstrap the host gateway first" >&2
|
|
49
46
|
return 1
|
|
50
47
|
fi
|
|
51
|
-
local args=()
|
|
52
|
-
local d
|
|
53
|
-
for d in "${domains[@]}"; do
|
|
54
|
-
args+=(-d "$d")
|
|
55
|
-
done
|
|
56
|
-
sudo certbot certonly --standalone "${args[@]}"
|
|
57
48
|
;;
|
|
58
49
|
esac
|
|
59
50
|
}
|
package/package.json
CHANGED
package/schema/packageconfig.md
CHANGED
|
@@ -27,7 +27,7 @@ gateway:
|
|
|
27
27
|
aliases: [www.app.example.com]
|
|
28
28
|
healthPath: /api/health
|
|
29
29
|
createPath: /api/items
|
|
30
|
-
tlsCertDir: /etc/letsencrypt
|
|
30
|
+
tlsCertDir: ./certs # host gateway PEMs; legacy /etc/letsencrypt/... paths ignored in .env.gateway.example
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
Defaults: containers `{name}-api` / `{name}-ui` / `{name}-worker`; host gateway container `vps-gateway`.
|
|
@@ -39,7 +39,7 @@ Compiled `packageconfig.yaml` also includes `webhook.url` / `fallbackUrl` (from
|
|
|
39
39
|
| Path | Purpose |
|
|
40
40
|
|------|---------|
|
|
41
41
|
| `install-*.sh` | wget entrypoints |
|
|
42
|
-
| `notify-vps-pull.py` |
|
|
42
|
+
| `notify-vps-pull.py` | GHCR workflow: mint OIDC JWT and POST webhook URLs from `webhook:` |
|
|
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 |
|
|
@@ -47,11 +47,15 @@ Compiled `packageconfig.yaml` also includes `webhook.url` / `fallbackUrl` (from
|
|
|
47
47
|
|
|
48
48
|
Gateway install writes `$GATEWAY_HOME/apps/{name}/sites.conf` (host-extension mode).
|
|
49
49
|
|
|
50
|
-
## Auto-update
|
|
50
|
+
## Auto-update and docker pull
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Install registers each role with the machine **update-agent**. Pulls are serial and digest-gated (`update-*.sh`).
|
|
53
53
|
|
|
54
|
-
Immediate
|
|
54
|
+
- **Immediate:** reusable GHCR workflow on the default branch mints a GitHub Actions OIDC JWT and POSTs `https://{host}/_vibed/hooks/ghcr` (fallback `http://{publicIp}/_vibed/hooks/ghcr`). Caller needs `id-token: write`. See [`skills/infra-update-agent/SKILL.md`](../skills/infra-update-agent/SKILL.md).
|
|
55
|
+
- **Cron:** when `*_AUTO_UPDATE=1`, cron enqueues on an interval (backup if notify is missed). Fallback: direct `update-*.sh` if the agent is missing.
|
|
56
|
+
- After update: dangling image prune (`DOCKER_AUTO_PRUNE`).
|
|
57
|
+
|
|
58
|
+
`webhook.token` in compiled packageconfig is only for local curl; CI does not need `VIBED_WEBHOOK_SECRET`.
|
|
55
59
|
|
|
56
60
|
## Persist logs
|
|
57
61
|
|
|
@@ -63,6 +67,7 @@ Immediate pulls: install registers the app. The reusable docker-build workflow m
|
|
|
63
67
|
|----------|---------|
|
|
64
68
|
| `GATEWAY_HOME` | Host gateway (default `~/services/gateway`) |
|
|
65
69
|
| `VIBED_HOME` | `~/services/vibed-infra` |
|
|
70
|
+
| `VIBED_UPDATE_AGENT` | Override update-agent dir (default `$VIBED_HOME/update-agent`) |
|
|
66
71
|
| `GATEWAY_PUBLIC_IP` / `TLS_EMAIL` / `TLS_MODE` | Host TLS (`setup-tls.sh`) |
|
|
67
72
|
| `PACKAGER_RAW` / `PRODUCT_RAW` / `PACKAGECONFIG_URL` | as before |
|
|
68
73
|
| `INFRA_PROFILE` | `api`, `ui`, `nodes`, `gateway` |
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
name: infra-cicd
|
|
3
3
|
description: >-
|
|
4
4
|
Set up GitHub Actions CI/CD for infra-packaged products: GHCR image build,
|
|
5
|
-
package validation, and dist e2e
|
|
6
|
-
pipelines for Backend/UI/worker images.
|
|
5
|
+
OIDC notify for immediate VPS pulls, package validation, and dist e2e.
|
|
7
6
|
---
|
|
8
7
|
|
|
9
8
|
# Infra packager CI/CD
|
|
@@ -39,13 +38,14 @@ On the default branch, the reusable workflow mints a GitHub Actions OIDC JWT (au
|
|
|
39
38
|
## Product repo CI
|
|
40
39
|
|
|
41
40
|
1. Run `./package.sh` and fail if `dist/` drifted from templates (optional `git diff --exit-code dist`).
|
|
42
|
-
2. Build/push images to GHCR (reusable workflow notifies the VPS
|
|
41
|
+
2. Build/push images to GHCR (reusable workflow notifies the VPS; requires `id-token: write`).
|
|
43
42
|
3. Optional job: `test-dist.sh --profile api` on a Docker-enabled runner.
|
|
44
43
|
|
|
45
44
|
## Checklist
|
|
46
45
|
|
|
47
46
|
- [ ] Four YAML templates under `templates/` (`gateway.publicIp` / `tlsEmail` / `sites[]` set for production)
|
|
48
47
|
- [ ] Committed `dist/` matches `./package.sh` output (including `DNS-SKILL.md`)
|
|
49
|
-
- [ ] Image names in configs match GHCR
|
|
48
|
+
- [ ] Image names in configs match GHCR (`ghcr.io/{owner}/…` so OIDC `repository_owner` can match)
|
|
49
|
+
- [ ] GHCR reusable job has `permissions.id-token: write`
|
|
50
50
|
- [ ] `npm test` / install dry-run passes
|
|
51
51
|
- [ ] Secrets not in templates — only `.env.*.example` placeholders (publicIp/domain are OK to commit)
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
name: infra-packager
|
|
3
3
|
description: >-
|
|
4
4
|
Wire a new product to the infra VPS packager: four YAML configs, package.sh →
|
|
5
|
-
committed dist/, Docker images, and CI. Use when adding
|
|
6
|
-
deploy to a repo or migrating from ad-hoc
|
|
5
|
+
committed dist/, Docker images, GHCR OIDC pulls, and CI. Use when adding
|
|
6
|
+
Backend/UI/worker deploy to a repo or migrating from ad-hoc install scripts.
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Infra packager — onboard a product
|
|
@@ -17,7 +17,7 @@ description: >-
|
|
|
17
17
|
|
|
18
18
|
1. **Depend on** [`vibed-infra`](https://www.npmjs.com/package/vibed-infra) or clone this repo for `package.sh`.
|
|
19
19
|
|
|
20
|
-
2. **Create** `templates/` with four files — see [`schema/packageconfig.md`](../../schema/packageconfig.md) and [`examples/vps-hello/templates/`](../../examples/vps-hello/templates/). Set `gateway.publicIp`, `gateway.tlsEmail`, and `gateway.sites[].host` for DNS-SKILL + HTTPS.
|
|
20
|
+
2. **Create** `templates/` with four files — see [`schema/packageconfig.md`](../../schema/packageconfig.md) and [`examples/vps-hello/templates/`](../../examples/vps-hello/templates/). Set `gateway.publicIp`, `gateway.tlsEmail`, and `gateway.sites[].host` for DNS-SKILL + HTTPS (those values are also the GHCR notify URLs).
|
|
21
21
|
|
|
22
22
|
3. **Add** a tiny product `package.sh`:
|
|
23
23
|
|
|
@@ -27,7 +27,7 @@ PACKAGER="$(node -e "console.log(require('path').dirname(require.resolve('vibed-
|
|
|
27
27
|
exec bash "$PACKAGER/package.sh" --product "$ROOT" --out "$ROOT/dist"
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
4. **
|
|
30
|
+
4. **Images** — Dockerfiles in `app/`; tag `:local` for dev; GHCR `:main` for prod (names in `*-config.yaml`). Immediate VPS pull uses the reusable GHCR workflow with `id-token: write` — see infra-cicd and infra-update-agent. No extra GitHub secret.
|
|
31
31
|
|
|
32
32
|
5. **Package and commit** `dist/` (includes `DNS-SKILL.md` with domains + public IP):
|
|
33
33
|
|
|
@@ -36,9 +36,9 @@ exec bash "$PACKAGER/package.sh" --product "$ROOT" --out "$ROOT/dist"
|
|
|
36
36
|
git add dist && git commit && git push
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
6. **CI** —
|
|
39
|
+
6. **CI** — product repo: package drift check + GHCR build (notifies VPS). Packager repo: `npm test`, `oidc-webhook-e2e`, `test-dist.sh`, `npm run test:e2e-multi`.
|
|
40
40
|
|
|
41
|
-
7. **VPS** — DNS first (paste `dist/DNS-SKILL.md` into AU agent), then wget:
|
|
41
|
+
7. **VPS** — DNS first (paste `dist/DNS-SKILL.md` into AU agent), then wget. Install registers the app with the update-agent so the next GHCR push can pull immediately:
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
44
|
wget -qO- .../dist/install-api.sh | bash
|
|
@@ -56,3 +56,4 @@ wget -qO- .../dist/install-gateway.sh | bash # bootstraps host + setup-tls.sh
|
|
|
56
56
|
- Gateway container names in `gateway.sites[]` must match running API/UI container names on `network.edge`.
|
|
57
57
|
- UI is a separate profile; gateway is nginx-only and does not start the UI.
|
|
58
58
|
- Prefer Let's Encrypt via `gateway.tlsEmail`; use `TLS_MODE=lab` only for CI/lab.
|
|
59
|
+
- Product GHCR workflow must set `id-token: write` or the VPS will not get an immediate pull.
|
|
@@ -3,44 +3,50 @@ name: infra-update-agent
|
|
|
3
3
|
description: >-
|
|
4
4
|
Machine-level serial Docker update queue for vibed-infra apps. Use when
|
|
5
5
|
multiple products share one VPS and must not pull images in parallel; includes
|
|
6
|
-
GHCR
|
|
6
|
+
immediate GHCR pulls via GitHub Actions OIDC (no extra GitHub secrets).
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Serial update agent
|
|
10
10
|
|
|
11
11
|
## Why
|
|
12
12
|
|
|
13
|
-
Five apps with auto-update cron would each `docker pull` at once and overload the box. The **update-agent** is installed once; apps only **enqueue** jobs.
|
|
13
|
+
Five apps with auto-update cron would each `docker pull` at once and overload the box. The **update-agent** is installed once; apps only **enqueue** jobs. The agent runs `update-*.sh`, which is **digest-gated**: pull if the registry digest changed, restart only if the running container is stale.
|
|
14
14
|
|
|
15
15
|
## Layout
|
|
16
16
|
|
|
17
17
|
```
|
|
18
18
|
~/services/vibed-infra/update-agent/
|
|
19
19
|
queue/ processing/ done/ failed/
|
|
20
|
-
registry/{app}/{role}.json
|
|
21
|
-
tokens/{app}
|
|
20
|
+
registry/{app}/{role}.json # image + installDir (written at wget install)
|
|
21
|
+
tokens/{app} # optional local curl; CI uses OIDC
|
|
22
22
|
agent.sh
|
|
23
23
|
enqueue.sh
|
|
24
|
-
webhook_server.py
|
|
24
|
+
webhook_server.py # 0.0.0.0:19200 — do not open this port publicly
|
|
25
25
|
lib/github_oidc.py
|
|
26
|
-
.env
|
|
26
|
+
.env # WEBHOOK_PORT=19200
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Override the directory with `VIBED_UPDATE_AGENT` (wins over `VIBED_HOME`).
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
2. Cron enqueues; agent cron (`*/5`) processes **serially**.
|
|
33
|
-
3. Image push: reusable GHCR workflow mints a GitHub Actions OIDC JWT (audience = webhook URL) and POSTs `{package,tag}` to `/_vibed/hooks/ghcr`.
|
|
34
|
-
4. Webhook verifies the JWT against GitHub’s JWKS (`iss`, `aud`, `exp`, RS256), then enqueues only registry images whose name contains `repository_owner`.
|
|
35
|
-
5. Agent runs `update-*.sh` immediately.
|
|
31
|
+
## Pull paths
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
```
|
|
34
|
+
git push main
|
|
35
|
+
→ reusable GHCR workflow builds/pushes :main
|
|
36
|
+
→ notify-vps-pull.py mints OIDC JWT (aud = webhook URL)
|
|
37
|
+
→ POST https://{host}/_vibed/hooks/ghcr (fallback http://{publicIp}/…)
|
|
38
|
+
→ nginx → webhook_server (verify GitHub JWKS)
|
|
39
|
+
→ enqueue matching registry entries (owner must appear in image name)
|
|
40
|
+
→ agent.sh → update-{role}.sh → docker pull + restart if digest changed
|
|
41
|
+
```
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
- **Immediate:** default-branch image push. Caller job needs `id-token: write`. No GitHub Packages webhook and no `VIBED_WEBHOOK_SECRET`.
|
|
44
|
+
- **Cron backup:** `*_AUTO_UPDATE=1` enqueues on an interval; agent cron `*/5` drains the queue if notify was missed.
|
|
45
|
+
- **Manual:** `bash ~/services/vibed-infra/update-agent/enqueue.sh /path/to/api-install`
|
|
40
46
|
|
|
41
|
-
|
|
47
|
+
Gateway HTTP (`00-default.conf`) and each HTTPS site proxy `/_vibed/hooks/` to `host.docker.internal:19200`. Keep **19200** off the public firewall; only 80/443.
|
|
42
48
|
|
|
43
|
-
Product
|
|
49
|
+
## Product CI
|
|
44
50
|
|
|
45
51
|
```yaml
|
|
46
52
|
jobs:
|
|
@@ -55,10 +61,11 @@ jobs:
|
|
|
55
61
|
image: ghcr.io/${{ github.repository_owner }}/my-api
|
|
56
62
|
```
|
|
57
63
|
|
|
58
|
-
|
|
64
|
+
URLs come from `gateway.publicIp` + `gateway.sites[].host` already in config (`dist/packageconfig.yaml` `webhook.url` / `fallbackUrl`). Disable notify with `notify: false`.
|
|
59
65
|
|
|
60
|
-
|
|
66
|
+
Local curl can send `X-Vibed-Secret` from `tokens/{app}` (compiled hash). Optional `WEBHOOK_SECRET` still works as an override.
|
|
61
67
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
## Tests
|
|
69
|
+
|
|
70
|
+
- `npm test` — openssl-signed JWT (same verifier) + enqueue under `VIBED_UPDATE_AGENT`
|
|
71
|
+
- `npm run test:oidc` / CI job `oidc-webhook-e2e` — real GitHub OIDC mint against a local webhook
|
|
@@ -55,12 +55,12 @@ cd ~/services/gateway
|
|
|
55
55
|
./setup-tls.sh --force # re-issue after host or IP change
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
- **Production:** certbot (
|
|
58
|
+
- **Production:** `setup-tls.sh` issues Let’s Encrypt via docker `certbot/certbot` by default (config under `$GATEWAY_HOME/letsencrypt`, PEMs copied to `$GATEWAY_HOME/certs/`). No sudo required when Docker is available. Host certbot + sudo still preferred when present (same `--config-dir` under gateway home). Webroot if gateway is up, else standalone. On failure, fix DNS then re-run `setup-tls.sh`.
|
|
59
59
|
- **Lab/CI:** `TLS_MODE=lab` → multi-SAN self-signed under `./certs/`.
|
|
60
60
|
|
|
61
|
-
## Webhook path
|
|
61
|
+
## Webhook path (immediate docker pull)
|
|
62
62
|
|
|
63
|
-
Host `00-default.conf` (HTTP) and each app `sites.conf` (HTTPS) proxy `/_vibed/hooks/` to the update-agent on `host.docker.internal:19200`.
|
|
63
|
+
Host `00-default.conf` (HTTP) and each app `sites.conf` (HTTPS) proxy `/_vibed/hooks/` to the update-agent on `host.docker.internal:19200`. After a GHCR `:main` push, CI POSTs a GitHub Actions OIDC JWT to `/_vibed/hooks/ghcr` — see infra-update-agent. Do not expose port 19200 on the public firewall.
|
|
64
64
|
|
|
65
65
|
## Pitfalls
|
|
66
66
|
|
|
@@ -9,6 +9,9 @@ TLS_MODE=
|
|
|
9
9
|
TLS_EMAIL=
|
|
10
10
|
GATEWAY_PUBLIC_IP=
|
|
11
11
|
CERTBOT_WWW=./certbot-www
|
|
12
|
+
# Optional Let's Encrypt (docker certbot when host certbot/sudo unavailable)
|
|
13
|
+
# CERTBOT_IMAGE=certbot/certbot
|
|
14
|
+
# LETSENCRYPT_HOME=./letsencrypt
|
|
12
15
|
HTTP_PORT=80
|
|
13
16
|
HTTPS_PORT=443
|
|
14
17
|
GATEWAY_MEMORY_LIMIT=64m
|
|
@@ -12,6 +12,8 @@ for arg in "$@"; do
|
|
|
12
12
|
-h|--help)
|
|
13
13
|
echo "usage: $0 [--force]"
|
|
14
14
|
echo " Issues Let's Encrypt (TLS_EMAIL / TLS_MODE=letsencrypt) or lab self-signed certs."
|
|
15
|
+
echo " LE prefers host certbot when available (root or passwordless sudo);"
|
|
16
|
+
echo " otherwise uses docker run certbot/certbot (no sudo). PEMs land in ./certs/."
|
|
15
17
|
echo " Re-runs when certs missing, domains/IP changed vs .vibed-tls-state, or --force."
|
|
16
18
|
exit 0
|
|
17
19
|
;;
|
|
@@ -137,27 +139,104 @@ issue_letsencrypt() {
|
|
|
137
139
|
echo "TLS_EMAIL (or gateway.tlsEmail) required for Let's Encrypt" >&2
|
|
138
140
|
return 1
|
|
139
141
|
fi
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
|
|
143
|
+
local LE_HOME="${LETSENCRYPT_HOME:-${SCRIPT_DIR}/letsencrypt}"
|
|
144
|
+
local CERTBOT_IMAGE="${CERTBOT_IMAGE:-certbot/certbot}"
|
|
145
|
+
mkdir -p "$LE_HOME" "$LE_HOME/work" "$LE_HOME/logs" "$CERT_DIR" "$CERTBOT_WWW"
|
|
146
|
+
|
|
147
|
+
# Absolute webroot for docker bind mounts
|
|
148
|
+
local CERTBOT_WWW_ABS="$CERTBOT_WWW"
|
|
149
|
+
[[ "$CERTBOT_WWW_ABS" != /* ]] && CERTBOT_WWW_ABS="${SCRIPT_DIR}/${CERTBOT_WWW_ABS#./}"
|
|
150
|
+
mkdir -p "$CERTBOT_WWW_ABS"
|
|
151
|
+
|
|
152
|
+
local use_webroot=0
|
|
153
|
+
if gateway_running; then
|
|
154
|
+
use_webroot=1
|
|
143
155
|
fi
|
|
144
|
-
|
|
156
|
+
|
|
157
|
+
local domain_args=()
|
|
145
158
|
local d
|
|
146
159
|
for d in "${all[@]}"; do
|
|
147
|
-
|
|
160
|
+
domain_args+=(-d "$d")
|
|
148
161
|
done
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
162
|
+
|
|
163
|
+
local base_args=(
|
|
164
|
+
certonly
|
|
165
|
+
--agree-tos
|
|
166
|
+
--non-interactive
|
|
167
|
+
--email "$email"
|
|
168
|
+
--cert-name "$primary"
|
|
169
|
+
"${domain_args[@]}"
|
|
170
|
+
)
|
|
156
171
|
if [[ "${CERTBOT_DRY_RUN:-0}" == "1" ]]; then
|
|
157
|
-
|
|
172
|
+
base_args+=(--dry-run)
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
local ran=0
|
|
176
|
+
local host_can_run=0
|
|
177
|
+
if command -v certbot >/dev/null 2>&1; then
|
|
178
|
+
if [[ "$(id -u)" -eq 0 ]]; then
|
|
179
|
+
host_can_run=1
|
|
180
|
+
elif sudo -n true >/dev/null 2>&1; then
|
|
181
|
+
host_can_run=1
|
|
182
|
+
fi
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
if [[ "$host_can_run" == "1" ]]; then
|
|
186
|
+
local host_args=(
|
|
187
|
+
"${base_args[@]}"
|
|
188
|
+
--config-dir "$LE_HOME"
|
|
189
|
+
--work-dir "$LE_HOME/work"
|
|
190
|
+
--logs-dir "$LE_HOME/logs"
|
|
191
|
+
)
|
|
192
|
+
if [[ "$use_webroot" == "1" ]]; then
|
|
193
|
+
echo "Gateway running — host certbot webroot ($CERTBOT_WWW_ABS)"
|
|
194
|
+
host_args+=(--webroot -w "$CERTBOT_WWW_ABS")
|
|
195
|
+
else
|
|
196
|
+
echo "Gateway not running — host certbot standalone (port 80 must be free)"
|
|
197
|
+
host_args+=(--standalone)
|
|
198
|
+
fi
|
|
199
|
+
local run_certbot=(certbot)
|
|
200
|
+
if [[ "$(id -u)" -ne 0 ]]; then
|
|
201
|
+
run_certbot=(sudo certbot)
|
|
202
|
+
fi
|
|
203
|
+
if "${run_certbot[@]}" "${host_args[@]}"; then
|
|
204
|
+
ran=1
|
|
205
|
+
# Best-effort: reclaim root-owned LE tree after sudo certbot
|
|
206
|
+
if [[ "$(id -u)" -ne 0 ]]; then
|
|
207
|
+
sudo -n chown -R "$(id -u):$(id -g)" "$LE_HOME" 2>/dev/null || true
|
|
208
|
+
fi
|
|
209
|
+
fi
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
if [[ "$ran" != "1" && "$host_can_run" != "1" ]] && command -v docker >/dev/null 2>&1; then
|
|
213
|
+
local docker_args=(
|
|
214
|
+
run --rm
|
|
215
|
+
-v "${LE_HOME}:/etc/letsencrypt"
|
|
216
|
+
)
|
|
217
|
+
local container_args=("${base_args[@]}")
|
|
218
|
+
if [[ "$use_webroot" == "1" ]]; then
|
|
219
|
+
echo "Gateway running — docker certbot webroot ($CERTBOT_WWW_ABS)"
|
|
220
|
+
docker_args+=(-v "${CERTBOT_WWW_ABS}:/var/www/certbot")
|
|
221
|
+
container_args+=(--webroot -w /var/www/certbot)
|
|
222
|
+
else
|
|
223
|
+
echo "Gateway not running — docker certbot standalone (-p 80:80)"
|
|
224
|
+
docker_args+=(-p 80:80)
|
|
225
|
+
container_args+=(--standalone)
|
|
226
|
+
fi
|
|
227
|
+
if docker "${docker_args[@]}" "$CERTBOT_IMAGE" "${container_args[@]}"; then
|
|
228
|
+
ran=1
|
|
229
|
+
fi
|
|
158
230
|
fi
|
|
159
|
-
|
|
160
|
-
|
|
231
|
+
|
|
232
|
+
if [[ "$ran" != "1" ]]; then
|
|
233
|
+
if [[ "$host_can_run" != "1" ]] && ! command -v docker >/dev/null 2>&1; then
|
|
234
|
+
cat <<EOF >&2
|
|
235
|
+
Neither usable host certbot nor docker found.
|
|
236
|
+
Install Docker (preferred for non-root), or certbot with sudo, or set TLS_MODE=lab.
|
|
237
|
+
EOF
|
|
238
|
+
else
|
|
239
|
+
cat <<EOF >&2
|
|
161
240
|
|
|
162
241
|
Let's Encrypt failed (often DNS not pointing here yet).
|
|
163
242
|
1. Paste dist/DNS-SKILL.md into your AU DNS agent (or create the A records yourself).
|
|
@@ -165,15 +244,27 @@ Let's Encrypt failed (often DNS not pointing here yet).
|
|
|
165
244
|
3. Re-run: cd ${SCRIPT_DIR} && ./setup-tls.sh --force
|
|
166
245
|
|
|
167
246
|
EOF
|
|
247
|
+
fi
|
|
168
248
|
return 1
|
|
169
249
|
fi
|
|
170
|
-
|
|
250
|
+
|
|
251
|
+
# Dry-run does not write live PEMs
|
|
252
|
+
if [[ "${CERTBOT_DRY_RUN:-0}" == "1" ]]; then
|
|
253
|
+
echo "certbot dry-run succeeded (no PEMs written)"
|
|
254
|
+
return 0
|
|
255
|
+
fi
|
|
256
|
+
|
|
257
|
+
local live="${LE_HOME}/live/${primary}"
|
|
171
258
|
if [[ ! -f "${live}/fullchain.pem" || ! -f "${live}/privkey.pem" ]]; then
|
|
172
259
|
echo "certbot succeeded but ${live} PEMs missing" >&2
|
|
173
260
|
return 1
|
|
174
261
|
fi
|
|
175
|
-
|
|
176
|
-
|
|
262
|
+
cp -L "${live}/fullchain.pem" "${CERT_DIR}/fullchain.pem"
|
|
263
|
+
cp -L "${live}/privkey.pem" "${CERT_DIR}/privkey.pem"
|
|
264
|
+
chmod 644 "${CERT_DIR}/fullchain.pem"
|
|
265
|
+
chmod 600 "${CERT_DIR}/privkey.pem"
|
|
266
|
+
patch_env_key TLS_FULLCHAIN "${CERT_DIR}/fullchain.pem"
|
|
267
|
+
patch_env_key TLS_PRIVKEY "${CERT_DIR}/privkey.pem"
|
|
177
268
|
patch_env_key TLS_MODE letsencrypt
|
|
178
269
|
patch_env_key TLS_EMAIL "$email"
|
|
179
270
|
}
|
|
@@ -33,7 +33,7 @@ if [[ ! -f "$TLS_FULLCHAIN" || ! -f "$TLS_PRIVKEY" ]]; then
|
|
|
33
33
|
echo "TLS certs not found:" >&2
|
|
34
34
|
echo " $TLS_FULLCHAIN" >&2
|
|
35
35
|
echo " $TLS_PRIVKEY" >&2
|
|
36
|
-
echo "Run
|
|
36
|
+
echo "Run ./setup-tls.sh (lab or letsencrypt) to create ./certs PEMs." >&2
|
|
37
37
|
exit 1
|
|
38
38
|
fi
|
|
39
39
|
|
|
@@ -44,6 +44,10 @@ fi
|
|
|
44
44
|
|
|
45
45
|
docker network create "$NETWORK" >/dev/null 2>&1 || true
|
|
46
46
|
mkdir -p "$CERTBOT_WWW" "$APPS_DIR"
|
|
47
|
+
# Absolute path for live ACME webroot bind mount
|
|
48
|
+
CERTBOT_ABS="$CERTBOT_WWW"
|
|
49
|
+
[[ "$CERTBOT_ABS" != /* ]] && CERTBOT_ABS="${SCRIPT_DIR}/${CERTBOT_ABS#./}"
|
|
50
|
+
mkdir -p "$CERTBOT_ABS"
|
|
47
51
|
|
|
48
52
|
if docker inspect "$GATEWAY_NAME" >/dev/null 2>&1; then
|
|
49
53
|
echo "Removing existing container $GATEWAY_NAME ..."
|
|
@@ -58,7 +62,7 @@ STAGE="$(mktemp -d)"
|
|
|
58
62
|
cleanup_stage() { rm -rf "$STAGE"; }
|
|
59
63
|
trap cleanup_stage EXIT
|
|
60
64
|
|
|
61
|
-
mkdir -p "$STAGE/conf.d" "$STAGE/certs" "$STAGE/
|
|
65
|
+
mkdir -p "$STAGE/conf.d" "$STAGE/certs" "$STAGE/apps"
|
|
62
66
|
cp "$NGINX_CONF" "$STAGE/nginx.conf"
|
|
63
67
|
cp -r "$CONF_D/." "$STAGE/conf.d/"
|
|
64
68
|
cp "$TLS_FULLCHAIN" "$STAGE/certs/fullchain.pem"
|
|
@@ -75,10 +79,6 @@ if [[ -d "$APPS_DIR" ]]; then
|
|
|
75
79
|
fi
|
|
76
80
|
done
|
|
77
81
|
fi
|
|
78
|
-
if [[ -d "$CERTBOT_WWW" ]]; then
|
|
79
|
-
cp -r "$CERTBOT_WWW/." "$STAGE/certbot/" 2>/dev/null || true
|
|
80
|
-
fi
|
|
81
|
-
|
|
82
82
|
tar -C "$STAGE" -cf - . | docker run --rm -i -v "${CONF_VOL}:/cfg" alpine tar -xf - -C /cfg
|
|
83
83
|
|
|
84
84
|
echo "Starting $GATEWAY_NAME (HTTP ${HTTP_PORT}, HTTPS ${HTTPS_PORT}) ..."
|
|
@@ -92,6 +92,7 @@ docker run -d \
|
|
|
92
92
|
-p "${HTTP_PORT}:80" \
|
|
93
93
|
-p "${HTTPS_PORT}:443" \
|
|
94
94
|
-v "${CONF_VOL}:/gateway-cfg:ro" \
|
|
95
|
+
-v "${CERTBOT_ABS}:/var/www/certbot" \
|
|
95
96
|
"$NGINX_IMAGE" \
|
|
96
97
|
sh -c 'mkdir -p /etc/nginx/certs /etc/nginx/conf.d /etc/nginx/apps /var/www/certbot && \
|
|
97
98
|
cp /gateway-cfg/nginx.conf /etc/nginx/nginx.conf && \
|
|
@@ -99,7 +100,6 @@ docker run -d \
|
|
|
99
100
|
if [ -d /gateway-cfg/apps ]; then cp -r /gateway-cfg/apps/. /etc/nginx/apps/; fi && \
|
|
100
101
|
cp /gateway-cfg/certs/fullchain.pem /etc/nginx/certs/fullchain.pem && \
|
|
101
102
|
cp /gateway-cfg/certs/privkey.pem /etc/nginx/certs/privkey.pem && \
|
|
102
|
-
cp -r /gateway-cfg/certbot/. /var/www/certbot/ 2>/dev/null || true && \
|
|
103
103
|
exec nginx -g "daemon off;"' >/dev/null
|
|
104
104
|
|
|
105
105
|
trap - EXIT
|
|
Binary file
|