vibed-infra 0.3.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 +99 -0
- package/github/workflows/docker-build-reusable.yml +61 -0
- package/install-auto-update.sh +125 -0
- package/install.sh +259 -0
- package/lib/__pycache__/load_config.cpython-312.pyc +0 -0
- package/lib/env.sh +224 -0
- package/lib/fetch.sh +49 -0
- package/lib/generate.py +119 -0
- package/lib/load_config.py +110 -0
- package/lib/prompt.sh +25 -0
- package/lib/tls.sh +59 -0
- package/package.json +34 -0
- package/schema/packageconfig.md +88 -0
- package/skills/infra-cicd/SKILL.md +47 -0
- package/skills/infra-packager/SKILL.md +54 -0
- package/skills/system-gateway/SKILL.md +52 -0
- package/start.sh +15 -0
- package/templates/docker-compose.backend.yml +32 -0
- package/templates/docker-compose.gateway.yml +25 -0
- package/templates/docker-compose.workers.yml +17 -0
- package/templates/nginx/nginx.conf +21 -0
- package/update.sh +15 -0
package/lib/env.sh
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
2
|
+
# Environment helpers for infra packager.
|
|
3
|
+
|
|
4
|
+
load_dotenv() {
|
|
5
|
+
local env_file="${1:-.env}"
|
|
6
|
+
[[ -f "$env_file" ]] || return 0
|
|
7
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
8
|
+
line="${line%$'\r'}"
|
|
9
|
+
[[ -z "${line//[[:space:]]/}" || "$line" =~ ^[[:space:]]*# ]] && continue
|
|
10
|
+
local key="${line%%=*}"
|
|
11
|
+
local val="${line#*=}"
|
|
12
|
+
key="${key%%[[:space:]]*}"
|
|
13
|
+
key="${key##[[:space:]]*}"
|
|
14
|
+
key="${key%$'\r'}"
|
|
15
|
+
val="${val%$'\r'}"
|
|
16
|
+
if [[ "$val" =~ ^\"(.*)\"$ ]]; then val="${BASH_REMATCH[1]}"; fi
|
|
17
|
+
if [[ "$val" =~ ^\'(.*)\'$ ]]; then val="${BASH_REMATCH[1]}"; fi
|
|
18
|
+
[[ -z "$key" || "$key" == *[!A-Za-z0-9_]* ]] && continue
|
|
19
|
+
if [[ -z "${!key-}" ]]; then
|
|
20
|
+
export "$key=$val"
|
|
21
|
+
fi
|
|
22
|
+
done <"$env_file"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
env_flag_on() {
|
|
26
|
+
local name="$1"
|
|
27
|
+
local val="${!name-}"
|
|
28
|
+
case "$val" in
|
|
29
|
+
1|true|TRUE|yes|YES|on|ON) return 0 ;;
|
|
30
|
+
*) return 1 ;;
|
|
31
|
+
esac
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
role_auto_update_on() {
|
|
35
|
+
local primary="$1"
|
|
36
|
+
if [[ -n "${!primary+x}" ]]; then
|
|
37
|
+
env_flag_on "$primary"
|
|
38
|
+
return
|
|
39
|
+
fi
|
|
40
|
+
env_flag_on AUTO_UPDATE
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
any_auto_update_on() {
|
|
44
|
+
local flag
|
|
45
|
+
for flag in "$@"; do
|
|
46
|
+
if role_auto_update_on "$flag"; then
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
done
|
|
50
|
+
return 1
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
append_missing_env_keys() {
|
|
54
|
+
local example="$1"
|
|
55
|
+
local envfile="$2"
|
|
56
|
+
shift 2
|
|
57
|
+
[[ -f "$example" && -f "$envfile" ]] || return 0
|
|
58
|
+
local key line added=0
|
|
59
|
+
local legacy_off=0
|
|
60
|
+
if grep -qiE "^[[:space:]]*AUTO_UPDATE=(0|false|off)\b" "$envfile"; then
|
|
61
|
+
legacy_off=1
|
|
62
|
+
fi
|
|
63
|
+
for key in "$@"; do
|
|
64
|
+
if grep -qE "^[[:space:]]*${key}=" "$envfile"; then
|
|
65
|
+
continue
|
|
66
|
+
fi
|
|
67
|
+
line="$(grep -E "^[[:space:]]*${key}=" "$example" | head -1 || true)"
|
|
68
|
+
[[ -n "$line" ]] || continue
|
|
69
|
+
if [[ "$legacy_off" -eq 1 && "$key" == *_AUTO_UPDATE ]]; then
|
|
70
|
+
line="${key}=0"
|
|
71
|
+
fi
|
|
72
|
+
if [[ "$added" -eq 0 ]]; then
|
|
73
|
+
{
|
|
74
|
+
echo ""
|
|
75
|
+
echo "# --- Auto-update (added by infra install; see .env.example) ---"
|
|
76
|
+
} >>"$envfile"
|
|
77
|
+
fi
|
|
78
|
+
echo "$line" >>"$envfile"
|
|
79
|
+
echo "appended to .env: $key"
|
|
80
|
+
added=1
|
|
81
|
+
done
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
log_update() {
|
|
85
|
+
local dir="${1:-.}"
|
|
86
|
+
local msg="$2"
|
|
87
|
+
mkdir -p "$dir/logs"
|
|
88
|
+
local line
|
|
89
|
+
line="$(date -u +"%Y-%m-%dT%H:%M:%SZ") $msg"
|
|
90
|
+
echo "$line" | tee -a "$dir/logs/auto-update.log"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
resolve_update_lock_file() {
|
|
94
|
+
if [[ -n "${UPDATE_LOCK_FILE:-}" ]]; then
|
|
95
|
+
echo "$UPDATE_LOCK_FILE"
|
|
96
|
+
return 0
|
|
97
|
+
fi
|
|
98
|
+
if [[ -d /var/lock && -w /var/lock ]]; then
|
|
99
|
+
echo /var/lock/infra-auto-update.lock
|
|
100
|
+
return 0
|
|
101
|
+
fi
|
|
102
|
+
echo /tmp/infra-auto-update.lock
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
with_update_lock() {
|
|
106
|
+
local log_dir="$1"
|
|
107
|
+
local role_label="$2"
|
|
108
|
+
local fn="$3"
|
|
109
|
+
local lock
|
|
110
|
+
lock="$(resolve_update_lock_file)"
|
|
111
|
+
exec 9>"$lock" || {
|
|
112
|
+
log_update "$log_dir" "${role_label}: cannot open lock $lock — proceeding without flock"
|
|
113
|
+
"$fn"
|
|
114
|
+
return $?
|
|
115
|
+
}
|
|
116
|
+
if ! flock -n 9; then
|
|
117
|
+
log_update "$log_dir" "${role_label}: another auto-update holds $lock — skip"
|
|
118
|
+
exec 9>&-
|
|
119
|
+
return 0
|
|
120
|
+
fi
|
|
121
|
+
"$fn"
|
|
122
|
+
local rc=$?
|
|
123
|
+
flock -u 9 2>/dev/null || true
|
|
124
|
+
exec 9>&-
|
|
125
|
+
return "$rc"
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
local_image_id() {
|
|
129
|
+
local image="$1"
|
|
130
|
+
docker image inspect -f '{{.Id}}' "$image" 2>/dev/null || true
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
remote_image_digest() {
|
|
134
|
+
local image="$1"
|
|
135
|
+
local digest="" out
|
|
136
|
+
if out="$(docker buildx imagetools inspect "$image" --format '{{.Manifest.Digest}}' 2>/dev/null)"; then
|
|
137
|
+
digest="$(printf '%s\n' "$out" | tr -d '[:space:]')"
|
|
138
|
+
fi
|
|
139
|
+
if [[ "$digest" != sha256:* ]]; then
|
|
140
|
+
if out="$(docker buildx imagetools inspect "$image" -f '{{.Manifest.Digest}}' 2>/dev/null)"; then
|
|
141
|
+
digest="$(printf '%s\n' "$out" | tr -d '[:space:]')"
|
|
142
|
+
fi
|
|
143
|
+
fi
|
|
144
|
+
if [[ "$digest" != sha256:* ]]; then
|
|
145
|
+
if out="$(docker buildx imagetools inspect "$image" 2>/dev/null)"; then
|
|
146
|
+
digest="$(printf '%s\n' "$out" | awk '/^Digest:/{print $2; exit}')"
|
|
147
|
+
fi
|
|
148
|
+
fi
|
|
149
|
+
if [[ "$digest" == sha256:* ]]; then
|
|
150
|
+
echo "$digest"
|
|
151
|
+
fi
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
local_repo_digest() {
|
|
155
|
+
local image="$1"
|
|
156
|
+
docker image inspect -f '{{range .RepoDigests}}{{println .}}{{end}}' "$image" 2>/dev/null \
|
|
157
|
+
| sed -n 's/.*@//p' \
|
|
158
|
+
| head -1
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
pull_image_if_needed() {
|
|
162
|
+
local image="$1"
|
|
163
|
+
local remote locald
|
|
164
|
+
if ! docker image inspect "$image" >/dev/null 2>&1; then
|
|
165
|
+
docker pull "$image" >/dev/null
|
|
166
|
+
echo "pulled (missing locally)"
|
|
167
|
+
return 0
|
|
168
|
+
fi
|
|
169
|
+
remote="$(remote_image_digest "$image")"
|
|
170
|
+
if [[ -z "$remote" ]]; then
|
|
171
|
+
docker pull "$image" >/dev/null
|
|
172
|
+
echo "pulled (digest probe unavailable)"
|
|
173
|
+
return 0
|
|
174
|
+
fi
|
|
175
|
+
locald="$(local_repo_digest "$image")"
|
|
176
|
+
if [[ -n "$locald" && "$locald" == "$remote" ]]; then
|
|
177
|
+
echo "skipped (already $remote)"
|
|
178
|
+
return 0
|
|
179
|
+
fi
|
|
180
|
+
docker pull "$image" >/dev/null
|
|
181
|
+
echo "pulled ($remote)"
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
container_needs_image() {
|
|
185
|
+
local name="$1"
|
|
186
|
+
local image="$2"
|
|
187
|
+
if ! docker inspect "$name" >/dev/null 2>&1; then
|
|
188
|
+
return 0
|
|
189
|
+
fi
|
|
190
|
+
local running new
|
|
191
|
+
running="$(docker inspect -f '{{.Image}}' "$name" 2>/dev/null || true)"
|
|
192
|
+
new="$(docker image inspect -f '{{.Id}}' "$image" 2>/dev/null || true)"
|
|
193
|
+
[[ -z "$running" || -z "$new" || "$running" != "$new" ]]
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
graceful_stop() {
|
|
197
|
+
local name="$1"
|
|
198
|
+
local timeout="${2:-180}"
|
|
199
|
+
if docker inspect "$name" >/dev/null 2>&1; then
|
|
200
|
+
docker stop -t "$timeout" "$name" >/dev/null 2>&1 || true
|
|
201
|
+
fi
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
memory_args() {
|
|
205
|
+
local limit="${1:-}"
|
|
206
|
+
if [[ -n "$limit" ]]; then
|
|
207
|
+
echo --memory="$limit"
|
|
208
|
+
fi
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
chown_data_dir() {
|
|
212
|
+
local dir="$1"
|
|
213
|
+
local uid="${2:-1000}"
|
|
214
|
+
mkdir -p "$dir"
|
|
215
|
+
if [[ "$(id -u)" -eq 0 ]]; then
|
|
216
|
+
chown -R "${uid}:${uid}" "$dir" || true
|
|
217
|
+
elif command -v sudo >/dev/null 2>&1; then
|
|
218
|
+
sudo chown -R "${uid}:${uid}" "$dir" 2>/dev/null || \
|
|
219
|
+
echo "warning: could not chown $dir to ${uid} — fix if container cannot write" >&2
|
|
220
|
+
else
|
|
221
|
+
echo "warning: ensure $dir is writable by uid ${uid}" >&2
|
|
222
|
+
fi
|
|
223
|
+
chmod 755 "$dir" 2>/dev/null || true
|
|
224
|
+
}
|
package/lib/fetch.sh
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
2
|
+
# Fetch helpers for infra packager.
|
|
3
|
+
|
|
4
|
+
infra_fetch() {
|
|
5
|
+
local url="$1"
|
|
6
|
+
local out="$2"
|
|
7
|
+
mkdir -p "$(dirname "$out")"
|
|
8
|
+
if [[ "$url" =~ ^/ ]]; then
|
|
9
|
+
cp -f "$url" "$out"
|
|
10
|
+
return 0
|
|
11
|
+
fi
|
|
12
|
+
if [[ "$url" =~ ^file:// ]]; then
|
|
13
|
+
cp -f "${url#file://}" "$out"
|
|
14
|
+
return 0
|
|
15
|
+
fi
|
|
16
|
+
if command -v curl >/dev/null 2>&1; then
|
|
17
|
+
curl -fsSL "$url" -o "$out"
|
|
18
|
+
else
|
|
19
|
+
wget -qO "$out" "$url"
|
|
20
|
+
fi
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
infra_write_if_missing() {
|
|
24
|
+
local url="$1"
|
|
25
|
+
local path="$2"
|
|
26
|
+
local executable="${3:-0}"
|
|
27
|
+
if [[ -f "$path" ]]; then
|
|
28
|
+
echo "exists (unchanged): $path"
|
|
29
|
+
return 0
|
|
30
|
+
fi
|
|
31
|
+
echo "downloading $(basename "$path") ..."
|
|
32
|
+
infra_fetch "$url" "$path"
|
|
33
|
+
if [[ "$executable" == "1" ]]; then
|
|
34
|
+
chmod +x "$path"
|
|
35
|
+
fi
|
|
36
|
+
echo "created: $path"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
infra_write_template() {
|
|
40
|
+
local url="$1"
|
|
41
|
+
local path="$2"
|
|
42
|
+
local executable="${3:-0}"
|
|
43
|
+
echo "refreshing $(basename "$path") ..."
|
|
44
|
+
infra_fetch "$url" "$path"
|
|
45
|
+
if [[ "$executable" == "1" ]]; then
|
|
46
|
+
chmod +x "$path"
|
|
47
|
+
fi
|
|
48
|
+
echo "updated: $path"
|
|
49
|
+
}
|
package/lib/generate.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate nginx site config from packageconfig gateway profile."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
10
|
+
from load_config import get_profile, load_packageconfig # noqa: E402
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def render_site(site: dict) -> str:
|
|
14
|
+
host = site["host"]
|
|
15
|
+
aliases = site.get("aliases") or []
|
|
16
|
+
server_names = " ".join([host, *aliases])
|
|
17
|
+
backend = site["backend"]
|
|
18
|
+
backend_port = site.get("backendPort", 8080)
|
|
19
|
+
ui = site["ui"]
|
|
20
|
+
ui_port = site.get("uiPort", 80)
|
|
21
|
+
health = site.get("healthPath", "/api/health")
|
|
22
|
+
create = site.get("createPath") or ""
|
|
23
|
+
be_var = backend.replace("-", "_")
|
|
24
|
+
ui_var = ui.replace("-", "_")
|
|
25
|
+
proxy_headers = f""" proxy_pass http://${be_var}_upstream;
|
|
26
|
+
proxy_set_header Host $host;
|
|
27
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
28
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
29
|
+
proxy_set_header X-Forwarded-Proto $scheme;"""
|
|
30
|
+
create_block = ""
|
|
31
|
+
if create:
|
|
32
|
+
create_block = f"""
|
|
33
|
+
location = {create} {{
|
|
34
|
+
limit_req zone=api_create burst=2 nodelay;
|
|
35
|
+
{proxy_headers}
|
|
36
|
+
}}
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
return f"""# --- {host} ---
|
|
40
|
+
server {{
|
|
41
|
+
listen 443 ssl;
|
|
42
|
+
http2 on;
|
|
43
|
+
server_name {server_names};
|
|
44
|
+
|
|
45
|
+
ssl_certificate /etc/nginx/certs/fullchain.pem;
|
|
46
|
+
ssl_certificate_key /etc/nginx/certs/privkey.pem;
|
|
47
|
+
ssl_protocols TLSv1.2 TLSv1.3;
|
|
48
|
+
ssl_prefer_server_ciphers on;
|
|
49
|
+
|
|
50
|
+
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
51
|
+
add_header X-Content-Type-Options nosniff always;
|
|
52
|
+
add_header X-Frame-Options DENY always;
|
|
53
|
+
add_header Referrer-Policy strict-origin-when-cross-origin always;
|
|
54
|
+
|
|
55
|
+
limit_conn addr 40;
|
|
56
|
+
|
|
57
|
+
set ${be_var}_upstream {backend}:{backend_port};
|
|
58
|
+
set ${ui_var}_upstream {ui}:{ui_port};
|
|
59
|
+
|
|
60
|
+
location = {health} {{
|
|
61
|
+
{proxy_headers}
|
|
62
|
+
}}
|
|
63
|
+
{create_block}
|
|
64
|
+
location /api/ {{
|
|
65
|
+
limit_req zone=api_public burst=40 nodelay;
|
|
66
|
+
{proxy_headers}
|
|
67
|
+
}}
|
|
68
|
+
|
|
69
|
+
location / {{
|
|
70
|
+
proxy_pass http://${ui_var}_upstream;
|
|
71
|
+
proxy_http_version 1.1;
|
|
72
|
+
proxy_set_header Host $host;
|
|
73
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
74
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
75
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
76
|
+
}}
|
|
77
|
+
}}
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def render_gateway(conf: dict, profile: str, prof: dict, output_name: str = "domains.conf") -> str:
|
|
82
|
+
sites = prof.get("sites") or []
|
|
83
|
+
parts = [
|
|
84
|
+
"# Generated by lib/generate.py — edit packageconfig sites[] and re-install",
|
|
85
|
+
"resolver 127.0.0.11 valid=10s ipv6=off;",
|
|
86
|
+
"",
|
|
87
|
+
"server {",
|
|
88
|
+
" listen 80 default_server;",
|
|
89
|
+
" server_name _;",
|
|
90
|
+
" location /.well-known/acme-challenge/ { root /var/www/certbot; }",
|
|
91
|
+
" location / { return 301 https://$host$request_uri; }",
|
|
92
|
+
"}",
|
|
93
|
+
"",
|
|
94
|
+
]
|
|
95
|
+
for site in sites:
|
|
96
|
+
parts.append(render_site(site))
|
|
97
|
+
parts.append("")
|
|
98
|
+
include = prof.get("templates", {}).get("nginxInclude")
|
|
99
|
+
if include and Path(include).name != output_name:
|
|
100
|
+
parts.append(f"include /etc/nginx/conf.d/{Path(include).name};")
|
|
101
|
+
return "\n".join(parts)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main() -> int:
|
|
105
|
+
ap = argparse.ArgumentParser()
|
|
106
|
+
ap.add_argument("packageconfig")
|
|
107
|
+
ap.add_argument("--profile", default="gateway")
|
|
108
|
+
ap.add_argument("-o", "--output", required=True)
|
|
109
|
+
args = ap.parse_args()
|
|
110
|
+
conf = load_packageconfig(Path(args.packageconfig))
|
|
111
|
+
prof = get_profile(conf, args.profile)
|
|
112
|
+
out = render_gateway(conf, args.profile, prof, Path(args.output).name)
|
|
113
|
+
Path(args.output).write_text(out, encoding="utf-8")
|
|
114
|
+
print(f"wrote {args.output}")
|
|
115
|
+
return 0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
sys.exit(main())
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Load infra packageconfig.yaml without external deps (subset YAML)."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _strip_quotes(s: str) -> str:
|
|
12
|
+
s = s.strip()
|
|
13
|
+
if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")):
|
|
14
|
+
return s[1:-1]
|
|
15
|
+
return s
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_packageconfig(path: Path) -> dict[str, Any]:
|
|
19
|
+
try:
|
|
20
|
+
import yaml # type: ignore
|
|
21
|
+
|
|
22
|
+
with path.open(encoding="utf-8") as f:
|
|
23
|
+
return yaml.safe_load(f) or {}
|
|
24
|
+
except ImportError:
|
|
25
|
+
return _parse_minimal(path.read_text(encoding="utf-8"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse_minimal(text: str) -> dict[str, Any]:
|
|
29
|
+
"""Parse flat/nested keys for our packageconfig shape only."""
|
|
30
|
+
root: dict[str, Any] = {}
|
|
31
|
+
stack: list[tuple[int, dict[str, Any]]] = [(-1, root)]
|
|
32
|
+
list_key: str | None = None
|
|
33
|
+
list_indent = -1
|
|
34
|
+
|
|
35
|
+
for raw in text.splitlines():
|
|
36
|
+
if not raw.strip() or raw.lstrip().startswith("#"):
|
|
37
|
+
continue
|
|
38
|
+
indent = len(raw) - len(raw.lstrip(" "))
|
|
39
|
+
line = raw.lstrip()
|
|
40
|
+
while stack and indent <= stack[-1][0]:
|
|
41
|
+
stack.pop()
|
|
42
|
+
if list_indent >= 0 and indent <= list_indent:
|
|
43
|
+
list_key = None
|
|
44
|
+
list_indent = -1
|
|
45
|
+
parent = stack[-1][1]
|
|
46
|
+
|
|
47
|
+
if line.startswith("- ") and list_key and isinstance(parent.get(list_key), list):
|
|
48
|
+
item: dict[str, Any] = {}
|
|
49
|
+
rest = line[2:].strip()
|
|
50
|
+
if ":" in rest:
|
|
51
|
+
k, _, v = rest.partition(":")
|
|
52
|
+
item[k.strip()] = _strip_quotes(v.strip())
|
|
53
|
+
parent[list_key].append(item)
|
|
54
|
+
stack.append((indent, item))
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
if ":" not in line:
|
|
58
|
+
continue
|
|
59
|
+
key, _, rest = line.partition(":")
|
|
60
|
+
key = key.strip()
|
|
61
|
+
rest = rest.strip()
|
|
62
|
+
if rest == "":
|
|
63
|
+
# Only known sequences — do not treat every plural key as a list
|
|
64
|
+
# (`profiles`, `images`, `templates` are mappings).
|
|
65
|
+
if key in ("sites", "aliases", "extras", "services", "flags"):
|
|
66
|
+
parent[key] = []
|
|
67
|
+
list_key = key
|
|
68
|
+
list_indent = indent
|
|
69
|
+
else:
|
|
70
|
+
child: dict[str, Any] = {}
|
|
71
|
+
parent[key] = child
|
|
72
|
+
stack.append((indent, child))
|
|
73
|
+
elif rest.startswith("[") and rest.endswith("]"):
|
|
74
|
+
inner = rest[1:-1].strip()
|
|
75
|
+
parent[key] = [_strip_quotes(x.strip()) for x in inner.split(",") if x.strip()] if inner else []
|
|
76
|
+
else:
|
|
77
|
+
parent[key] = _strip_quotes(rest)
|
|
78
|
+
return root
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_profile(conf: dict[str, Any], name: str) -> dict[str, Any]:
|
|
82
|
+
profiles = conf.get("profiles") or {}
|
|
83
|
+
if name not in profiles:
|
|
84
|
+
raise KeyError(f"profile not found: {name}")
|
|
85
|
+
return profiles[name]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main() -> int:
|
|
89
|
+
if len(sys.argv) < 3:
|
|
90
|
+
print("usage: load_config.py <packageconfig> <profile> [key...]", file=sys.stderr)
|
|
91
|
+
return 2
|
|
92
|
+
conf = load_packageconfig(Path(sys.argv[1]))
|
|
93
|
+
prof = get_profile(conf, sys.argv[2])
|
|
94
|
+
node: Any = prof
|
|
95
|
+
for k in sys.argv[3:]:
|
|
96
|
+
if isinstance(node, dict):
|
|
97
|
+
node = node.get(k, "")
|
|
98
|
+
else:
|
|
99
|
+
node = ""
|
|
100
|
+
if isinstance(node, (dict, list)):
|
|
101
|
+
import json
|
|
102
|
+
|
|
103
|
+
print(json.dumps(node))
|
|
104
|
+
else:
|
|
105
|
+
print(node)
|
|
106
|
+
return 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
sys.exit(main())
|
package/lib/prompt.sh
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
2
|
+
# Interactive component picker (TTY only).
|
|
3
|
+
|
|
4
|
+
infra_pick_profile() {
|
|
5
|
+
if [[ -n "${INFRA_PROFILE:-}" ]]; then
|
|
6
|
+
echo "$INFRA_PROFILE"
|
|
7
|
+
return 0
|
|
8
|
+
fi
|
|
9
|
+
if [[ ! -t 0 ]]; then
|
|
10
|
+
echo "Set INFRA_PROFILE=api|nodes|gateway (or pass --profile) when piping install.sh" >&2
|
|
11
|
+
return 1
|
|
12
|
+
fi
|
|
13
|
+
echo "Select component to install:" >&2
|
|
14
|
+
echo " 1) Backend (API)" >&2
|
|
15
|
+
echo " 2) Workers (nodes)" >&2
|
|
16
|
+
echo " 3) Gateway (HTTPS + UI)" >&2
|
|
17
|
+
local choice
|
|
18
|
+
read -r -p "Choice [1-3]: " choice
|
|
19
|
+
case "$choice" in
|
|
20
|
+
1|api|backend) echo "api" ;;
|
|
21
|
+
2|nodes|workers) echo "nodes" ;;
|
|
22
|
+
3|gateway) echo "gateway" ;;
|
|
23
|
+
*) echo "Invalid choice" >&2; return 1 ;;
|
|
24
|
+
esac
|
|
25
|
+
}
|
package/lib/tls.sh
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# shellcheck shell=bash
|
|
2
|
+
# TLS / certbot helpers.
|
|
3
|
+
|
|
4
|
+
infra_tls_check() {
|
|
5
|
+
local fullchain="$1"
|
|
6
|
+
local privkey="$2"
|
|
7
|
+
[[ -f "$fullchain" && -f "$privkey" ]]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
infra_tls_suggest_certbot() {
|
|
11
|
+
local domains=("$@")
|
|
12
|
+
local joined=""
|
|
13
|
+
local d
|
|
14
|
+
for d in "${domains[@]}"; do
|
|
15
|
+
joined+=" -d $d"
|
|
16
|
+
done
|
|
17
|
+
cat <<EOF
|
|
18
|
+
|
|
19
|
+
TLS certificates not found. Issue with certbot (port 80 must be free):
|
|
20
|
+
|
|
21
|
+
sudo certbot certonly --standalone${joined}
|
|
22
|
+
|
|
23
|
+
Or after gateway HTTP is up (webroot):
|
|
24
|
+
|
|
25
|
+
sudo certbot certonly --webroot -w /var/www/certbot${joined}
|
|
26
|
+
|
|
27
|
+
Then set TLS_FULLCHAIN and TLS_PRIVKEY in .env and run ./start.sh
|
|
28
|
+
|
|
29
|
+
EOF
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
infra_tls_offer_interactive() {
|
|
33
|
+
local fullchain="$1"
|
|
34
|
+
local privkey="$2"
|
|
35
|
+
shift 2
|
|
36
|
+
local domains=("$@")
|
|
37
|
+
if infra_tls_check "$fullchain" "$privkey"; then
|
|
38
|
+
return 0
|
|
39
|
+
fi
|
|
40
|
+
infra_tls_suggest_certbot "${domains[@]}"
|
|
41
|
+
if [[ ! -t 0 ]]; then
|
|
42
|
+
return 0
|
|
43
|
+
fi
|
|
44
|
+
read -r -p "Run certbot --standalone now? [y/N] " ans
|
|
45
|
+
case "$ans" in
|
|
46
|
+
y|Y|yes|YES)
|
|
47
|
+
if ! command -v certbot >/dev/null 2>&1; then
|
|
48
|
+
echo "certbot not found — install certbot first" >&2
|
|
49
|
+
return 1
|
|
50
|
+
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
|
+
;;
|
|
58
|
+
esac
|
|
59
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vibed-infra",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Product-agnostic VPS packager: wget install, Docker Compose, TLS/nginx, auto-update",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/naiemk/vibed-infra.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/naiemk/vibed-infra/issues"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/naiemk/vibed-infra#readme",
|
|
14
|
+
"bin": {
|
|
15
|
+
"vibed-infra": "./install.sh"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"install.sh",
|
|
19
|
+
"start.sh",
|
|
20
|
+
"update.sh",
|
|
21
|
+
"install-auto-update.sh",
|
|
22
|
+
"lib",
|
|
23
|
+
"templates",
|
|
24
|
+
"schema",
|
|
25
|
+
"github",
|
|
26
|
+
"skills"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "bash scripts/validate-package.sh"
|
|
33
|
+
}
|
|
34
|
+
}
|