vela 0.10.14 → 0.11.1
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 +1 -1
- package/dist/bin.js +1297 -1023
- package/dist/bin.js.map +4 -4
- package/package.json +1 -1
- package/templates/server/apply.sh +51 -11
- package/templates/server/destroy.sh +2 -2
- package/templates/server/lib.sh +38 -0
- package/templates/server/origin.sh +62 -0
- package/templates/server/provision.sh +43 -5
package/package.json
CHANGED
|
@@ -24,6 +24,7 @@ APP_NAME=$INSTANCE
|
|
|
24
24
|
APP_ID=$INSTANCE
|
|
25
25
|
ENV_TAG=prod
|
|
26
26
|
DOMAIN=""
|
|
27
|
+
MANAGED=""
|
|
27
28
|
HEALTH_PATH=/
|
|
28
29
|
KEEP=5
|
|
29
30
|
PB_VERSION=""
|
|
@@ -37,6 +38,7 @@ while [ $# -gt 0 ]; do
|
|
|
37
38
|
--app-id) APP_ID=$2; shift 2 ;;
|
|
38
39
|
--env) ENV_TAG=$2; shift 2 ;;
|
|
39
40
|
--domain) DOMAIN=$2; shift 2 ;;
|
|
41
|
+
--managed) MANAGED=$2; shift 2 ;;
|
|
40
42
|
--health-path) HEALTH_PATH=$2; shift 2 ;;
|
|
41
43
|
--keep) KEEP=$2; shift 2 ;;
|
|
42
44
|
--pb-version) PB_VERSION=$2; shift 2 ;;
|
|
@@ -88,7 +90,10 @@ PORTS=$(allocate_ports "$INSTANCE")
|
|
|
88
90
|
WEB_PORT=$(printf '%s' "$PORTS" | jq -r .web)
|
|
89
91
|
PB_PORT=$(printf '%s' "$PORTS" | jq -r .pb)
|
|
90
92
|
|
|
93
|
+
# The canonical host: a domain the user's own DNS points here outranks a
|
|
94
|
+
# managed velastack.app name, which then redirects to it at the edge.
|
|
91
95
|
PRIMARY_DOMAIN=$(printf '%s' "$DOMAIN" | cut -d, -f1 | tr -d ' ')
|
|
96
|
+
[ -n "$PRIMARY_DOMAIN" ] || PRIMARY_DOMAIN=$(printf '%s' "$MANAGED" | cut -d, -f1 | tr -d ' ')
|
|
92
97
|
if [ -n "$PRIMARY_DOMAIN" ]; then
|
|
93
98
|
ORIGIN="https://$PRIMARY_DOMAIN"
|
|
94
99
|
else
|
|
@@ -325,17 +330,28 @@ ACTIVATED=1
|
|
|
325
330
|
state_merge "$INSTANCE" "$(jq -c -n \
|
|
326
331
|
--arg app "$APP_ID" --arg name "$APP_NAME" --arg env "$ENV_TAG" \
|
|
327
332
|
--arg instance "$INSTANCE" --arg release "$RELEASE" --arg previous "$PREVIOUS" \
|
|
328
|
-
--arg domain "$DOMAIN" --arg
|
|
333
|
+
--arg domain "$DOMAIN" --arg managed "$MANAGED" --arg url "$ORIGIN" \
|
|
334
|
+
--arg health "$HEALTH_PATH" --arg pb "$PB_VERSION" \
|
|
329
335
|
--arg sha "$GIT_SHA" --arg at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
330
336
|
--argjson web "$WEB_PORT" --argjson pbport "$PB_PORT" --argjson backend "$BACKEND" \
|
|
331
337
|
'{appId: $app, name: $name, env: $env, instance: $instance,
|
|
332
338
|
activeRelease: $release, previousRelease: $previous, domain: $domain,
|
|
339
|
+
managed: $managed, url: $url,
|
|
333
340
|
healthCheckPath: $health, pocketbaseVersion: $pb, gitSha: $sha,
|
|
334
341
|
webPort: $web, pbPort: $pbport, backend: ($backend == 1), deployedAt: $at}')"
|
|
335
342
|
|
|
336
343
|
# ------------------------------------------------------------------ routing
|
|
344
|
+
#
|
|
345
|
+
# Two files, written independently and each only if the whole Caddy config
|
|
346
|
+
# still validates with it in place (`caddy_install` keeps the previous one
|
|
347
|
+
# otherwise). Hosts the user's DNS points here get a site block of their own
|
|
348
|
+
# and a certificate each; managed velastack.app hosts arrive through the
|
|
349
|
+
# Worker at this server's origin site and are picked out by header.
|
|
337
350
|
|
|
338
351
|
CADDY_SNIPPET="$VELA_ETC/caddy/$INSTANCE.caddy"
|
|
352
|
+
ROUTE_SNIPPET="$VELA_ETC/caddy/routes/$INSTANCE.route"
|
|
353
|
+
ROUTING_CHANGED=0
|
|
354
|
+
|
|
339
355
|
if [ -n "$DOMAIN" ]; then
|
|
340
356
|
hosts=$(printf '%s' "$DOMAIN" | tr ',' '\n' | sed 's/^ *//; s/ *$//' | grep -v '^$' | paste -sd, - | sed 's/,/, /g')
|
|
341
357
|
tmp=$(mktemp "$VELA_ETC/caddy/.snippet.XXXXXX")
|
|
@@ -343,18 +359,42 @@ if [ -n "$DOMAIN" ]; then
|
|
|
343
359
|
printf '# Managed by vela - app %s (%s)\n' "$APP_NAME" "$INSTANCE"
|
|
344
360
|
printf '%s {\n\treverse_proxy 127.0.0.1:%s\n}\n' "$hosts" "$WEB_PORT"
|
|
345
361
|
} > "$tmp"
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if ! caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile >/dev/null 2>&1; then
|
|
349
|
-
rm -f "$CADDY_SNIPPET"
|
|
350
|
-
die "generated Caddy config for $DOMAIN is invalid"
|
|
351
|
-
fi
|
|
352
|
-
systemctl reload caddy
|
|
362
|
+
caddy_install "$tmp" "$CADDY_SNIPPET" || die "generated Caddy config for $DOMAIN is invalid"
|
|
363
|
+
ROUTING_CHANGED=1
|
|
353
364
|
elif [ -f "$CADDY_SNIPPET" ]; then
|
|
354
365
|
rm -f "$CADDY_SNIPPET"
|
|
355
|
-
|
|
366
|
+
ROUTING_CHANGED=1
|
|
356
367
|
fi
|
|
357
368
|
|
|
369
|
+
if [ -n "$MANAGED" ]; then
|
|
370
|
+
[ -f "$VELA_ETC/caddy/00-origin.caddy" ] \
|
|
371
|
+
|| die "managed hostnames need this server's origin site - the CLI runs origin.sh first"
|
|
372
|
+
# A matcher name is an identifier; instance ids carry dashes.
|
|
373
|
+
matcher=vela_$(printf '%s' "$INSTANCE" | tr -c 'a-zA-Z0-9_' '_')
|
|
374
|
+
tmp=$(mktemp "$VELA_ETC/caddy/routes/.route.XXXXXX")
|
|
375
|
+
{
|
|
376
|
+
printf '# Managed by vela - app %s (%s)\n' "$APP_NAME" "$INSTANCE"
|
|
377
|
+
printf '@%s {\n' "$matcher"
|
|
378
|
+
printf '%s' "$MANAGED" | tr ',' '\n' | sed 's/^ *//; s/ *$//' | grep -v '^$' \
|
|
379
|
+
| while IFS= read -r host; do printf '\theader X-Velastack-Host %s\n' "$host"; done
|
|
380
|
+
printf '}\n'
|
|
381
|
+
# The app should see the public host and the visitor, not the origin
|
|
382
|
+
# name and the Cloudflare edge that carried the request here.
|
|
383
|
+
printf 'handle @%s {\n' "$matcher"
|
|
384
|
+
printf '\treverse_proxy 127.0.0.1:%s {\n' "$WEB_PORT"
|
|
385
|
+
printf '\t\theader_up Host {http.request.header.X-Velastack-Host}\n'
|
|
386
|
+
printf '\t\theader_up X-Forwarded-For {http.request.header.X-Velastack-Client-IP}\n'
|
|
387
|
+
printf '\t}\n}\n'
|
|
388
|
+
} > "$tmp"
|
|
389
|
+
caddy_install "$tmp" "$ROUTE_SNIPPET" || die "generated Caddy route for $MANAGED is invalid"
|
|
390
|
+
ROUTING_CHANGED=1
|
|
391
|
+
elif [ -f "$ROUTE_SNIPPET" ]; then
|
|
392
|
+
rm -f "$ROUTE_SNIPPET"
|
|
393
|
+
ROUTING_CHANGED=1
|
|
394
|
+
fi
|
|
395
|
+
|
|
396
|
+
[ "$ROUTING_CHANGED" = 0 ] || caddy_reload
|
|
397
|
+
|
|
358
398
|
|
|
359
399
|
# ------------------------------------------------------------------- pruning
|
|
360
400
|
|
|
@@ -379,7 +419,7 @@ if [ "$KEEP" -gt 0 ] 2>/dev/null; then
|
|
|
379
419
|
fi
|
|
380
420
|
|
|
381
421
|
emit_result \
|
|
382
|
-
--arg instance "$INSTANCE" --arg release "$RELEASE" --arg domain "$DOMAIN" \
|
|
422
|
+
--arg instance "$INSTANCE" --arg release "$RELEASE" --arg domain "$DOMAIN" --arg managed "$MANAGED" \
|
|
383
423
|
--arg url "$ORIGIN" --argjson web "$WEB_PORT" --argjson pb "$PB_PORT" --argjson su "$SU_CREATED" \
|
|
384
|
-
'{instance: $instance, release: $release, domain: $domain, url: $url,
|
|
424
|
+
'{instance: $instance, release: $release, domain: $domain, managed: $managed, url: $url,
|
|
385
425
|
webPort: $web, pbPort: $pb, superuserCreated: ($su == 1)}'
|
|
@@ -34,8 +34,8 @@ for unit in "$(unit_web "$INSTANCE")" "$(unit_pb "$INSTANCE")"; do
|
|
|
34
34
|
done
|
|
35
35
|
|
|
36
36
|
log "removing routing"
|
|
37
|
-
rm -f "$VELA_ETC/caddy/$INSTANCE.caddy"
|
|
38
|
-
|
|
37
|
+
rm -f "$VELA_ETC/caddy/$INSTANCE.caddy" "$VELA_ETC/caddy/routes/$INSTANCE.route"
|
|
38
|
+
caddy_reload || true
|
|
39
39
|
|
|
40
40
|
log "removing releases"
|
|
41
41
|
rm -rf "$APP/releases" "$APP/deps" "$APP/current" "$APP/bin"
|
package/templates/server/lib.sh
CHANGED
|
@@ -228,3 +228,41 @@ assert_superuser_auth() {
|
|
|
228
228
|
-X POST -H 'content-type: application/json' --data-binary @- \
|
|
229
229
|
"http://127.0.0.1:$port/api/collections/_superusers/auth-with-password"
|
|
230
230
|
}
|
|
231
|
+
|
|
232
|
+
# ------------------------------------------------------------------- caddy
|
|
233
|
+
|
|
234
|
+
CADDYFILE=${CADDYFILE:-/etc/caddy/Caddyfile}
|
|
235
|
+
|
|
236
|
+
caddy_valid() {
|
|
237
|
+
caddy validate --config "$CADDYFILE" --adapter caddyfile >/dev/null 2>&1
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# Put a generated snippet in place only if the whole Caddy config still
|
|
241
|
+
# validates with it there. The file it replaces is kept and restored on
|
|
242
|
+
# failure, so a bad generation cannot take a working route down.
|
|
243
|
+
#
|
|
244
|
+
# usage: caddy_install <tmp> <dest> [mode] [owner]
|
|
245
|
+
caddy_install() {
|
|
246
|
+
local tmp=$1 dest=$2 mode=${3:-0644} owner=${4:-root:root} backup=""
|
|
247
|
+
chmod "$mode" "$tmp"
|
|
248
|
+
chown "$owner" "$tmp"
|
|
249
|
+
if [ -f "$dest" ]; then
|
|
250
|
+
backup=$(mktemp "$(dirname "$dest")/.previous.XXXXXX")
|
|
251
|
+
cp -p "$dest" "$backup"
|
|
252
|
+
fi
|
|
253
|
+
mv -f "$tmp" "$dest"
|
|
254
|
+
if caddy_valid; then
|
|
255
|
+
[ -z "$backup" ] || rm -f "$backup"
|
|
256
|
+
return 0
|
|
257
|
+
fi
|
|
258
|
+
if [ -n "$backup" ]; then
|
|
259
|
+
mv -f "$backup" "$dest"
|
|
260
|
+
else
|
|
261
|
+
rm -f "$dest"
|
|
262
|
+
fi
|
|
263
|
+
return 1
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
caddy_reload() {
|
|
267
|
+
systemctl reload caddy >/dev/null 2>&1 || systemctl restart caddy
|
|
268
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Publish this server's origin site: the one hostname the velastack.app Worker
|
|
4
|
+
# proxies managed hostnames to.
|
|
5
|
+
#
|
|
6
|
+
# Reads /etc/vela/origin.json, which the CLI writes after registering the
|
|
7
|
+
# server with velastack.dev. The site block refuses anything without the
|
|
8
|
+
# server's token, then hands matching requests to per-instance route files
|
|
9
|
+
# that apply.sh writes under routes/. Idempotent, and quiet when nothing has
|
|
10
|
+
# changed, so a deploy does not reload Caddy for no reason.
|
|
11
|
+
#
|
|
12
|
+
# usage: origin.sh
|
|
13
|
+
set -Eeuo pipefail
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
16
|
+
# shellcheck source=lib.sh
|
|
17
|
+
. "$SCRIPT_DIR/lib.sh"
|
|
18
|
+
|
|
19
|
+
require_provisioned
|
|
20
|
+
[ "$(id -u)" -eq 0 ] || die "origin must run as root"
|
|
21
|
+
|
|
22
|
+
ORIGIN_FILE="$VELA_ETC/origin.json"
|
|
23
|
+
[ -f "$ORIGIN_FILE" ] || die "$ORIGIN_FILE is missing - the CLI registers the server before running this"
|
|
24
|
+
|
|
25
|
+
HOST=$(jq -er .originHost "$ORIGIN_FILE") || die "$ORIGIN_FILE has no originHost"
|
|
26
|
+
TOKEN=$(jq -er .token "$ORIGIN_FILE") || die "$ORIGIN_FILE has no token"
|
|
27
|
+
|
|
28
|
+
ROUTES="$VELA_ETC/caddy/routes"
|
|
29
|
+
mkdir -p "$ROUTES"
|
|
30
|
+
chmod 0755 "$ROUTES"
|
|
31
|
+
# An import glob that matches nothing is a hard error in Caddy, so keep a
|
|
32
|
+
# comment-only file in place before the first managed instance is deployed.
|
|
33
|
+
if [ ! -f "$ROUTES/00-vela.route" ]; then
|
|
34
|
+
printf '# Managed by vela. Instance routes are written alongside this file.\n' \
|
|
35
|
+
> "$ROUTES/00-vela.route"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
SNIPPET="$VELA_ETC/caddy/00-origin.caddy"
|
|
39
|
+
desired=$(
|
|
40
|
+
printf '# Managed by vela - origin for velastack.app managed hostnames\n'
|
|
41
|
+
printf '%s {\n' "$HOST"
|
|
42
|
+
printf '\t@untrusted not header X-Velastack-Origin-Token "%s"\n' "$TOKEN"
|
|
43
|
+
printf '\thandle @untrusted {\n\t\trespond 403\n\t}\n'
|
|
44
|
+
printf '\timport %s/*.route\n' "$ROUTES"
|
|
45
|
+
printf '\thandle {\n\t\trespond 404\n\t}\n'
|
|
46
|
+
printf '}\n'
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if [ -f "$SNIPPET" ] && [ "$(cat "$SNIPPET")" = "$desired" ]; then
|
|
50
|
+
emit_result --arg host "$HOST" '{originHost: $host, changed: false}'
|
|
51
|
+
exit 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
tmp=$(mktemp "$VELA_ETC/caddy/.origin.XXXXXX")
|
|
55
|
+
printf '%s\n' "$desired" > "$tmp"
|
|
56
|
+
# Readable by the caddy user (which is what `caddy reload` runs as) and no one
|
|
57
|
+
# else: the token must not leak to the apps, which run as $VELA_USER.
|
|
58
|
+
caddy_install "$tmp" "$SNIPPET" 0640 root:caddy \
|
|
59
|
+
|| die "generated origin config for $HOST is invalid"
|
|
60
|
+
caddy_reload
|
|
61
|
+
|
|
62
|
+
emit_result --arg host "$HOST" '{originHost: $host, changed: true}'
|
|
@@ -34,10 +34,47 @@ case "${ID:-}" in
|
|
|
34
34
|
esac
|
|
35
35
|
|
|
36
36
|
export DEBIAN_FRONTEND=noninteractive
|
|
37
|
+
# Ubuntu's needrestart hook prints several paragraphs of "Restarting services"
|
|
38
|
+
# chatter to stderr after every install. Nothing vela installs needs a service
|
|
39
|
+
# restart mid-provision, so skip it.
|
|
40
|
+
export NEEDRESTART_SUSPEND=1
|
|
41
|
+
|
|
42
|
+
# A freshly created cloud box is usually still busy on first boot: cloud-init
|
|
43
|
+
# runs its package step and unattended-upgrades kicks off, both holding the
|
|
44
|
+
# dpkg lock. Provisioning that races them fails with "Could not get lock", so
|
|
45
|
+
# wait for first boot to settle before touching apt.
|
|
46
|
+
if command -v cloud-init >/dev/null 2>&1; then
|
|
47
|
+
log "waiting for cloud-init to finish"
|
|
48
|
+
cloud-init status --wait >/dev/null 2>&1 || true
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
APT_LOCK_TIMEOUT=${APT_LOCK_TIMEOUT:-600}
|
|
52
|
+
# Block until no other process holds an apt/dpkg lock, up to APT_LOCK_TIMEOUT
|
|
53
|
+
# seconds. Covers callers that invoke apt-get themselves (the nodesource setup
|
|
54
|
+
# script) and older apt without DPkg::Lock::Timeout support.
|
|
55
|
+
wait_for_apt() {
|
|
56
|
+
local waited=0 announced=0
|
|
57
|
+
command -v fuser >/dev/null 2>&1 || return 0
|
|
58
|
+
while fuser /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/lib/apt/lists/lock >/dev/null 2>&1; do
|
|
59
|
+
[ "$announced" -eq 1 ] || { log "waiting for another apt process to finish"; announced=1; }
|
|
60
|
+
[ "$waited" -lt "$APT_LOCK_TIMEOUT" ] \
|
|
61
|
+
|| die "gave up waiting for the apt lock after ${APT_LOCK_TIMEOUT}s - is unattended-upgrades still running?"
|
|
62
|
+
sleep 2
|
|
63
|
+
waited=$((waited + 2))
|
|
64
|
+
done
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Every apt-get call goes through here so a lock held by unattended-upgrades
|
|
68
|
+
# waits instead of failing the whole provision.
|
|
69
|
+
apt_get() {
|
|
70
|
+
wait_for_apt
|
|
71
|
+
apt-get -o DPkg::Lock::Timeout="$APT_LOCK_TIMEOUT" "$@"
|
|
72
|
+
}
|
|
73
|
+
|
|
37
74
|
APT_UPDATED=0
|
|
38
75
|
apt_update_once() {
|
|
39
76
|
[ "$APT_UPDATED" -eq 1 ] && return 0
|
|
40
|
-
|
|
77
|
+
apt_get update -qq
|
|
41
78
|
APT_UPDATED=1
|
|
42
79
|
}
|
|
43
80
|
|
|
@@ -49,7 +86,7 @@ ensure_packages() {
|
|
|
49
86
|
[ ${#missing[@]} -eq 0 ] && return 0
|
|
50
87
|
log "installing ${missing[*]}"
|
|
51
88
|
apt_update_once
|
|
52
|
-
|
|
89
|
+
apt_get install -y -qq --no-install-recommends "${missing[@]}" >/dev/null
|
|
53
90
|
}
|
|
54
91
|
|
|
55
92
|
log "checking base packages"
|
|
@@ -70,9 +107,10 @@ install_node() {
|
|
|
70
107
|
fi
|
|
71
108
|
log "installing Node.js $NODE_MAJOR"
|
|
72
109
|
curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" -o /tmp/nodesource_setup.sh
|
|
110
|
+
wait_for_apt
|
|
73
111
|
bash /tmp/nodesource_setup.sh >/dev/null
|
|
74
112
|
rm -f /tmp/nodesource_setup.sh
|
|
75
|
-
|
|
113
|
+
apt_get install -y -qq nodejs >/dev/null
|
|
76
114
|
}
|
|
77
115
|
install_node
|
|
78
116
|
|
|
@@ -86,8 +124,8 @@ install_caddy() {
|
|
|
86
124
|
| gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
|
87
125
|
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
|
|
88
126
|
> /etc/apt/sources.list.d/caddy-stable.list
|
|
89
|
-
|
|
90
|
-
|
|
127
|
+
apt_get update -qq
|
|
128
|
+
apt_get install -y -qq caddy >/dev/null
|
|
91
129
|
APT_UPDATED=1
|
|
92
130
|
}
|
|
93
131
|
install_caddy
|