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
package/package.json
CHANGED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Activate an uploaded release for one instance.
|
|
4
|
+
#
|
|
5
|
+
# The CLI has already put the release files in place; everything that makes them
|
|
6
|
+
# live happens here, in one transaction. If any step fails the previous release
|
|
7
|
+
# is put back and the services are restarted before exiting non-zero.
|
|
8
|
+
#
|
|
9
|
+
# usage: apply.sh <instance> <release> [options]
|
|
10
|
+
set -Eeuo pipefail
|
|
11
|
+
|
|
12
|
+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
13
|
+
# shellcheck source=lib.sh
|
|
14
|
+
. "$SCRIPT_DIR/lib.sh"
|
|
15
|
+
|
|
16
|
+
require_provisioned
|
|
17
|
+
[ "$(id -u)" -eq 0 ] || die "apply must run as root"
|
|
18
|
+
|
|
19
|
+
INSTANCE=${1:-}; shift || true
|
|
20
|
+
RELEASE=${1:-}; shift || true
|
|
21
|
+
[ -n "$INSTANCE" ] && [ -n "$RELEASE" ] || die "usage: apply.sh <instance> <release> [options]"
|
|
22
|
+
|
|
23
|
+
APP_NAME=$INSTANCE
|
|
24
|
+
APP_ID=$INSTANCE
|
|
25
|
+
ENV_TAG=prod
|
|
26
|
+
DOMAIN=""
|
|
27
|
+
HEALTH_PATH=/
|
|
28
|
+
KEEP=5
|
|
29
|
+
PB_VERSION=""
|
|
30
|
+
BACKEND=1
|
|
31
|
+
GIT_SHA=""
|
|
32
|
+
SU_CREATED=0
|
|
33
|
+
|
|
34
|
+
while [ $# -gt 0 ]; do
|
|
35
|
+
case "$1" in
|
|
36
|
+
--name) APP_NAME=$2; shift 2 ;;
|
|
37
|
+
--app-id) APP_ID=$2; shift 2 ;;
|
|
38
|
+
--env) ENV_TAG=$2; shift 2 ;;
|
|
39
|
+
--domain) DOMAIN=$2; shift 2 ;;
|
|
40
|
+
--health-path) HEALTH_PATH=$2; shift 2 ;;
|
|
41
|
+
--keep) KEEP=$2; shift 2 ;;
|
|
42
|
+
--pb-version) PB_VERSION=$2; shift 2 ;;
|
|
43
|
+
--backend) BACKEND=$2; shift 2 ;;
|
|
44
|
+
--git-sha) GIT_SHA=$2; shift 2 ;;
|
|
45
|
+
*) die "unknown argument: $1" ;;
|
|
46
|
+
esac
|
|
47
|
+
done
|
|
48
|
+
|
|
49
|
+
# Commands that drop to the app user inherit this working directory, and the
|
|
50
|
+
# directory the CLI happened to invoke from is usually one it cannot stat.
|
|
51
|
+
cd "$VELA_ROOT"
|
|
52
|
+
|
|
53
|
+
APP=$(app_dir "$INSTANCE")
|
|
54
|
+
ETC=$(etc_dir "$INSTANCE")
|
|
55
|
+
RELEASE_DIR="$APP/releases/$RELEASE"
|
|
56
|
+
WEB_UNIT=$(unit_web "$INSTANCE")
|
|
57
|
+
PB_UNIT=$(unit_pb "$INSTANCE")
|
|
58
|
+
|
|
59
|
+
[ -d "$RELEASE_DIR" ] || die "release $RELEASE was not uploaded to $RELEASE_DIR"
|
|
60
|
+
[ -f "$RELEASE_DIR/build/index.js" ] || die \
|
|
61
|
+
"release $RELEASE has no build/index.js - the app must build with @sveltejs/adapter-node"
|
|
62
|
+
|
|
63
|
+
mkdir -p "$APP"/{releases,shared,bin,deps} "$APP/shared/pb_data"
|
|
64
|
+
mkdir -p "$ETC"
|
|
65
|
+
chmod 0700 "$ETC"
|
|
66
|
+
chown -R "$VELA_USER:$VELA_USER" "$APP"
|
|
67
|
+
chown "$VELA_USER:$VELA_USER" "$RELEASE_DIR"
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------- ports & env
|
|
70
|
+
|
|
71
|
+
PORTS=$(allocate_ports "$INSTANCE")
|
|
72
|
+
WEB_PORT=$(printf '%s' "$PORTS" | jq -r .web)
|
|
73
|
+
PB_PORT=$(printf '%s' "$PORTS" | jq -r .pb)
|
|
74
|
+
|
|
75
|
+
PRIMARY_DOMAIN=$(printf '%s' "$DOMAIN" | cut -d, -f1 | tr -d ' ')
|
|
76
|
+
if [ -n "$PRIMARY_DOMAIN" ]; then
|
|
77
|
+
ORIGIN="https://$PRIMARY_DOMAIN"
|
|
78
|
+
else
|
|
79
|
+
ORIGIN="http://127.0.0.1:$WEB_PORT"
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Production secrets live in $ETC/env and are managed only by `vela env`.
|
|
83
|
+
# Nothing here reads or rewrites that file.
|
|
84
|
+
[ -f "$ETC/env" ] || { : > "$ETC/env"; chmod 0600 "$ETC/env"; chown root:root "$ETC/env"; }
|
|
85
|
+
|
|
86
|
+
runtime_tmp=$(mktemp "$ETC/.runtime.XXXXXX")
|
|
87
|
+
{
|
|
88
|
+
printf '# Generated by vela on each deploy. Edit /etc/vela/apps/%s/env instead.\n' "$INSTANCE"
|
|
89
|
+
printf 'NODE_ENV=production\n'
|
|
90
|
+
printf 'HOST=127.0.0.1\n'
|
|
91
|
+
printf 'PORT=%s\n' "$WEB_PORT"
|
|
92
|
+
printf 'ORIGIN=%s\n' "$ORIGIN"
|
|
93
|
+
printf 'PB_PORT=%s\n' "$PB_PORT"
|
|
94
|
+
printf 'POCKETBASE_URL=http://127.0.0.1:%s\n' "$PB_PORT"
|
|
95
|
+
printf 'VELA_APP_ID=%s\n' "$APP_ID"
|
|
96
|
+
printf 'VELA_APP_NAME=%s\n' "$APP_NAME"
|
|
97
|
+
printf 'VELA_ENV=%s\n' "$ENV_TAG"
|
|
98
|
+
printf 'VELA_RELEASE=%s\n' "$RELEASE"
|
|
99
|
+
} > "$runtime_tmp"
|
|
100
|
+
chmod 0600 "$runtime_tmp"
|
|
101
|
+
chown root:root "$runtime_tmp"
|
|
102
|
+
mv -f "$runtime_tmp" "$ETC/runtime.env"
|
|
103
|
+
|
|
104
|
+
# ------------------------------------------------------------------- runtime
|
|
105
|
+
|
|
106
|
+
if [ "$BACKEND" = "1" ]; then
|
|
107
|
+
[ -n "$PB_VERSION" ] || die "--pb-version is required for an app with a backend"
|
|
108
|
+
PB_BIN=$("$SCRIPT_DIR/pocketbase.sh" "$PB_VERSION" | tail -1)
|
|
109
|
+
ln -sfn "$PB_BIN" "$APP/bin/.pocketbase.tmp"
|
|
110
|
+
mv -Tf "$APP/bin/.pocketbase.tmp" "$APP/bin/pocketbase"
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# --------------------------------------------------------------- dependencies
|
|
114
|
+
#
|
|
115
|
+
# node_modules is keyed by the lockfile hash and shared between releases that
|
|
116
|
+
# pin the same dependencies: redeploys of unchanged dependencies skip the
|
|
117
|
+
# install entirely, and a rollback still finds the tree its own lockfile
|
|
118
|
+
# described.
|
|
119
|
+
|
|
120
|
+
install_deps() {
|
|
121
|
+
local lock="$RELEASE_DIR/package-lock.json"
|
|
122
|
+
local key
|
|
123
|
+
local -a install_cmd
|
|
124
|
+
# `--no-engine-strict`: vela projects set engine-strict for development, but a
|
|
125
|
+
# production install takes only `dependencies` - a dev tool that wants a newer
|
|
126
|
+
# Node than the server runs is not a reason to refuse the deploy.
|
|
127
|
+
local -a flags=(--omit=dev --no-audit --no-fund --no-engine-strict --loglevel=error)
|
|
128
|
+
if [ -f "$lock" ]; then
|
|
129
|
+
key=$(sha256sum "$lock" | cut -c1-16)
|
|
130
|
+
install_cmd=(npm ci "${flags[@]}")
|
|
131
|
+
elif [ -f "$RELEASE_DIR/package.json" ]; then
|
|
132
|
+
key=$(sha256sum "$RELEASE_DIR/package.json" | cut -c1-16)
|
|
133
|
+
install_cmd=(npm install "${flags[@]}")
|
|
134
|
+
else
|
|
135
|
+
log "no package.json in release - skipping dependency install"
|
|
136
|
+
return 0
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
local deps="$APP/deps/$key"
|
|
140
|
+
if [ ! -d "$deps/node_modules" ]; then
|
|
141
|
+
log "installing dependencies ($key)"
|
|
142
|
+
rm -rf "$deps"
|
|
143
|
+
mkdir -p "$deps"
|
|
144
|
+
cp "$RELEASE_DIR/package.json" "$deps/"
|
|
145
|
+
if [ -f "$lock" ]; then cp "$lock" "$deps/"; fi
|
|
146
|
+
if [ -f "$RELEASE_DIR/.npmrc" ]; then cp "$RELEASE_DIR/.npmrc" "$deps/"; fi
|
|
147
|
+
chown -R "$VELA_USER:$VELA_USER" "$deps"
|
|
148
|
+
# npm needs a writable HOME and cache; $VELA_ROOT itself stays root-owned
|
|
149
|
+
# so the app user can never swap out the scripts root runs.
|
|
150
|
+
mkdir -p "$VELA_ROOT/cache/npm"
|
|
151
|
+
chown -R "$VELA_USER:$VELA_USER" "$VELA_ROOT/cache"
|
|
152
|
+
( cd "$deps" && runuser -u "$VELA_USER" -- env \
|
|
153
|
+
HOME="$deps" npm_config_cache="$VELA_ROOT/cache/npm" \
|
|
154
|
+
"${install_cmd[@]}" >&2 ) || die "dependency install failed"
|
|
155
|
+
else
|
|
156
|
+
log "dependencies already installed ($key)"
|
|
157
|
+
fi
|
|
158
|
+
ln -sfn "$deps/node_modules" "$RELEASE_DIR/.node_modules.tmp"
|
|
159
|
+
mv -Tf "$RELEASE_DIR/.node_modules.tmp" "$RELEASE_DIR/node_modules"
|
|
160
|
+
printf '%s' "$key" > "$RELEASE_DIR/.vela-deps"
|
|
161
|
+
}
|
|
162
|
+
install_deps
|
|
163
|
+
|
|
164
|
+
# --------------------------------------------------------------- the swap
|
|
165
|
+
|
|
166
|
+
PREVIOUS=""
|
|
167
|
+
if [ -L "$APP/current" ]; then
|
|
168
|
+
PREVIOUS=$(basename "$(readlink -f "$APP/current")")
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
# From here on the instance is being mutated. Any exit before ACTIVATED=1 puts
|
|
172
|
+
# the previous release back - a failed deploy must never leave a dead service.
|
|
173
|
+
IN_TRANSACTION=0
|
|
174
|
+
ACTIVATED=0
|
|
175
|
+
|
|
176
|
+
restore() {
|
|
177
|
+
log "deploy failed - restoring ${PREVIOUS:-nothing}"
|
|
178
|
+
if [ -n "$PREVIOUS" ] && [ -d "$APP/releases/$PREVIOUS" ]; then
|
|
179
|
+
ln -sfn "$APP/releases/$PREVIOUS" "$APP/.current.tmp"
|
|
180
|
+
mv -Tf "$APP/.current.tmp" "$APP/current"
|
|
181
|
+
if [ "$BACKEND" = "1" ]; then systemctl restart "$PB_UNIT" >/dev/null 2>&1 || true; fi
|
|
182
|
+
systemctl restart "$WEB_UNIT" >/dev/null 2>&1 || true
|
|
183
|
+
else
|
|
184
|
+
systemctl stop "$WEB_UNIT" >/dev/null 2>&1 || true
|
|
185
|
+
if [ "$BACKEND" = "1" ]; then systemctl stop "$PB_UNIT" >/dev/null 2>&1 || true; fi
|
|
186
|
+
fi
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
on_exit() {
|
|
190
|
+
local code=$?
|
|
191
|
+
if [ "$IN_TRANSACTION" = "1" ] && [ "$ACTIVATED" = "0" ]; then restore; fi
|
|
192
|
+
exit "$code"
|
|
193
|
+
}
|
|
194
|
+
trap on_exit EXIT
|
|
195
|
+
|
|
196
|
+
IN_TRANSACTION=1
|
|
197
|
+
|
|
198
|
+
log "stopping services"
|
|
199
|
+
systemctl stop "$WEB_UNIT" >/dev/null 2>&1 || true
|
|
200
|
+
if [ "$BACKEND" = "1" ]; then
|
|
201
|
+
systemctl stop "$PB_UNIT" >/dev/null 2>&1 || true
|
|
202
|
+
|
|
203
|
+
if [ -d "$RELEASE_DIR/migrations" ]; then
|
|
204
|
+
log "running migrations"
|
|
205
|
+
runuser -u "$VELA_USER" -- "$APP/bin/pocketbase" \
|
|
206
|
+
--dir "$APP/shared/pb_data" \
|
|
207
|
+
--migrationsDir "$RELEASE_DIR/migrations" \
|
|
208
|
+
migrate up >&2
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------- superuser bootstrap
|
|
212
|
+
#
|
|
213
|
+
# The app authenticates to its own PocketBase as a superuser on every render,
|
|
214
|
+
# so a database without one answers every request with a 401 and the deploy
|
|
215
|
+
# fails its health check. A brand new instance has an empty pb_data, which is
|
|
216
|
+
# why the account is created here rather than assumed to exist.
|
|
217
|
+
#
|
|
218
|
+
# $ETC/env is otherwise `vela env`'s alone. This is the single exception, and
|
|
219
|
+
# it writes only on the deploy that creates the account. From then on the env
|
|
220
|
+
# file is the source of truth: a value changed there is pushed into the
|
|
221
|
+
# database by the next deploy, which is what keeps the two from drifting.
|
|
222
|
+
SU_EMAIL=$(env_file_get "$ETC/env" POCKETBASE_SUPERUSER_EMAIL || true)
|
|
223
|
+
SU_PASSWORD=$(env_file_get "$ETC/env" POCKETBASE_SUPERUSER_PASSWORD || true)
|
|
224
|
+
|
|
225
|
+
if [ -z "$SU_EMAIL" ] || [ -z "$SU_PASSWORD" ]; then
|
|
226
|
+
SU_EMAIL=${SU_EMAIL:-admin@${PRIMARY_DOMAIN:-$INSTANCE.invalid}}
|
|
227
|
+
SU_PASSWORD=$(random_secret)
|
|
228
|
+
SU_CREATED=1
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
# The password reaches PocketBase as an argument, so the upsert runs only on
|
|
232
|
+
# the deploys that need it rather than on every one.
|
|
233
|
+
SU_STAMP="$ETC/.superuser"
|
|
234
|
+
SU_FINGERPRINT=$(printf '%s\n%s' "$SU_EMAIL" "$SU_PASSWORD" | sha256sum | cut -d' ' -f1)
|
|
235
|
+
if [ "$SU_CREATED" = "1" ] || [ "$(cat "$SU_STAMP" 2>/dev/null || true)" != "$SU_FINGERPRINT" ]; then
|
|
236
|
+
if [ "$SU_CREATED" = "1" ]; then
|
|
237
|
+
log "creating the PocketBase superuser"
|
|
238
|
+
else
|
|
239
|
+
log "updating the PocketBase superuser"
|
|
240
|
+
fi
|
|
241
|
+
runuser -u "$VELA_USER" -- "$APP/bin/pocketbase" \
|
|
242
|
+
--dir "$APP/shared/pb_data" \
|
|
243
|
+
superuser upsert "$SU_EMAIL" "$SU_PASSWORD" >&2 \
|
|
244
|
+
|| die "could not create the PocketBase superuser"
|
|
245
|
+
|
|
246
|
+
# Recorded only once the database holds the account, so a failure above
|
|
247
|
+
# leaves nothing behind and the next deploy simply tries again.
|
|
248
|
+
if [ "$SU_CREATED" = "1" ]; then
|
|
249
|
+
env_file_append "$ETC/env" POCKETBASE_SUPERUSER_EMAIL "$SU_EMAIL"
|
|
250
|
+
env_file_append "$ETC/env" POCKETBASE_SUPERUSER_PASSWORD "$SU_PASSWORD"
|
|
251
|
+
fi
|
|
252
|
+
printf '%s\n' "$SU_FINGERPRINT" > "$SU_STAMP"
|
|
253
|
+
chmod 0600 "$SU_STAMP"
|
|
254
|
+
chown root:root "$SU_STAMP"
|
|
255
|
+
fi
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
log "activating release $RELEASE"
|
|
259
|
+
ln -sfn "$RELEASE_DIR" "$APP/.current.tmp"
|
|
260
|
+
mv -Tf "$APP/.current.tmp" "$APP/current"
|
|
261
|
+
# A human-readable alias for the opaque app id, purely for looking around.
|
|
262
|
+
if [ "$ENV_TAG" = "prod" ]; then LINK_LABEL=$APP_NAME; else LINK_LABEL="$APP_NAME-$ENV_TAG"; fi
|
|
263
|
+
LINK_NAME=$(printf '%s' "$LINK_LABEL" | tr -c 'A-Za-z0-9._-' '-')
|
|
264
|
+
if [ -n "$LINK_NAME" ]; then
|
|
265
|
+
ln -sfn "$APP" "$VELA_ROOT/by-name/.link.tmp" && \
|
|
266
|
+
mv -Tf "$VELA_ROOT/by-name/.link.tmp" "$VELA_ROOT/by-name/$LINK_NAME" || true
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
systemctl daemon-reload
|
|
270
|
+
|
|
271
|
+
if [ "$BACKEND" = "1" ]; then
|
|
272
|
+
log "starting PocketBase on 127.0.0.1:$PB_PORT"
|
|
273
|
+
systemctl enable "$PB_UNIT" >/dev/null 2>&1 || true
|
|
274
|
+
systemctl restart "$PB_UNIT"
|
|
275
|
+
wait_for_http "http://127.0.0.1:$PB_PORT/api/health" 60 0.5 \
|
|
276
|
+
|| die "PocketBase did not become healthy - journalctl -u $PB_UNIT"
|
|
277
|
+
fi
|
|
278
|
+
|
|
279
|
+
log "starting app on 127.0.0.1:$WEB_PORT"
|
|
280
|
+
systemctl enable "$WEB_UNIT" >/dev/null 2>&1 || true
|
|
281
|
+
systemctl restart "$WEB_UNIT"
|
|
282
|
+
wait_for_http "http://127.0.0.1:$WEB_PORT$HEALTH_PATH" 60 0.5 \
|
|
283
|
+
|| die "app did not become healthy at $HEALTH_PATH - journalctl -u $WEB_UNIT"
|
|
284
|
+
|
|
285
|
+
ACTIVATED=1
|
|
286
|
+
|
|
287
|
+
# --------------------------------------------------------------------- state
|
|
288
|
+
|
|
289
|
+
state_merge "$INSTANCE" "$(jq -c -n \
|
|
290
|
+
--arg app "$APP_ID" --arg name "$APP_NAME" --arg env "$ENV_TAG" \
|
|
291
|
+
--arg instance "$INSTANCE" --arg release "$RELEASE" --arg previous "$PREVIOUS" \
|
|
292
|
+
--arg domain "$DOMAIN" --arg health "$HEALTH_PATH" --arg pb "$PB_VERSION" \
|
|
293
|
+
--arg sha "$GIT_SHA" --arg at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
294
|
+
--argjson web "$WEB_PORT" --argjson pbport "$PB_PORT" --argjson backend "$BACKEND" \
|
|
295
|
+
'{appId: $app, name: $name, env: $env, instance: $instance,
|
|
296
|
+
activeRelease: $release, previousRelease: $previous, domain: $domain,
|
|
297
|
+
healthCheckPath: $health, pocketbaseVersion: $pb, gitSha: $sha,
|
|
298
|
+
webPort: $web, pbPort: $pbport, backend: ($backend == 1), deployedAt: $at}')"
|
|
299
|
+
|
|
300
|
+
# ------------------------------------------------------------------ routing
|
|
301
|
+
|
|
302
|
+
CADDY_SNIPPET="$VELA_ETC/caddy/$INSTANCE.caddy"
|
|
303
|
+
if [ -n "$DOMAIN" ]; then
|
|
304
|
+
hosts=$(printf '%s' "$DOMAIN" | tr ',' '\n' | sed 's/^ *//; s/ *$//' | grep -v '^$' | paste -sd, - | sed 's/,/, /g')
|
|
305
|
+
tmp=$(mktemp "$VELA_ETC/caddy/.snippet.XXXXXX")
|
|
306
|
+
{
|
|
307
|
+
printf '# Managed by vela - app %s (%s)\n' "$APP_NAME" "$INSTANCE"
|
|
308
|
+
printf '%s {\n\treverse_proxy 127.0.0.1:%s\n}\n' "$hosts" "$WEB_PORT"
|
|
309
|
+
} > "$tmp"
|
|
310
|
+
chmod 0644 "$tmp"
|
|
311
|
+
mv -f "$tmp" "$CADDY_SNIPPET"
|
|
312
|
+
if ! caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile >/dev/null 2>&1; then
|
|
313
|
+
rm -f "$CADDY_SNIPPET"
|
|
314
|
+
die "generated Caddy config for $DOMAIN is invalid"
|
|
315
|
+
fi
|
|
316
|
+
systemctl reload caddy
|
|
317
|
+
elif [ -f "$CADDY_SNIPPET" ]; then
|
|
318
|
+
rm -f "$CADDY_SNIPPET"
|
|
319
|
+
systemctl reload caddy || true
|
|
320
|
+
fi
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# ------------------------------------------------------------------- pruning
|
|
324
|
+
|
|
325
|
+
if [ "$KEEP" -gt 0 ] 2>/dev/null; then
|
|
326
|
+
mapfile -t old < <(ls -1 "$APP/releases" | sort -r | tail -n +$((KEEP + 1)))
|
|
327
|
+
for rel in "${old[@]:-}"; do
|
|
328
|
+
[ -n "$rel" ] || continue
|
|
329
|
+
[ "$rel" = "$RELEASE" ] && continue
|
|
330
|
+
[ "$rel" = "$PREVIOUS" ] && continue
|
|
331
|
+
log "pruning release $rel"
|
|
332
|
+
rm -rf "${APP:?}/releases/$rel"
|
|
333
|
+
done
|
|
334
|
+
# Drop dependency trees no surviving release refers to.
|
|
335
|
+
for deps in "$APP"/deps/*; do
|
|
336
|
+
[ -d "$deps" ] || continue
|
|
337
|
+
key=$(basename "$deps")
|
|
338
|
+
if ! grep -rqs -- "$key" "$APP"/releases/*/.vela-deps; then
|
|
339
|
+
log "pruning dependencies $key"
|
|
340
|
+
rm -rf "$deps"
|
|
341
|
+
fi
|
|
342
|
+
done
|
|
343
|
+
fi
|
|
344
|
+
|
|
345
|
+
emit_result \
|
|
346
|
+
--arg instance "$INSTANCE" --arg release "$RELEASE" --arg domain "$DOMAIN" \
|
|
347
|
+
--arg url "$ORIGIN" --argjson web "$WEB_PORT" --argjson pb "$PB_PORT" --argjson su "$SU_CREATED" \
|
|
348
|
+
'{instance: $instance, release: $release, domain: $domain, url: $url,
|
|
349
|
+
webPort: $web, pbPort: $pb, superuserCreated: ($su == 1)}'
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Remove one instance from the server.
|
|
4
|
+
#
|
|
5
|
+
# Releases and configuration always go; the database and uploads only go with
|
|
6
|
+
# --purge, so a mistyped instance name cannot silently delete production data.
|
|
7
|
+
#
|
|
8
|
+
# usage: destroy.sh <instance> [--purge]
|
|
9
|
+
set -Eeuo pipefail
|
|
10
|
+
|
|
11
|
+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
12
|
+
# shellcheck source=lib.sh
|
|
13
|
+
. "$SCRIPT_DIR/lib.sh"
|
|
14
|
+
|
|
15
|
+
require_provisioned
|
|
16
|
+
[ "$(id -u)" -eq 0 ] || die "destroy must run as root"
|
|
17
|
+
|
|
18
|
+
INSTANCE=${1:-}; shift || true
|
|
19
|
+
[ -n "$INSTANCE" ] || die "usage: destroy.sh <instance> [--purge]"
|
|
20
|
+
PURGE=0
|
|
21
|
+
while [ $# -gt 0 ]; do
|
|
22
|
+
case "$1" in
|
|
23
|
+
--purge) PURGE=1; shift ;;
|
|
24
|
+
*) die "unknown argument: $1" ;;
|
|
25
|
+
esac
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
APP=$(app_dir "$INSTANCE")
|
|
29
|
+
ETC=$(etc_dir "$INSTANCE")
|
|
30
|
+
|
|
31
|
+
log "stopping services"
|
|
32
|
+
for unit in "$(unit_web "$INSTANCE")" "$(unit_pb "$INSTANCE")"; do
|
|
33
|
+
systemctl disable --now "$unit" >/dev/null 2>&1 || true
|
|
34
|
+
done
|
|
35
|
+
|
|
36
|
+
log "removing routing"
|
|
37
|
+
rm -f "$VELA_ETC/caddy/$INSTANCE.caddy"
|
|
38
|
+
systemctl reload caddy >/dev/null 2>&1 || true
|
|
39
|
+
|
|
40
|
+
log "removing releases"
|
|
41
|
+
rm -rf "$APP/releases" "$APP/deps" "$APP/current" "$APP/bin"
|
|
42
|
+
|
|
43
|
+
if [ "$PURGE" = "1" ]; then
|
|
44
|
+
log "purging data and configuration"
|
|
45
|
+
rm -rf "$APP" "$ETC"
|
|
46
|
+
release_ports "$INSTANCE"
|
|
47
|
+
else
|
|
48
|
+
log "keeping $APP/shared (pass --purge to remove the database)"
|
|
49
|
+
rm -f "$APP/state.json"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
find "$VELA_ROOT/by-name" -maxdepth 1 -type l ! -exec test -e {} \; -delete 2>/dev/null || true
|
|
53
|
+
|
|
54
|
+
emit_result --arg instance "$INSTANCE" --argjson purged "$PURGE" \
|
|
55
|
+
'{instance: $instance, purged: ($purged == 1)}'
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared helpers for the vela server scripts. Sourced, never run directly.
|
|
3
|
+
|
|
4
|
+
VELA_ROOT=${VELA_ROOT:-/var/lib/vela}
|
|
5
|
+
VELA_ETC=${VELA_ETC:-/etc/vela}
|
|
6
|
+
VELA_USER=${VELA_USER:-vela}
|
|
7
|
+
VELA_WEB_PORT_BASE=${VELA_WEB_PORT_BASE:-4100}
|
|
8
|
+
VELA_PB_PORT_BASE=${VELA_PB_PORT_BASE:-8100}
|
|
9
|
+
VELA_PORT_RANGE=${VELA_PORT_RANGE:-900}
|
|
10
|
+
|
|
11
|
+
app_dir() { printf '%s/apps/%s' "$VELA_ROOT" "$1"; }
|
|
12
|
+
etc_dir() { printf '%s/apps/%s' "$VELA_ETC" "$1"; }
|
|
13
|
+
state_file() { printf '%s/apps/%s/state.json' "$VELA_ROOT" "$1"; }
|
|
14
|
+
|
|
15
|
+
log() { printf ' %s\n' "$*" >&2; }
|
|
16
|
+
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
|
|
17
|
+
|
|
18
|
+
require_provisioned() {
|
|
19
|
+
[ -f "$VELA_ETC/provisioned" ] || die "server is not provisioned - run 'vela provision' first"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Read a top-level key out of an instance's state file.
|
|
23
|
+
state_get() {
|
|
24
|
+
local instance=$1 key=$2 file
|
|
25
|
+
file=$(state_file "$instance")
|
|
26
|
+
[ -f "$file" ] || return 1
|
|
27
|
+
# `// empty` would swallow a legitimate `false`, so test for the key itself.
|
|
28
|
+
jq -er --arg k "$key" 'if has($k) and .[$k] != null then .[$k] else empty end' "$file" 2>/dev/null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Merge a JSON object into an instance's state file, atomically.
|
|
32
|
+
state_merge() {
|
|
33
|
+
local instance=$1 patch=$2 file tmp
|
|
34
|
+
file=$(state_file "$instance")
|
|
35
|
+
mkdir -p "$(dirname "$file")"
|
|
36
|
+
[ -f "$file" ] || echo '{}' > "$file"
|
|
37
|
+
tmp=$(mktemp "$(dirname "$file")/.state.XXXXXX")
|
|
38
|
+
jq -S --argjson patch "$patch" '. * $patch' "$file" > "$tmp"
|
|
39
|
+
chown "$VELA_USER:$VELA_USER" "$tmp" 2>/dev/null || true
|
|
40
|
+
mv -f "$tmp" "$file"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Assign a web/PocketBase port pair to an instance, or echo the existing one.
|
|
44
|
+
# Ports live in one server-wide file guarded by flock so that concurrent
|
|
45
|
+
# deploys of different apps cannot land on the same port.
|
|
46
|
+
allocate_ports() {
|
|
47
|
+
local instance=$1
|
|
48
|
+
local ports_file="$VELA_ROOT/state/ports.json"
|
|
49
|
+
local lock="$VELA_ROOT/state/.ports.lock"
|
|
50
|
+
mkdir -p "$VELA_ROOT/state"
|
|
51
|
+
[ -f "$ports_file" ] || echo '{}' > "$ports_file"
|
|
52
|
+
touch "$lock"
|
|
53
|
+
|
|
54
|
+
(
|
|
55
|
+
flock 9
|
|
56
|
+
local existing
|
|
57
|
+
existing=$(jq -er --arg i "$instance" '.[$i] // empty' "$ports_file")
|
|
58
|
+
if [ -n "$existing" ]; then
|
|
59
|
+
printf '%s' "$existing"
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
local offset=0 web pb
|
|
64
|
+
while [ "$offset" -lt "$VELA_PORT_RANGE" ]; do
|
|
65
|
+
web=$((VELA_WEB_PORT_BASE + offset))
|
|
66
|
+
pb=$((VELA_PB_PORT_BASE + offset))
|
|
67
|
+
if ! jq -e --argjson w "$web" 'to_entries | any(.value.web == $w)' "$ports_file" >/dev/null \
|
|
68
|
+
&& ! port_in_use "$web" && ! port_in_use "$pb"; then
|
|
69
|
+
local tmp
|
|
70
|
+
tmp=$(mktemp "$VELA_ROOT/state/.ports.XXXXXX")
|
|
71
|
+
jq -S --arg i "$instance" --argjson w "$web" --argjson p "$pb" \
|
|
72
|
+
'.[$i] = {web: $w, pb: $p}' "$ports_file" > "$tmp"
|
|
73
|
+
mv -f "$tmp" "$ports_file"
|
|
74
|
+
jq -c -n --argjson w "$web" --argjson p "$pb" '{web: $w, pb: $p}'
|
|
75
|
+
exit 0
|
|
76
|
+
fi
|
|
77
|
+
offset=$((offset + 1))
|
|
78
|
+
done
|
|
79
|
+
die "no free port pair in range"
|
|
80
|
+
) 9>"$lock"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
release_ports() {
|
|
84
|
+
local instance=$1
|
|
85
|
+
local ports_file="$VELA_ROOT/state/ports.json"
|
|
86
|
+
local lock="$VELA_ROOT/state/.ports.lock"
|
|
87
|
+
[ -f "$ports_file" ] || return 0
|
|
88
|
+
(
|
|
89
|
+
flock 9
|
|
90
|
+
local tmp
|
|
91
|
+
tmp=$(mktemp "$VELA_ROOT/state/.ports.XXXXXX")
|
|
92
|
+
jq -S --arg i "$instance" 'del(.[$i])' "$ports_file" > "$tmp"
|
|
93
|
+
mv -f "$tmp" "$ports_file"
|
|
94
|
+
) 9>"$lock"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
port_in_use() {
|
|
98
|
+
local port=$1
|
|
99
|
+
if command -v ss >/dev/null 2>&1; then
|
|
100
|
+
ss -ltn "sport = :$port" 2>/dev/null | tail -n +2 | grep -q . && return 0
|
|
101
|
+
fi
|
|
102
|
+
return 1
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
unit_web() { printf 'vela-web@%s.service' "$1"; }
|
|
106
|
+
unit_pb() { printf 'vela-pb@%s.service' "$1"; }
|
|
107
|
+
|
|
108
|
+
unit_active() { systemctl is-active --quiet "$1"; }
|
|
109
|
+
|
|
110
|
+
# Poll an HTTP endpoint until it answers with a non-5xx status.
|
|
111
|
+
wait_for_http() {
|
|
112
|
+
local url=$1 attempts=${2:-60} delay=${3:-0.5} code
|
|
113
|
+
local i=0
|
|
114
|
+
while [ "$i" -lt "$attempts" ]; do
|
|
115
|
+
code=$(curl -fsS -o /dev/null -w '%{http_code}' --max-time 5 "$url" 2>/dev/null || echo 000)
|
|
116
|
+
case "$code" in
|
|
117
|
+
2*|3*|4*) return 0 ;;
|
|
118
|
+
esac
|
|
119
|
+
i=$((i + 1))
|
|
120
|
+
sleep "$delay"
|
|
121
|
+
done
|
|
122
|
+
return 1
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# How many migrations the current release has that the target release does not.
|
|
126
|
+
# PocketBase reverts by count, so this is what `migrate down` needs.
|
|
127
|
+
migrations_ahead() {
|
|
128
|
+
local current_dir=$1 target_dir=$2 count=0 f
|
|
129
|
+
[ -d "$current_dir" ] || { printf '0'; return 0; }
|
|
130
|
+
for f in "$current_dir"/*.js; do
|
|
131
|
+
[ -f "$f" ] || continue
|
|
132
|
+
if [ ! -f "$target_dir/$(basename "$f")" ]; then count=$((count + 1)); fi
|
|
133
|
+
done
|
|
134
|
+
printf '%s' "$count"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
emit_result() { printf 'VELA_RESULT %s\n' "$(jq -c -n "$@")"; }
|
|
138
|
+
|
|
139
|
+
# Read one value out of a vela-managed env file. `vela env` writes values with
|
|
140
|
+
# systemd's quoting, whose escapes (\\, \", \n, \t) are also JSON's, so jq
|
|
141
|
+
# decodes them exactly.
|
|
142
|
+
env_file_get() {
|
|
143
|
+
local file=$1 key=$2 raw
|
|
144
|
+
[ -f "$file" ] || return 1
|
|
145
|
+
raw=$(sed -n "s/^[[:space:]]*${key}=//p" "$file" | head -n1)
|
|
146
|
+
[ -n "$raw" ] || return 1
|
|
147
|
+
case "$raw" in
|
|
148
|
+
'"'*) printf '%s' "$raw" | jq -r . ;;
|
|
149
|
+
*) printf '%s' "$raw" ;;
|
|
150
|
+
esac
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Append a value in the same quoted form `vela env` uses, so it round-trips.
|
|
154
|
+
env_file_append() {
|
|
155
|
+
local file=$1 key=$2 value=$3
|
|
156
|
+
[ -f "$file" ] || : > "$file"
|
|
157
|
+
if [ -s "$file" ] && [ -n "$(tail -c1 "$file")" ]; then printf '\n' >> "$file"; fi
|
|
158
|
+
printf '%s=%s\n' "$key" "$(jq -rn --arg v "$value" '$v | @json')" >> "$file"
|
|
159
|
+
chmod 0600 "$file"
|
|
160
|
+
chown root:root "$file"
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# 24 bytes of hex - no shell metacharacters, nothing to escape anywhere it goes.
|
|
164
|
+
random_secret() {
|
|
165
|
+
head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n'
|
|
166
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Ensure a given PocketBase version is available on this server and print its
|
|
4
|
+
# path. Versions live side by side so two apps can pin different ones and a
|
|
5
|
+
# rollback never has to downgrade a shared binary.
|
|
6
|
+
#
|
|
7
|
+
# usage: pocketbase.sh <version>
|
|
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
|
+
VERSION=${1:-}
|
|
15
|
+
[ -n "$VERSION" ] || die "pocketbase.sh needs a version"
|
|
16
|
+
|
|
17
|
+
DEST="$VELA_ROOT/runtime/pocketbase/$VERSION"
|
|
18
|
+
BIN="$DEST/pocketbase"
|
|
19
|
+
|
|
20
|
+
if [ -x "$BIN" ]; then
|
|
21
|
+
printf '%s\n' "$BIN"
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
case "$(uname -m)" in
|
|
26
|
+
x86_64) NPM_PKG=pocketbase-server-linux-x64 ;;
|
|
27
|
+
aarch64|arm64) NPM_PKG=pocketbase-server-linux-arm64 ;;
|
|
28
|
+
*) die "unsupported architecture: $(uname -m)" ;;
|
|
29
|
+
esac
|
|
30
|
+
|
|
31
|
+
# The same npm package the CLI runs locally, so the server and a developer's
|
|
32
|
+
# machine are always on the identical PocketBase build.
|
|
33
|
+
tmp=$(mktemp -d)
|
|
34
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
35
|
+
(
|
|
36
|
+
cd "$tmp"
|
|
37
|
+
npm init -y >/dev/null 2>&1
|
|
38
|
+
npm install --no-audit --no-fund --loglevel=error "$NPM_PKG@$VERSION" >/dev/null
|
|
39
|
+
)
|
|
40
|
+
[ -f "$tmp/node_modules/$NPM_PKG/bin/pocketbase" ] \
|
|
41
|
+
|| die "PocketBase $VERSION could not be installed from $NPM_PKG"
|
|
42
|
+
|
|
43
|
+
mkdir -p "$DEST"
|
|
44
|
+
install -m 0755 "$tmp/node_modules/$NPM_PKG/bin/pocketbase" "$BIN"
|
|
45
|
+
printf '%s\n' "$BIN"
|