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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vela",
3
- "version": "0.10.14",
3
+ "version": "0.11.1",
4
4
  "type": "module",
5
5
  "description": "A CLI for creating and updating SvelteKit projects",
6
6
  "license": "MIT",
@@ -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 health "$HEALTH_PATH" --arg pb "$PB_VERSION" \
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
- chmod 0644 "$tmp"
347
- mv -f "$tmp" "$CADDY_SNIPPET"
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
- systemctl reload caddy || true
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
- systemctl reload caddy >/dev/null 2>&1 || true
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"
@@ -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
- apt-get update -qq
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
- apt-get install -y -qq --no-install-recommends "${missing[@]}" >/dev/null
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
- apt-get install -y -qq nodejs >/dev/null
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
- apt-get update -qq
90
- apt-get install -y -qq caddy >/dev/null
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