vibed-infra 0.6.0 → 0.7.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 +12 -6
- package/github/__pycache__/notify-vps-pull.cpython-312.pyc +0 -0
- package/github/notify-vps-pull.py +194 -0
- package/github/workflows/docker-build-reusable.yml +32 -1
- package/install.sh +139 -41
- 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/generate.py +8 -0
- package/lib/github_oidc.py +224 -0
- package/lib/host_gateway.sh +14 -12
- package/lib/package.py +104 -16
- package/lib/persistlog/__pycache__/__init__.cpython-312.pyc +0 -0
- package/lib/product_config.py +29 -14
- package/lib/update_queue.sh +4 -0
- package/lib/webhook.py +90 -0
- package/package.json +4 -2
- package/schema/packageconfig.md +9 -1
- package/skills/dns-configure/SKILL.md +8 -6
- package/skills/infra-cicd/SKILL.md +14 -7
- package/skills/infra-packager/SKILL.md +9 -7
- package/skills/infra-update-agent/SKILL.md +33 -9
- package/skills/system-gateway/SKILL.md +23 -6
- package/templates/generic/DNS-SKILL.md.tmpl +7 -12
- package/templates/host-gateway/.env.example +4 -0
- package/templates/host-gateway/setup-tls.sh +249 -0
- package/templates/update-agent/__pycache__/webhook_server.cpython-312.pyc +0 -0
- package/templates/update-agent/install-agent.sh +16 -6
- package/templates/update-agent/webhook_server.py +114 -38
package/README.md
CHANGED
|
@@ -8,30 +8,33 @@ Published on npm as **`vibed-infra`**.
|
|
|
8
8
|
|
|
9
9
|
| File | Purpose |
|
|
10
10
|
|------|---------|
|
|
11
|
-
| `vibed-infra-config.yml` | Name, templates, `network.edge`
|
|
11
|
+
| `vibed-infra-config.yml` | Name, templates, `network.edge`, `gateway.publicIp` / `tlsEmail` / `sites[]`, auto-update |
|
|
12
12
|
| `api-config.yaml` / `ui-config.yaml` / `nodes-config.yaml` | Images + opaque config |
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
15
|
./package.sh
|
|
16
16
|
git add dist && git commit && git push
|
|
17
|
-
# Copy dist/DNS-SKILL.md into AU agent
|
|
17
|
+
# Copy dist/DNS-SKILL.md into AU agent (domains + publicIp already filled when configured)
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
## Operator flow (VPS)
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
+
# 1) DNS — paste dist/DNS-SKILL.md into AU browser agent
|
|
24
|
+
# 2) App roles
|
|
23
25
|
wget -qO- .../dist/install-api.sh | bash
|
|
24
26
|
wget -qO- .../dist/install-ui.sh | bash
|
|
25
27
|
wget -qO- .../dist/install-nodes.sh | bash
|
|
26
|
-
|
|
28
|
+
# 3) Host gateway + TLS (setup-tls.sh; re-run with --force if host/IP changes)
|
|
29
|
+
wget -qO- .../dist/install-gateway.sh | bash
|
|
27
30
|
```
|
|
28
31
|
|
|
29
32
|
| Profile | Role |
|
|
30
33
|
|---------|------|
|
|
31
34
|
| `api` / `ui` / `nodes` | Join shared `vps-edge` |
|
|
32
|
-
| `gateway` | Host nginx +
|
|
35
|
+
| `gateway` | Host nginx + `apps/{name}/sites.conf` + HTTPS via `~/services/gateway/setup-tls.sh` |
|
|
33
36
|
|
|
34
|
-
Multi-app: further products’ `install-gateway.sh` only add `apps/{other}/sites.conf
|
|
37
|
+
Multi-app: further products’ `install-gateway.sh` only add `apps/{other}/sites.conf`, refresh TLS SANs, and reload — no second 80/443 bind.
|
|
35
38
|
|
|
36
39
|
## Machine services (installed once)
|
|
37
40
|
|
|
@@ -41,7 +44,7 @@ Multi-app: further products’ `install-gateway.sh` only add `apps/{other}/sites
|
|
|
41
44
|
| `~/services/vibed-infra/update-agent` | Serial pull queue + optional GHCR webhook |
|
|
42
45
|
| `~/services/vibed-infra/persist-logs` | Per-app WALs + optional R2/S3 ship |
|
|
43
46
|
|
|
44
|
-
Auto-update cron **enqueues** work; the agent processes one job at a time. Updates prune dangling images (`DOCKER_AUTO_PRUNE=1`).
|
|
47
|
+
Auto-update cron **enqueues** work; the agent processes one job at a time. After a GHCR push, the reusable image workflow mints a GitHub Actions OIDC JWT and notifies `https://{domain}/_vibed/hooks/ghcr` (fallback `http://{publicIp}/…`) so the pull does not wait for cron. Updates prune dangling images (`DOCKER_AUTO_PRUNE=1`).
|
|
45
48
|
|
|
46
49
|
## Layout
|
|
47
50
|
|
|
@@ -60,6 +63,8 @@ Auto-update cron **enqueues** work; the agent processes one job at a time. Updat
|
|
|
60
63
|
|----------|---------|
|
|
61
64
|
| `GATEWAY_HOME` | Host gateway dir (default `~/services/gateway`) |
|
|
62
65
|
| `VIBED_HOME` | Machine vibed root (default `~/services/vibed-infra`) |
|
|
66
|
+
| `GATEWAY_PUBLIC_IP` | VPS IPv4 (from `gateway.publicIp`) |
|
|
67
|
+
| `TLS_EMAIL` / `TLS_MODE` | Let’s Encrypt email; `lab` or `letsencrypt` |
|
|
63
68
|
| `PERSIST_LOG_DIR` | Per-service event log dir |
|
|
64
69
|
| `DOCKER_AUTO_PRUNE` | Prune dangling images after update (default on) |
|
|
65
70
|
|
|
@@ -68,6 +73,7 @@ Auto-update cron **enqueues** work; the agent processes one job at a time. Updat
|
|
|
68
73
|
```bash
|
|
69
74
|
npm test
|
|
70
75
|
npm run test:dist
|
|
76
|
+
npm run test:e2e-multi # two apps, localhost wget|bash, shared host gateway
|
|
71
77
|
```
|
|
72
78
|
|
|
73
79
|
## Publishing
|
|
Binary file
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Notify a vibed-infra VPS to pull a GHCR image.
|
|
3
|
+
|
|
4
|
+
Prefers GitHub Actions OIDC (no shared secret). Falls back to compiled
|
|
5
|
+
config token / VIBED_WEBHOOK_SECRET for local curl.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import urllib.error
|
|
14
|
+
import urllib.parse
|
|
15
|
+
import urllib.request
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
# Keep in sync with lib/webhook.py
|
|
19
|
+
HOOK_PATH = "/_vibed/hooks/ghcr"
|
|
20
|
+
TOKEN_VERSION = "vibed-webhook|v1|"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _derive_token(name: str, public_ip: str, host: str) -> str:
|
|
24
|
+
import hashlib
|
|
25
|
+
|
|
26
|
+
raw = f"{TOKEN_VERSION}{name}|{public_ip}|{host}"
|
|
27
|
+
return hashlib.sha256(raw.encode()).hexdigest()[:32]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _strip(s: str) -> str:
|
|
31
|
+
s = s.strip()
|
|
32
|
+
if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")):
|
|
33
|
+
return s[1:-1]
|
|
34
|
+
return s
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load_webhook(path: Path) -> dict[str, str]:
|
|
38
|
+
text = path.read_text(encoding="utf-8")
|
|
39
|
+
url = fallback = token = name = public_ip = host = ""
|
|
40
|
+
in_webhook = False
|
|
41
|
+
in_sites = False
|
|
42
|
+
for raw in text.splitlines():
|
|
43
|
+
line = raw.rstrip()
|
|
44
|
+
stripped = line.lstrip()
|
|
45
|
+
if stripped.startswith("webhook:"):
|
|
46
|
+
in_webhook = True
|
|
47
|
+
in_sites = False
|
|
48
|
+
continue
|
|
49
|
+
if in_webhook:
|
|
50
|
+
if stripped and not line.startswith(" ") and not line.startswith("\t"):
|
|
51
|
+
in_webhook = False
|
|
52
|
+
elif stripped.startswith("url:"):
|
|
53
|
+
url = _strip(stripped.split(":", 1)[1])
|
|
54
|
+
elif stripped.startswith("fallbackUrl:"):
|
|
55
|
+
fallback = _strip(stripped.split(":", 1)[1])
|
|
56
|
+
elif stripped.startswith("token:"):
|
|
57
|
+
token = _strip(stripped.split(":", 1)[1])
|
|
58
|
+
if stripped.startswith("name:") and not name:
|
|
59
|
+
name = _strip(stripped.split(":", 1)[1])
|
|
60
|
+
if stripped.startswith("publicIp:"):
|
|
61
|
+
public_ip = _strip(stripped.split(":", 1)[1])
|
|
62
|
+
if stripped.startswith("sites:"):
|
|
63
|
+
in_sites = True
|
|
64
|
+
continue
|
|
65
|
+
if in_sites and stripped.startswith("- host:"):
|
|
66
|
+
host = _strip(stripped.split(":", 1)[1])
|
|
67
|
+
in_sites = False
|
|
68
|
+
elif in_sites and stripped.startswith("host:"):
|
|
69
|
+
host = _strip(stripped.split(":", 1)[1])
|
|
70
|
+
in_sites = False
|
|
71
|
+
if not token and name and (public_ip or host):
|
|
72
|
+
token = _derive_token(name, public_ip, host)
|
|
73
|
+
if not url and host:
|
|
74
|
+
url = f"https://{host}{HOOK_PATH}"
|
|
75
|
+
if not fallback and public_ip:
|
|
76
|
+
fallback = f"http://{public_ip}{HOOK_PATH}"
|
|
77
|
+
if not url:
|
|
78
|
+
url = fallback
|
|
79
|
+
return {"url": url, "fallbackUrl": fallback, "token": token}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _package_from_image(image: str) -> str:
|
|
83
|
+
img = image.split("@", 1)[0]
|
|
84
|
+
if "/" in img:
|
|
85
|
+
return img.rsplit("/", 1)[-1].split(":")[0]
|
|
86
|
+
return img.split(":")[0]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def fetch_oidc_token(audience: str) -> str:
|
|
90
|
+
url = os.environ.get("ACTIONS_ID_TOKEN_REQUEST_URL") or ""
|
|
91
|
+
tok = os.environ.get("ACTIONS_ID_TOKEN_REQUEST_TOKEN") or ""
|
|
92
|
+
if not url or not tok:
|
|
93
|
+
return ""
|
|
94
|
+
sep = "&" if "?" in url else "?"
|
|
95
|
+
req = urllib.request.Request(
|
|
96
|
+
f"{url}{sep}audience={urllib.parse.quote(audience, safe='')}",
|
|
97
|
+
headers={"Authorization": f"Bearer {tok}", "Accept": "application/json"},
|
|
98
|
+
)
|
|
99
|
+
try:
|
|
100
|
+
with urllib.request.urlopen(req, timeout=20) as resp:
|
|
101
|
+
data = json.loads(resp.read().decode())
|
|
102
|
+
return str(data.get("value") or "")
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f"notify-vps: OIDC mint failed: {e}", file=sys.stderr)
|
|
105
|
+
return ""
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _post(url: str, body: dict, *, bearer: str = "", secret: str = "") -> tuple[int, str]:
|
|
109
|
+
headers = {"Content-Type": "application/json"}
|
|
110
|
+
if bearer:
|
|
111
|
+
headers["Authorization"] = f"Bearer {bearer}"
|
|
112
|
+
if secret:
|
|
113
|
+
headers["X-Vibed-Secret"] = secret
|
|
114
|
+
req = urllib.request.Request(
|
|
115
|
+
url,
|
|
116
|
+
data=json.dumps(body).encode(),
|
|
117
|
+
method="POST",
|
|
118
|
+
headers=headers,
|
|
119
|
+
)
|
|
120
|
+
try:
|
|
121
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
122
|
+
return resp.status, resp.read().decode()
|
|
123
|
+
except urllib.error.HTTPError as e:
|
|
124
|
+
return e.code, e.read().decode(errors="replace")
|
|
125
|
+
except Exception as e:
|
|
126
|
+
return 0, str(e)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def main() -> int:
|
|
130
|
+
ap = argparse.ArgumentParser()
|
|
131
|
+
ap.add_argument("--image", required=True)
|
|
132
|
+
ap.add_argument("--tag", default="main")
|
|
133
|
+
ap.add_argument("--config", default="")
|
|
134
|
+
ap.add_argument("--url", action="append", default=[], help="Override webhook URL (repeatable)")
|
|
135
|
+
ap.add_argument("--strict", action="store_true", help="Exit 1 if notify fails")
|
|
136
|
+
args = ap.parse_args()
|
|
137
|
+
|
|
138
|
+
spec = {"url": "", "fallbackUrl": "", "token": ""}
|
|
139
|
+
if not args.url:
|
|
140
|
+
roots = []
|
|
141
|
+
if args.config:
|
|
142
|
+
roots.append(Path(args.config))
|
|
143
|
+
cwd = Path.cwd()
|
|
144
|
+
roots.extend(
|
|
145
|
+
[
|
|
146
|
+
cwd / "dist" / "packageconfig.yaml",
|
|
147
|
+
cwd / "packageconfig.yaml",
|
|
148
|
+
cwd / "templates" / "vibed-infra-config.yml",
|
|
149
|
+
]
|
|
150
|
+
)
|
|
151
|
+
conf_path = next((p for p in roots if p.is_file()), None)
|
|
152
|
+
if conf_path:
|
|
153
|
+
spec = _load_webhook(conf_path)
|
|
154
|
+
elif not args.url:
|
|
155
|
+
print("notify-vps: no packageconfig (set gateway.publicIp + sites[].host)", file=sys.stderr)
|
|
156
|
+
return 1 if args.strict else 0
|
|
157
|
+
|
|
158
|
+
token = os.environ.get("VIBED_WEBHOOK_SECRET") or spec.get("token") or ""
|
|
159
|
+
urls = list(args.url) if args.url else [u for u in (spec.get("url"), spec.get("fallbackUrl")) if u]
|
|
160
|
+
seen: set[str] = set()
|
|
161
|
+
urls = [u for u in urls if not (u in seen or seen.add(u))]
|
|
162
|
+
has_oidc = bool(os.environ.get("ACTIONS_ID_TOKEN_REQUEST_URL"))
|
|
163
|
+
if not urls:
|
|
164
|
+
print("notify-vps: skipped (no webhook url)", file=sys.stderr)
|
|
165
|
+
return 1 if args.strict else 0
|
|
166
|
+
if not has_oidc and not token:
|
|
167
|
+
print("notify-vps: skipped (no OIDC env and no token)", file=sys.stderr)
|
|
168
|
+
return 1 if args.strict else 0
|
|
169
|
+
|
|
170
|
+
pkg = _package_from_image(args.image)
|
|
171
|
+
payload = {"package": pkg, "tag": args.tag, "image": args.image}
|
|
172
|
+
last_err = ""
|
|
173
|
+
for url in urls:
|
|
174
|
+
bearer = fetch_oidc_token(url) if has_oidc else ""
|
|
175
|
+
secret = "" if bearer else token
|
|
176
|
+
if not bearer and not secret:
|
|
177
|
+
last_err = f"{url}: no OIDC token"
|
|
178
|
+
print(f"notify-vps: {last_err}", file=sys.stderr)
|
|
179
|
+
continue
|
|
180
|
+
try:
|
|
181
|
+
code, text = _post(url, payload, bearer=bearer, secret=secret)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
code, text = 0, str(e)
|
|
184
|
+
if code == 200:
|
|
185
|
+
print(f"notify-vps: {url} → {text.strip()}")
|
|
186
|
+
return 0
|
|
187
|
+
last_err = f"{url} HTTP {code}: {text[:200]}"
|
|
188
|
+
print(f"notify-vps: {last_err}", file=sys.stderr)
|
|
189
|
+
print(f"notify-vps: failed ({last_err})", file=sys.stderr)
|
|
190
|
+
return 1 if args.strict else 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
raise SystemExit(main())
|
|
@@ -4,12 +4,19 @@
|
|
|
4
4
|
#
|
|
5
5
|
# jobs:
|
|
6
6
|
# build-api:
|
|
7
|
-
# uses:
|
|
7
|
+
# uses: naiemk/vibed-infra/.github/workflows/docker-build-reusable.yml@main
|
|
8
8
|
# with:
|
|
9
9
|
# dockerfile: deploy/Dockerfile.api
|
|
10
10
|
# image: ghcr.io/${{ github.repository_owner }}/my-api
|
|
11
11
|
# context: .
|
|
12
|
+
# permissions:
|
|
13
|
+
# contents: read
|
|
14
|
+
# packages: write
|
|
15
|
+
# id-token: write
|
|
12
16
|
# secrets: inherit
|
|
17
|
+
#
|
|
18
|
+
# After push to the default branch, notifies the VPS using GitHub Actions OIDC
|
|
19
|
+
# (audience = webhook URL). No GitHub Packages webhook and no extra secrets.
|
|
13
20
|
|
|
14
21
|
name: Reusable docker build (single image)
|
|
15
22
|
|
|
@@ -26,14 +33,26 @@ on:
|
|
|
26
33
|
required: false
|
|
27
34
|
type: string
|
|
28
35
|
default: .
|
|
36
|
+
notify:
|
|
37
|
+
required: false
|
|
38
|
+
type: boolean
|
|
39
|
+
default: true
|
|
40
|
+
secrets:
|
|
41
|
+
VIBED_WEBHOOK_SECRET:
|
|
42
|
+
required: false
|
|
29
43
|
|
|
30
44
|
permissions:
|
|
31
45
|
contents: read
|
|
32
46
|
packages: write
|
|
47
|
+
id-token: write
|
|
33
48
|
|
|
34
49
|
jobs:
|
|
35
50
|
build-push:
|
|
36
51
|
runs-on: ubuntu-latest
|
|
52
|
+
permissions:
|
|
53
|
+
contents: read
|
|
54
|
+
packages: write
|
|
55
|
+
id-token: write
|
|
37
56
|
steps:
|
|
38
57
|
- uses: actions/checkout@v4
|
|
39
58
|
- uses: docker/setup-buildx-action@v3
|
|
@@ -59,3 +78,15 @@ jobs:
|
|
|
59
78
|
labels: |
|
|
60
79
|
org.opencontainers.image.revision=${{ github.sha }}
|
|
61
80
|
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
|
81
|
+
- name: Notify VPS to pull
|
|
82
|
+
if: inputs.notify && github.event.repository.default_branch == github.ref_name
|
|
83
|
+
continue-on-error: true
|
|
84
|
+
env:
|
|
85
|
+
VIBED_WEBHOOK_SECRET: ${{ secrets.VIBED_WEBHOOK_SECRET }}
|
|
86
|
+
run: |
|
|
87
|
+
script=dist/notify-vps-pull.py
|
|
88
|
+
if [[ ! -f "$script" ]]; then
|
|
89
|
+
echo "notify-vps: dist/notify-vps-pull.py missing — re-package and commit dist/"
|
|
90
|
+
exit 0
|
|
91
|
+
fi
|
|
92
|
+
python3 "$script" --image "${{ inputs.image }}" --tag main
|
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 host_gateway.sh; do
|
|
60
|
+
for lib in fetch.sh env.sh prompt.sh tls.sh load_config.py generate.py host_gateway.sh update_queue.sh webhook.py github_oidc.py; do
|
|
61
61
|
if [[ "$LOCAL_PACKAGER" == "1" ]]; then
|
|
62
62
|
cp -f "${PACKAGER_RAW}/lib/${lib}" "${INFRA_LIB}/${lib}"
|
|
63
63
|
else
|
|
@@ -319,8 +319,83 @@ EOF
|
|
|
319
319
|
chmod +x "$DEST/${START_SCRIPT}" 2>/dev/null || true
|
|
320
320
|
|
|
321
321
|
# Ensure update-agent + persist-logs on first install (idempotent)
|
|
322
|
-
|
|
323
|
-
|
|
322
|
+
_install_update_agent() {
|
|
323
|
+
local agent_packager="$PACKAGER_RAW"
|
|
324
|
+
if [[ "$LOCAL_PACKAGER" != "1" ]]; then
|
|
325
|
+
agent_packager="${DEST}/.infra-packager"
|
|
326
|
+
mkdir -p "$agent_packager/lib" "$agent_packager/templates/update-agent"
|
|
327
|
+
cp -f "${INFRA_LIB}/update_queue.sh" "$agent_packager/lib/update_queue.sh" 2>/dev/null || true
|
|
328
|
+
cp -f "${INFRA_LIB}/webhook.py" "$agent_packager/lib/webhook.py" 2>/dev/null || true
|
|
329
|
+
cp -f "${INFRA_LIB}/github_oidc.py" "$agent_packager/lib/github_oidc.py" 2>/dev/null || true
|
|
330
|
+
for f in install-agent.sh agent.sh enqueue.sh webhook_server.py; do
|
|
331
|
+
_fetch "${PACKAGER_RAW}/templates/update-agent/${f}" "$agent_packager/templates/update-agent/${f}" || true
|
|
332
|
+
done
|
|
333
|
+
fi
|
|
334
|
+
if [[ -f "${agent_packager}/templates/update-agent/install-agent.sh" ]]; then
|
|
335
|
+
chmod +x "$agent_packager/templates/update-agent/"*.sh 2>/dev/null || true
|
|
336
|
+
PACKAGER_RAW="$agent_packager" VIBED_HOME="${VIBED_HOME:-}" bash "${agent_packager}/templates/update-agent/install-agent.sh" || true
|
|
337
|
+
fi
|
|
338
|
+
}
|
|
339
|
+
_install_update_agent
|
|
340
|
+
|
|
341
|
+
# Register this install so GHCR notify can enqueue without waiting for cron
|
|
342
|
+
PRODUCT_NAME="${PRODUCT_NAME:-$(
|
|
343
|
+
python3 -c "
|
|
344
|
+
import sys
|
|
345
|
+
sys.path.insert(0, '${INFRA_LIB}')
|
|
346
|
+
from load_config import load_packageconfig
|
|
347
|
+
from pathlib import Path
|
|
348
|
+
print(load_packageconfig(Path('${PC_LOCAL}')).get('name') or 'app')
|
|
349
|
+
"
|
|
350
|
+
)}"
|
|
351
|
+
_REG_ROLE="$ROLE"
|
|
352
|
+
case "$ROLE" in
|
|
353
|
+
backend) _REG_ROLE=api ;;
|
|
354
|
+
workers) _REG_ROLE=nodes ;;
|
|
355
|
+
esac
|
|
356
|
+
# shellcheck source=/dev/null
|
|
357
|
+
if [[ -f "${INFRA_LIB}/update_queue.sh" ]]; then
|
|
358
|
+
source "${INFRA_LIB}/update_queue.sh"
|
|
359
|
+
elif [[ -f "${PACKAGER_RAW}/lib/update_queue.sh" ]]; then
|
|
360
|
+
source "${PACKAGER_RAW}/lib/update_queue.sh"
|
|
361
|
+
fi
|
|
362
|
+
if declare -F vibed_register_app >/dev/null 2>&1; then
|
|
363
|
+
_env_val() {
|
|
364
|
+
local key="$1"
|
|
365
|
+
[[ -f "$DEST/.env" ]] || return 0
|
|
366
|
+
grep -E "^${key}=" "$DEST/.env" 2>/dev/null | head -1 | cut -d= -f2-
|
|
367
|
+
}
|
|
368
|
+
_REG_IMAGE=""
|
|
369
|
+
case "$_REG_ROLE" in
|
|
370
|
+
api) _REG_IMAGE="$(_env_val BACKEND_IMAGE)" ;;
|
|
371
|
+
ui) _REG_IMAGE="$(_env_val UI_IMAGE)" ;;
|
|
372
|
+
nodes) _REG_IMAGE="$(_env_val WORKER_IMAGE)" ;;
|
|
373
|
+
gateway) _REG_IMAGE="$(_env_val NGINX_IMAGE)" ;;
|
|
374
|
+
esac
|
|
375
|
+
vibed_register_app "$PRODUCT_NAME" "$_REG_ROLE" "$DEST" "$_REG_IMAGE" || true
|
|
376
|
+
_HOOK_TOKEN="$(
|
|
377
|
+
python3 -c "
|
|
378
|
+
import sys
|
|
379
|
+
sys.path.insert(0, '${INFRA_LIB}')
|
|
380
|
+
from pathlib import Path
|
|
381
|
+
from load_config import load_packageconfig
|
|
382
|
+
try:
|
|
383
|
+
from webhook import webhook_from_packageconfig
|
|
384
|
+
except ImportError:
|
|
385
|
+
webhook_from_packageconfig = None
|
|
386
|
+
c = load_packageconfig(Path('${PC_LOCAL}'))
|
|
387
|
+
if webhook_from_packageconfig:
|
|
388
|
+
print(webhook_from_packageconfig(c).get('token') or '')
|
|
389
|
+
else:
|
|
390
|
+
print((c.get('webhook') or {}).get('token') or '')
|
|
391
|
+
"
|
|
392
|
+
)"
|
|
393
|
+
if [[ -n "${_HOOK_TOKEN}" ]]; then
|
|
394
|
+
AGENT_HOME="$(vibed_agent_home 2>/dev/null || echo "${VIBED_HOME:-$HOME/services/vibed-infra}/update-agent")"
|
|
395
|
+
mkdir -p "${AGENT_HOME}/tokens"
|
|
396
|
+
printf '%s\n' "$_HOOK_TOKEN" >"${AGENT_HOME}/tokens/${PRODUCT_NAME}"
|
|
397
|
+
echo "webhook token registered for ${PRODUCT_NAME}"
|
|
398
|
+
fi
|
|
324
399
|
fi
|
|
325
400
|
if [[ -f "${PACKAGER_RAW}/templates/persist-logs/install-persist-logs.sh" ]]; then
|
|
326
401
|
PACKAGER_RAW="$PACKAGER_RAW" VIBED_HOME="${VIBED_HOME:-}" bash "${PACKAGER_RAW}/templates/persist-logs/install-persist-logs.sh" || true
|
|
@@ -333,67 +408,90 @@ if [[ "$ROLE" == "gateway" ]]; then
|
|
|
333
408
|
source "$DEST/lib-env.sh"
|
|
334
409
|
load_dotenv "$DEST/.env"
|
|
335
410
|
HG="$(vibed_gateway_home)"
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
411
|
+
mkdir -p "$HG/certs" "$HG/certbot-www"
|
|
412
|
+
|
|
413
|
+
# Propagate public IP / TLS email from packageconfig into host + product .env
|
|
414
|
+
GW_PUBLIC_IP="$(
|
|
415
|
+
python3 -c "
|
|
341
416
|
import sys
|
|
342
417
|
sys.path.insert(0, '${INFRA_LIB}')
|
|
343
418
|
from load_config import load_packageconfig, get_profile
|
|
344
419
|
from pathlib import Path
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
")"
|
|
353
|
-
TLS_FULLCHAIN="${TLS_FULLCHAIN:-${CERT_DIR}/fullchain.pem}"
|
|
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 "
|
|
420
|
+
c = load_packageconfig(Path('${PC_LOCAL}'))
|
|
421
|
+
p = get_profile(c, '${PROFILE}')
|
|
422
|
+
print(p.get('publicIp') or '')
|
|
423
|
+
"
|
|
424
|
+
)"
|
|
425
|
+
GW_TLS_EMAIL="$(
|
|
426
|
+
python3 -c "
|
|
361
427
|
import sys
|
|
362
428
|
sys.path.insert(0, '${INFRA_LIB}')
|
|
363
429
|
from load_config import load_packageconfig, get_profile
|
|
364
430
|
from pathlib import Path
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
print(
|
|
368
|
-
"
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
431
|
+
c = load_packageconfig(Path('${PC_LOCAL}'))
|
|
432
|
+
p = get_profile(c, '${PROFILE}')
|
|
433
|
+
print(p.get('tlsEmail') or '')
|
|
434
|
+
"
|
|
435
|
+
)"
|
|
436
|
+
_patch_env() {
|
|
437
|
+
local file="$1" key="$2" val="$3"
|
|
438
|
+
[[ -n "$val" ]] || return 0
|
|
439
|
+
[[ -f "$file" ]] || return 0
|
|
440
|
+
if grep -q "^${key}=" "$file" 2>/dev/null; then
|
|
441
|
+
sed -i "s|^${key}=.*|${key}=${val}|" "$file"
|
|
442
|
+
else
|
|
443
|
+
echo "${key}=${val}" >>"$file"
|
|
373
444
|
fi
|
|
374
|
-
|
|
445
|
+
}
|
|
375
446
|
if [[ -f "$HG/.env" ]]; then
|
|
376
|
-
|
|
377
|
-
|
|
447
|
+
_patch_env "$HG/.env" GATEWAY_PUBLIC_IP "${GATEWAY_PUBLIC_IP:-$GW_PUBLIC_IP}"
|
|
448
|
+
_patch_env "$HG/.env" TLS_EMAIL "${TLS_EMAIL:-$GW_TLS_EMAIL}"
|
|
449
|
+
# Product/install env can force lab (CI) without wiping host LE later
|
|
450
|
+
if [[ -n "${TLS_MODE:-}" ]]; then
|
|
451
|
+
_patch_env "$HG/.env" TLS_MODE "$TLS_MODE"
|
|
452
|
+
fi
|
|
378
453
|
fi
|
|
379
|
-
|
|
454
|
+
_patch_env "$DEST/.env" GATEWAY_PUBLIC_IP "${GATEWAY_PUBLIC_IP:-$GW_PUBLIC_IP}"
|
|
455
|
+
_patch_env "$DEST/.env" TLS_EMAIL "${TLS_EMAIL:-$GW_TLS_EMAIL}"
|
|
456
|
+
|
|
457
|
+
# Collect domains for TLS_DOMAINS fallback if apps conf not parseable yet
|
|
458
|
+
TLS_DOMAINS="$(
|
|
459
|
+
python3 -c "
|
|
380
460
|
import sys
|
|
381
461
|
sys.path.insert(0, '${INFRA_LIB}')
|
|
382
462
|
from load_config import load_packageconfig, get_profile
|
|
383
463
|
from pathlib import Path
|
|
384
464
|
p = get_profile(load_packageconfig(Path('${PC_LOCAL}')), '${PROFILE}')
|
|
465
|
+
names=[]
|
|
385
466
|
for s in p.get('sites') or []:
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
"
|
|
390
|
-
|
|
467
|
+
names.append(s['host'])
|
|
468
|
+
names.extend(s.get('aliases') or [])
|
|
469
|
+
print(' '.join(names))
|
|
470
|
+
"
|
|
471
|
+
)"
|
|
472
|
+
export TLS_DOMAINS
|
|
473
|
+
export GATEWAY_PUBLIC_IP="${GATEWAY_PUBLIC_IP:-$GW_PUBLIC_IP}"
|
|
474
|
+
export TLS_EMAIL="${TLS_EMAIL:-$GW_TLS_EMAIL}"
|
|
475
|
+
|
|
476
|
+
if [[ -x "$HG/setup-tls.sh" ]]; then
|
|
477
|
+
echo "Running host gateway TLS setup ..."
|
|
478
|
+
(cd "$HG" && GATEWAY_HOME="$HG" TLS_DOMAINS="$TLS_DOMAINS" \
|
|
479
|
+
GATEWAY_PUBLIC_IP="${GATEWAY_PUBLIC_IP:-}" \
|
|
480
|
+
TLS_EMAIL="${TLS_EMAIL:-}" \
|
|
481
|
+
TLS_MODE="${TLS_MODE:-}" \
|
|
482
|
+
./setup-tls.sh) || {
|
|
483
|
+
echo "TLS setup did not complete — fix DNS then: cd $HG && ./setup-tls.sh --force" >&2
|
|
484
|
+
}
|
|
485
|
+
else
|
|
486
|
+
echo "warning: $HG/setup-tls.sh missing — re-run gateway install to bootstrap" >&2
|
|
487
|
+
fi
|
|
391
488
|
fi
|
|
392
489
|
|
|
393
490
|
echo ""
|
|
394
491
|
if [[ "$ROLE" == "gateway" ]]; then
|
|
395
492
|
echo "Install complete. Host gateway: $(vibed_gateway_home)"
|
|
396
493
|
echo "App sites: $(vibed_gateway_home)/apps/${PRODUCT_NAME:-app}/sites.conf"
|
|
494
|
+
echo "TLS: cd $(vibed_gateway_home) && ./setup-tls.sh # re-run if host/IP/domains change (--force)"
|
|
397
495
|
echo "Start: cd $DEST && ./start-gateway.sh"
|
|
398
496
|
else
|
|
399
497
|
echo "Install complete. Start: cd $DEST && ./${START_SCRIPT}"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/generate.py
CHANGED
|
@@ -59,6 +59,14 @@ server {{
|
|
|
59
59
|
set ${be_var}_upstream {backend}:{backend_port};
|
|
60
60
|
set ${ui_var}_upstream {ui}:{ui_port};
|
|
61
61
|
|
|
62
|
+
location /_vibed/hooks/ {{
|
|
63
|
+
proxy_pass http://host.docker.internal:19200;
|
|
64
|
+
proxy_set_header Host $host;
|
|
65
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
66
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
67
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
68
|
+
}}
|
|
69
|
+
|
|
62
70
|
location = {health} {{
|
|
63
71
|
{proxy_headers}
|
|
64
72
|
}}
|