vela 0.10.2 → 0.10.3
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 +34 -0
- package/dist/bin.js +2678 -684
- package/dist/bin.js.map +4 -4
- package/package.json +1 -1
- package/templates/server/apply.sh +349 -0
- package/templates/server/destroy.sh +55 -0
- package/templates/server/lib.sh +166 -0
- package/templates/server/pocketbase.sh +45 -0
- package/templates/server/provision.sh +153 -0
- package/templates/server/rollback.sh +88 -0
- package/templates/server/status.sh +44 -0
- package/templates/server/systemd/vela-pb@.service +41 -0
- package/templates/server/systemd/vela-web@.service +39 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Prepare a stock Ubuntu/Debian server to host vela apps.
|
|
4
|
+
#
|
|
5
|
+
# Idempotent: every step checks before it acts, so re-running after a CLI
|
|
6
|
+
# upgrade only fills in what is missing. It never touches an app's data, and it
|
|
7
|
+
# never rewrites an existing Caddyfile beyond appending one import line.
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
11
|
+
# shellcheck source=lib.sh
|
|
12
|
+
. "$SCRIPT_DIR/lib.sh"
|
|
13
|
+
|
|
14
|
+
PB_VERSION=""
|
|
15
|
+
NODE_MAJOR=22
|
|
16
|
+
CLI_VERSION="unknown"
|
|
17
|
+
LAYOUT_VERSION=1
|
|
18
|
+
|
|
19
|
+
while [ $# -gt 0 ]; do
|
|
20
|
+
case "$1" in
|
|
21
|
+
--pb-version) PB_VERSION=$2; shift 2 ;;
|
|
22
|
+
--node-major) NODE_MAJOR=$2; shift 2 ;;
|
|
23
|
+
--cli-version) CLI_VERSION=$2; shift 2 ;;
|
|
24
|
+
*) die "unknown argument: $1" ;;
|
|
25
|
+
esac
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
[ "$(id -u)" -eq 0 ] || die "provision must run as root"
|
|
29
|
+
|
|
30
|
+
. /etc/os-release
|
|
31
|
+
case "${ID:-}" in
|
|
32
|
+
ubuntu|debian) ;;
|
|
33
|
+
*) die "unsupported OS: ${PRETTY_NAME:-$ID}. vela provision supports Ubuntu and Debian." ;;
|
|
34
|
+
esac
|
|
35
|
+
|
|
36
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
37
|
+
APT_UPDATED=0
|
|
38
|
+
apt_update_once() {
|
|
39
|
+
[ "$APT_UPDATED" -eq 1 ] && return 0
|
|
40
|
+
apt-get update -qq
|
|
41
|
+
APT_UPDATED=1
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
ensure_packages() {
|
|
45
|
+
local missing=()
|
|
46
|
+
for pkg in "$@"; do
|
|
47
|
+
dpkg -s "$pkg" >/dev/null 2>&1 || missing+=("$pkg")
|
|
48
|
+
done
|
|
49
|
+
[ ${#missing[@]} -eq 0 ] && return 0
|
|
50
|
+
log "installing ${missing[*]}"
|
|
51
|
+
apt_update_once
|
|
52
|
+
apt-get install -y -qq --no-install-recommends "${missing[@]}" >/dev/null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
log "checking base packages"
|
|
56
|
+
ensure_packages ca-certificates curl gnupg rsync unzip tar sqlite3 jq git
|
|
57
|
+
|
|
58
|
+
# Native npm modules (argon2, better-sqlite3) are compiled on the server when a
|
|
59
|
+
# prebuild is unavailable, so the toolchain has to be here.
|
|
60
|
+
ensure_packages build-essential python3
|
|
61
|
+
|
|
62
|
+
install_node() {
|
|
63
|
+
local current=0
|
|
64
|
+
if command -v node >/dev/null 2>&1; then
|
|
65
|
+
current=$(node -p 'process.versions.node.split(".")[0]')
|
|
66
|
+
fi
|
|
67
|
+
if [ "$current" -ge 20 ] 2>/dev/null; then
|
|
68
|
+
log "node $(node -v) already installed"
|
|
69
|
+
return 0
|
|
70
|
+
fi
|
|
71
|
+
log "installing Node.js $NODE_MAJOR"
|
|
72
|
+
curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" -o /tmp/nodesource_setup.sh
|
|
73
|
+
bash /tmp/nodesource_setup.sh >/dev/null
|
|
74
|
+
rm -f /tmp/nodesource_setup.sh
|
|
75
|
+
apt-get install -y -qq nodejs >/dev/null
|
|
76
|
+
}
|
|
77
|
+
install_node
|
|
78
|
+
|
|
79
|
+
install_caddy() {
|
|
80
|
+
if command -v caddy >/dev/null 2>&1; then
|
|
81
|
+
log "caddy $(caddy version | head -1) already installed"
|
|
82
|
+
return 0
|
|
83
|
+
fi
|
|
84
|
+
log "installing caddy"
|
|
85
|
+
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
|
|
86
|
+
| gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
|
87
|
+
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
|
|
88
|
+
> /etc/apt/sources.list.d/caddy-stable.list
|
|
89
|
+
apt-get update -qq
|
|
90
|
+
apt-get install -y -qq caddy >/dev/null
|
|
91
|
+
APT_UPDATED=1
|
|
92
|
+
}
|
|
93
|
+
install_caddy
|
|
94
|
+
|
|
95
|
+
log "creating $VELA_USER user"
|
|
96
|
+
if ! id -u "$VELA_USER" >/dev/null 2>&1; then
|
|
97
|
+
useradd --system --home-dir "$VELA_ROOT" --shell /usr/sbin/nologin "$VELA_USER"
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
log "creating directory layout"
|
|
101
|
+
mkdir -p "$VELA_ROOT"/{apps,runtime,state,scripts,by-name,cache}
|
|
102
|
+
mkdir -p "$VELA_ETC"/{apps,caddy}
|
|
103
|
+
chown -R "$VELA_USER:$VELA_USER" "$VELA_ROOT/apps" "$VELA_ROOT/state" "$VELA_ROOT/by-name" "$VELA_ROOT/cache"
|
|
104
|
+
chmod 0755 "$VELA_ROOT" "$VELA_ROOT/apps"
|
|
105
|
+
chown root:root "$VELA_ETC" "$VELA_ETC/apps"
|
|
106
|
+
# Env files hold production secrets; only root reads them, systemd included.
|
|
107
|
+
chmod 0755 "$VELA_ETC"
|
|
108
|
+
chmod 0700 "$VELA_ETC/apps"
|
|
109
|
+
|
|
110
|
+
log "installing PocketBase $PB_VERSION"
|
|
111
|
+
"$SCRIPT_DIR/pocketbase.sh" "$PB_VERSION"
|
|
112
|
+
|
|
113
|
+
log "installing systemd unit templates"
|
|
114
|
+
install -m 0644 "$SCRIPT_DIR/systemd/vela-web@.service" /etc/systemd/system/vela-web@.service
|
|
115
|
+
install -m 0644 "$SCRIPT_DIR/systemd/vela-pb@.service" /etc/systemd/system/vela-pb@.service
|
|
116
|
+
systemctl daemon-reload
|
|
117
|
+
|
|
118
|
+
log "wiring caddy"
|
|
119
|
+
# An import glob that matches nothing is a hard error in Caddy, so keep a
|
|
120
|
+
# comment-only file in place before the first app is deployed.
|
|
121
|
+
if [ ! -f "$VELA_ETC/caddy/00-vela.caddy" ]; then
|
|
122
|
+
printf '# Managed by vela. App snippets are written alongside this file.\n' \
|
|
123
|
+
> "$VELA_ETC/caddy/00-vela.caddy"
|
|
124
|
+
fi
|
|
125
|
+
CADDYFILE=/etc/caddy/Caddyfile
|
|
126
|
+
mkdir -p /etc/caddy
|
|
127
|
+
[ -f "$CADDYFILE" ] || printf '' > "$CADDYFILE"
|
|
128
|
+
if ! grep -q 'import /etc/vela/caddy/\*.caddy' "$CADDYFILE"; then
|
|
129
|
+
{
|
|
130
|
+
printf '\n# Added by vela provision - app routes are generated into /etc/vela/caddy.\n'
|
|
131
|
+
printf 'import /etc/vela/caddy/*.caddy\n'
|
|
132
|
+
} >> "$CADDYFILE"
|
|
133
|
+
fi
|
|
134
|
+
caddy validate --config "$CADDYFILE" --adapter caddyfile >/dev/null 2>&1 \
|
|
135
|
+
|| die "existing /etc/caddy/Caddyfile is not valid after adding the vela import"
|
|
136
|
+
systemctl enable --now caddy >/dev/null 2>&1 || true
|
|
137
|
+
systemctl reload caddy 2>/dev/null || systemctl restart caddy
|
|
138
|
+
|
|
139
|
+
log "recording provisioning marker"
|
|
140
|
+
jq -c -n \
|
|
141
|
+
--arg cli "$CLI_VERSION" \
|
|
142
|
+
--arg pb "$PB_VERSION" \
|
|
143
|
+
--arg at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
144
|
+
--argjson layout "$LAYOUT_VERSION" \
|
|
145
|
+
'{cliVersion: $cli, pocketbaseVersion: $pb, provisionedAt: $at, layoutVersion: $layout}' \
|
|
146
|
+
> "$VELA_ETC/provisioned"
|
|
147
|
+
chmod 0644 "$VELA_ETC/provisioned"
|
|
148
|
+
|
|
149
|
+
emit_result \
|
|
150
|
+
--arg node "$(node -v)" \
|
|
151
|
+
--arg caddy "$(caddy version | head -1 | awk '{print $1}')" \
|
|
152
|
+
--arg pocketbase "$PB_VERSION" \
|
|
153
|
+
'{node: $node, caddy: $caddy, pocketbase: $pocketbase}'
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Put the previous release back for one instance.
|
|
4
|
+
#
|
|
5
|
+
# usage: rollback.sh <instance> [--to <release>]
|
|
6
|
+
set -Eeuo pipefail
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
9
|
+
# shellcheck source=lib.sh
|
|
10
|
+
. "$SCRIPT_DIR/lib.sh"
|
|
11
|
+
|
|
12
|
+
require_provisioned
|
|
13
|
+
[ "$(id -u)" -eq 0 ] || die "rollback must run as root"
|
|
14
|
+
|
|
15
|
+
INSTANCE=${1:-}; shift || true
|
|
16
|
+
[ -n "$INSTANCE" ] || die "usage: rollback.sh <instance> [--to <release>]"
|
|
17
|
+
|
|
18
|
+
TARGET=""
|
|
19
|
+
while [ $# -gt 0 ]; do
|
|
20
|
+
case "$1" in
|
|
21
|
+
--to) TARGET=$2; shift 2 ;;
|
|
22
|
+
*) die "unknown argument: $1" ;;
|
|
23
|
+
esac
|
|
24
|
+
done
|
|
25
|
+
|
|
26
|
+
# Commands that drop to the app user inherit this working directory, and the
|
|
27
|
+
# directory the CLI happened to invoke from is usually one it cannot stat.
|
|
28
|
+
cd "$VELA_ROOT"
|
|
29
|
+
|
|
30
|
+
APP=$(app_dir "$INSTANCE")
|
|
31
|
+
[ -f "$(state_file "$INSTANCE")" ] || die "no instance $INSTANCE on this server"
|
|
32
|
+
|
|
33
|
+
CURRENT=$(state_get "$INSTANCE" activeRelease || echo "")
|
|
34
|
+
[ -n "$TARGET" ] || TARGET=$(state_get "$INSTANCE" previousRelease || echo "")
|
|
35
|
+
[ -n "$TARGET" ] || die "no previous release recorded for $INSTANCE"
|
|
36
|
+
[ -d "$APP/releases/$TARGET" ] || die "release $TARGET is no longer on disk"
|
|
37
|
+
[ "$TARGET" != "$CURRENT" ] || die "$TARGET is already the active release"
|
|
38
|
+
|
|
39
|
+
BACKEND=$(state_get "$INSTANCE" backend 2>/dev/null || echo "true")
|
|
40
|
+
HEALTH_PATH=$(state_get "$INSTANCE" healthCheckPath 2>/dev/null || echo "/")
|
|
41
|
+
WEB_PORT=$(state_get "$INSTANCE" webPort)
|
|
42
|
+
PB_PORT=$(state_get "$INSTANCE" pbPort)
|
|
43
|
+
WEB_UNIT=$(unit_web "$INSTANCE")
|
|
44
|
+
PB_UNIT=$(unit_pb "$INSTANCE")
|
|
45
|
+
|
|
46
|
+
log "rolling back to $TARGET"
|
|
47
|
+
systemctl stop "$WEB_UNIT" >/dev/null 2>&1 || true
|
|
48
|
+
if [ "$BACKEND" = "true" ]; then
|
|
49
|
+
systemctl stop "$PB_UNIT" >/dev/null 2>&1 || true
|
|
50
|
+
# Down migrations belong to the release being left behind, so they run from
|
|
51
|
+
# the current release's migration set before the symlink moves.
|
|
52
|
+
if [ -n "$CURRENT" ] && [ -d "$APP/releases/$CURRENT/migrations" ]; then
|
|
53
|
+
AHEAD=$(migrations_ahead "$APP/releases/$CURRENT/migrations" "$APP/releases/$TARGET/migrations")
|
|
54
|
+
if [ "$AHEAD" -gt 0 ]; then
|
|
55
|
+
log "reverting $AHEAD migration(s) introduced after $TARGET"
|
|
56
|
+
runuser -u "$VELA_USER" -- "$APP/bin/pocketbase" \
|
|
57
|
+
--dir "$APP/shared/pb_data" \
|
|
58
|
+
--migrationsDir "$APP/releases/$CURRENT/migrations" \
|
|
59
|
+
migrate down "$AHEAD" >&2 \
|
|
60
|
+
|| die "down migrations failed - the app is still stopped"
|
|
61
|
+
fi
|
|
62
|
+
fi
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
ln -sfn "$APP/releases/$TARGET" "$APP/.current.tmp"
|
|
66
|
+
mv -Tf "$APP/.current.tmp" "$APP/current"
|
|
67
|
+
|
|
68
|
+
# The running release is part of the instance's environment, so it has to move
|
|
69
|
+
# with the symlink.
|
|
70
|
+
ETC=$(etc_dir "$INSTANCE")
|
|
71
|
+
if [ -f "$ETC/runtime.env" ]; then
|
|
72
|
+
sed -i "s|^VELA_RELEASE=.*|VELA_RELEASE=$TARGET|" "$ETC/runtime.env"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
if [ "$BACKEND" = "true" ]; then
|
|
76
|
+
systemctl restart "$PB_UNIT"
|
|
77
|
+
wait_for_http "http://127.0.0.1:$PB_PORT/api/health" 60 0.5 \
|
|
78
|
+
|| die "PocketBase did not become healthy after rollback"
|
|
79
|
+
fi
|
|
80
|
+
systemctl restart "$WEB_UNIT"
|
|
81
|
+
wait_for_http "http://127.0.0.1:$WEB_PORT$HEALTH_PATH" 60 0.5 \
|
|
82
|
+
|| die "app did not become healthy after rollback"
|
|
83
|
+
|
|
84
|
+
state_merge "$INSTANCE" "$(jq -c -n --arg t "$TARGET" --arg c "$CURRENT" \
|
|
85
|
+
--arg at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
86
|
+
'{activeRelease: $t, previousRelease: $c, rolledBackAt: $at}')"
|
|
87
|
+
|
|
88
|
+
emit_result --arg release "$TARGET" --arg from "$CURRENT" '{release: $release, from: $from}'
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Print the state of one instance, or every instance on the server, as JSON.
|
|
4
|
+
#
|
|
5
|
+
# usage: status.sh [instance]
|
|
6
|
+
set -Eeuo pipefail
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
9
|
+
# shellcheck source=lib.sh
|
|
10
|
+
. "$SCRIPT_DIR/lib.sh"
|
|
11
|
+
|
|
12
|
+
require_provisioned
|
|
13
|
+
|
|
14
|
+
describe() {
|
|
15
|
+
local instance=$1
|
|
16
|
+
local app; app=$(app_dir "$instance")
|
|
17
|
+
local state="$app/state.json"
|
|
18
|
+
[ -f "$state" ] || return 0
|
|
19
|
+
|
|
20
|
+
local web_state pb_state releases current
|
|
21
|
+
web_state=$(systemctl is-active "$(unit_web "$instance")" 2>/dev/null || true)
|
|
22
|
+
pb_state=$(systemctl is-active "$(unit_pb "$instance")" 2>/dev/null || true)
|
|
23
|
+
current=$([ -L "$app/current" ] && basename "$(readlink -f "$app/current")" || echo "")
|
|
24
|
+
releases=$(ls -1 "$app/releases" 2>/dev/null | sort -r | jq -R . | jq -sc .)
|
|
25
|
+
|
|
26
|
+
jq -c \
|
|
27
|
+
--arg web "$web_state" --arg pb "$pb_state" --arg current "$current" \
|
|
28
|
+
--argjson releases "$releases" \
|
|
29
|
+
'. + {services: {web: $web, pocketbase: $pb}, current: $current, releases: $releases}' \
|
|
30
|
+
"$state"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
collect() {
|
|
34
|
+
if [ $# -ge 1 ] && [ -n "$1" ]; then
|
|
35
|
+
describe "$1"
|
|
36
|
+
else
|
|
37
|
+
for dir in "$VELA_ROOT"/apps/*/; do
|
|
38
|
+
[ -d "$dir" ] || continue
|
|
39
|
+
describe "$(basename "$dir")"
|
|
40
|
+
done
|
|
41
|
+
fi
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
printf 'VELA_RESULT %s\n' "$(collect "$@" | jq -sc .)"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=vela PocketBase %i
|
|
3
|
+
Documentation=https://docs.velastack.dev
|
|
4
|
+
After=network-online.target
|
|
5
|
+
Wants=network-online.target
|
|
6
|
+
|
|
7
|
+
[Service]
|
|
8
|
+
Type=simple
|
|
9
|
+
User=vela
|
|
10
|
+
Group=vela
|
|
11
|
+
WorkingDirectory=/var/lib/vela/apps/%i
|
|
12
|
+
|
|
13
|
+
EnvironmentFile=-/etc/vela/apps/%i/env
|
|
14
|
+
EnvironmentFile=/etc/vela/apps/%i/runtime.env
|
|
15
|
+
|
|
16
|
+
ExecStart=/var/lib/vela/apps/%i/bin/pocketbase \
|
|
17
|
+
--dir /var/lib/vela/apps/%i/shared/pb_data \
|
|
18
|
+
--migrationsDir /var/lib/vela/apps/%i/current/migrations \
|
|
19
|
+
--hooksDir /var/lib/vela/apps/%i/current/hooks \
|
|
20
|
+
serve --http=127.0.0.1:${PB_PORT}
|
|
21
|
+
|
|
22
|
+
Restart=on-failure
|
|
23
|
+
RestartSec=2
|
|
24
|
+
TimeoutStartSec=60
|
|
25
|
+
TimeoutStopSec=30
|
|
26
|
+
KillSignal=SIGINT
|
|
27
|
+
|
|
28
|
+
StandardOutput=journal
|
|
29
|
+
StandardError=journal
|
|
30
|
+
SyslogIdentifier=vela-pb-%i
|
|
31
|
+
|
|
32
|
+
LimitNOFILE=65535
|
|
33
|
+
|
|
34
|
+
NoNewPrivileges=true
|
|
35
|
+
PrivateTmp=true
|
|
36
|
+
ProtectSystem=strict
|
|
37
|
+
ProtectHome=true
|
|
38
|
+
ReadWritePaths=/var/lib/vela/apps/%i
|
|
39
|
+
|
|
40
|
+
[Install]
|
|
41
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=vela app %i
|
|
3
|
+
Documentation=https://docs.velastack.dev
|
|
4
|
+
After=network-online.target vela-pb@%i.service
|
|
5
|
+
Wants=network-online.target
|
|
6
|
+
|
|
7
|
+
[Service]
|
|
8
|
+
Type=simple
|
|
9
|
+
User=vela
|
|
10
|
+
Group=vela
|
|
11
|
+
WorkingDirectory=/var/lib/vela/apps/%i/current
|
|
12
|
+
|
|
13
|
+
# Production secrets first, vela-generated runtime settings second, so PORT and
|
|
14
|
+
# POCKETBASE_URL always match what the server actually assigned.
|
|
15
|
+
EnvironmentFile=-/etc/vela/apps/%i/env
|
|
16
|
+
EnvironmentFile=/etc/vela/apps/%i/runtime.env
|
|
17
|
+
|
|
18
|
+
ExecStart=/usr/bin/node /var/lib/vela/apps/%i/current/build/index.js
|
|
19
|
+
|
|
20
|
+
Restart=on-failure
|
|
21
|
+
RestartSec=2
|
|
22
|
+
TimeoutStartSec=60
|
|
23
|
+
TimeoutStopSec=30
|
|
24
|
+
KillSignal=SIGINT
|
|
25
|
+
|
|
26
|
+
StandardOutput=journal
|
|
27
|
+
StandardError=journal
|
|
28
|
+
SyslogIdentifier=vela-web-%i
|
|
29
|
+
|
|
30
|
+
LimitNOFILE=65535
|
|
31
|
+
|
|
32
|
+
NoNewPrivileges=true
|
|
33
|
+
PrivateTmp=true
|
|
34
|
+
ProtectSystem=strict
|
|
35
|
+
ProtectHome=true
|
|
36
|
+
ReadWritePaths=/var/lib/vela/apps/%i
|
|
37
|
+
|
|
38
|
+
[Install]
|
|
39
|
+
WantedBy=multi-user.target
|