Restore the installer functions dropped by the Caddy commit

ed4a6f3 rewrote the middle of install-wg-ui.sh and deleted seven function
definitions while leaving every call site in place. The runtime only
stopped at the first one, so `update wg-ui` died with:

  install-wg-ui.sh: line 202: clone_or_update: command not found

Fixing that line alone would have moved the failure to
install_dependencies three lines down, and with install_systemd_unit /
install_openrc_unit gone the script could never have written a service
unit again. Restore all seven from 0f94004, keeping the Caddy work that
landed in the same commit:

- clone_or_update, install_dependencies, ensure_env_file,
  setup_wireguard, install_wg_quick_helper: byte-identical to 0f94004.
- install_systemd_unit / install_openrc_unit: identical, except wg-ui now
  starts after docker, since Caddy ingress reloads go through docker exec.

Add install-wg-ui.test.ts so this class of bug cannot come back silently:
it asserts the script parses, that every invoked function has a
definition (indented calls included, which is how clone_or_update hid),
and that the update flow runs in order. Checked against the pre-fix
script, the definition test fails with exactly the seven missing names.
This commit is contained in:
2026-10-21 14:41:59 +01:00
parent cbd5451ab7
commit d9f5367270
2 changed files with 228 additions and 0 deletions
+126
View File
@@ -122,6 +122,63 @@ setup_lib_dirs() {
chmod 755 "$WGUI_LIB_DIR" chmod 755 "$WGUI_LIB_DIR"
} }
clone_or_update() {
mkdir -p "$(dirname "$INSTALL_DIR")"
if [ -d "$INSTALL_DIR/.git" ]; then
UPDATE=true
log "updating existing install at $INSTALL_DIR ..."
git -C "$INSTALL_DIR" fetch --depth 1 origin "$BRANCH"
local db_backup="$INSTALL_DIR/db/.wgui-pre-update.db"
if [ -f "$INSTALL_DIR/db/wgui" ]; then
cp -a "$INSTALL_DIR/db/wgui" "$db_backup"
fi
git -C "$INSTALL_DIR" reset --hard "origin/$BRANCH"
if [ -f "$db_backup" ]; then
mv -f "$db_backup" "$INSTALL_DIR/db/wgui"
fi
else
log "cloning wg-ui ($REPO_URL, branch $BRANCH) ..."
git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$INSTALL_DIR"
fi
}
install_dependencies() {
log "installing app dependencies with bun ..."
cd "$INSTALL_DIR"
"$BUN_BIN" install
"$BUN_BIN" bun-sqlite schema
}
ensure_env_file() {
if [ ! -f "$INSTALL_DIR/.env" ]; then
log "generating $INSTALL_DIR/.env with fresh encryption secrets ..."
{
echo "NODE_ENV=${NODE_ENV:-production}"
echo "ENCRYPTION_KEY=$(openssl rand -base64 32 | tr -d '\n')"
echo "ENCRYPTION_SALT=$(openssl rand -base64 32 | tr -d '\n')"
echo "DATA_DIR=$INSTALL_DIR/.data"
} > "$INSTALL_DIR/.env"
chmod 600 "$INSTALL_DIR/.env"
fi
}
setup_wireguard() {
local setup_script="$INSTALL_DIR/src/scripts/setup-wireguard.sh"
if [ -x "$setup_script" ] && [ "${SKIP_WIREGUARD_SETUP:-0}" != "1" ]; then
log "running $(basename "$setup_script") to install wireguard on this host ..."
"$setup_script"
fi
}
install_wg_quick_helper() {
local helper_src="$INSTALL_DIR/src/scripts/wg-quick-manage.sh"
if [ ! -f "$helper_src" ]; then
fail "missing $helper_src — cannot install the wg-quick helper"
fi
log "installing wg-quick helper to $WG_QUICK_HELPER ..."
install -m 755 -o root -g root "$helper_src" "$WG_QUICK_HELPER"
}
install_caddy_stack() { install_caddy_stack() {
local install_script="$INSTALL_DIR/src/scripts/install-caddy.sh" local install_script="$INSTALL_DIR/src/scripts/install-caddy.sh"
@@ -190,6 +247,75 @@ print_firewall_instructions() {
log " and review all firewall rules under /admin/setup -> Firewall" log " and review all firewall rules under /admin/setup -> Firewall"
} }
install_systemd_unit() {
local unit="/etc/systemd/system/$SERVICE_NAME.service"
log "writing systemd unit $unit ..."
cat > "$unit" <<EOF
[Unit]
Description=Wireguard UI
After=network-online.target docker.service
Wants=network-online.target
[Service]
Type=simple
User=root
Group=root
WorkingDirectory=$INSTALL_DIR
Environment=NODE_ENV=production
ExecStart=$BUN_BIN src/server.ts
Restart=on-failure
RestartSec=5
TimeoutStartSec=300
PrivateTmp=true
UMask=0077
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
log "enabling and starting $SERVICE_NAME ..."
systemctl enable --now "$SERVICE_NAME.service"
if [ "$UPDATE" = true ] && [ -z "${SKIP_SERVICE_RESTART:-}" ]; then
log "restarting $SERVICE_NAME to load updated code ..."
systemctl restart "$SERVICE_NAME.service"
fi
}
install_openrc_unit() {
local init_script="/etc/init.d/$SERVICE_NAME"
log "writing openrc init script $init_script ..."
cat > "$init_script" <<EOF
#!/sbin/openrc-run
name="$SERVICE_NAME"
description="Wireguard UI"
command="$BUN_BIN"
command_args="src/server.ts"
command_user="root"
directory="$INSTALL_DIR"
output_log="/var/log/$SERVICE_NAME.log"
error_log="/var/log/$SERVICE_NAME.log"
pidfile="/run/\${RC_SVCNAME}.pid"
export NODE_ENV="production"
start_pre() {
umask 077
}
depend() {
need net
after docker
}
EOF
chmod +x "$init_script"
log "adding $SERVICE_NAME to default runlevel and starting ..."
rc-update add "$SERVICE_NAME" default
if [ "$UPDATE" = true ] && [ -z "${SKIP_SERVICE_RESTART:-}" ]; then
rc-service "$SERVICE_NAME" restart
else
rc-service "$SERVICE_NAME" start
fi
}
install_deps install_deps
install_bun install_bun
setup_lib_dirs setup_lib_dirs
+102
View File
@@ -0,0 +1,102 @@
import { describe, expect, test } from "bun:test";
import fs from "node:fs";
import path from "node:path";
import { execFileSync } from "node:child_process";
const SCRIPT_PATH = path.join(import.meta.dir, "install-wg-ui.sh");
const SCRIPT = fs.readFileSync(SCRIPT_PATH, "utf-8");
const BASH_KEYWORDS = new Set([
"fi",
"else",
"then",
"do",
"done",
"esac",
"EOF",
"in",
"{",
"}",
]);
/**
* Functions declared with `name() {`. `install_openrc_unit` embeds an
* OpenRC init script in a heredoc, so `start_pre` / `depend` show up here
* too — that is fine, they are only ever compared against definitions.
*/
function grabDefinedFunctions(): Set<string> {
const defined = new Set<string>();
for (const match of SCRIPT.matchAll(/^([A-Za-z_][A-Za-z0-9_]*)\(\) *\{/gm)) {
const name = match[1];
if (name) {
defined.add(name);
}
}
return defined;
}
/**
* A bare word on its own line is a function call — the installer always
* invokes helpers that way, both at top level and from inside another
* function. Catching the indented ones too is what surfaces a call whose
* definition was deleted.
*/
function grabInvocations(): string[] {
const invocations: string[] = [];
for (const match of SCRIPT.matchAll(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*$/gm)) {
const name = match[1];
if (!name || BASH_KEYWORDS.has(name)) {
continue;
}
invocations.push(name);
}
return invocations;
}
describe("install-wg-ui.sh", () => {
test("parses as valid bash", () => {
expect(() =>
execFileSync("bash", ["-n", SCRIPT_PATH], { stdio: "pipe" }),
).not.toThrow();
});
test("every invoked function has a definition", () => {
const defined = grabDefinedFunctions();
const undefined_invocations = grabInvocations().filter(
(name) => !defined.has(name),
);
expect(undefined_invocations).toEqual([]);
});
test("runs the update flow in the documented order", () => {
const invocations = grabInvocations();
const expected_order = [
"install_deps",
"install_bun",
"setup_lib_dirs",
"clone_or_update",
"install_caddy_stack",
"install_dependencies",
"ensure_env_file",
"setup_wireguard",
"install_wg_quick_helper",
];
let cursor = -1;
for (const name of expected_order) {
const index = invocations.indexOf(name);
expect(index).toBeGreaterThan(cursor);
cursor = index;
}
});
});