The script defaulted to menu-admin:latest and refused to run on monoco, where the image is registry.tanshu.com/menu-admin:<tag>. It now reads the image off the container the app is already running, deriving the container name the way the playbook does, and only asks for an explicit image when there is no container to borrow one from. The failure message names the exact command either way.
132 lines
4.2 KiB
Bash
Executable File
132 lines
4.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Writes the one file that never goes in git: the admin password hash and the
|
|
# session secret. Run this on the server, once per Site.
|
|
#
|
|
# scripts/provision-site-env.sh /var/lib/menu-app.hopsngrains.com
|
|
#
|
|
# The hash is produced by the app image itself, so the server needs no Node and
|
|
# the password is never visible in the process list.
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
usage: provision-site-env.sh <data-root> [image] [--force]
|
|
|
|
<data-root> directory the app mounts as MENU_ROOT, e.g.
|
|
/var/lib/menu-app.hopsngrains.com
|
|
[image] app image to borrow Node from (default menu-admin:latest)
|
|
--force overwrite an existing site.env
|
|
USAGE
|
|
}
|
|
|
|
DATA_ROOT="${1:-}"
|
|
if [[ -z "$DATA_ROOT" || "$DATA_ROOT" == "-h" || "$DATA_ROOT" == "--help" ]]; then
|
|
usage
|
|
exit 1
|
|
fi
|
|
shift || true
|
|
|
|
IMAGE=""
|
|
FORCE=0
|
|
for argument in "$@"; do
|
|
case "$argument" in
|
|
--force) FORCE=1 ;;
|
|
*) IMAGE="$argument" ;;
|
|
esac
|
|
done
|
|
|
|
ENV_FILE="$DATA_ROOT/site.env"
|
|
|
|
if [[ -f "$ENV_FILE" && "$FORCE" -ne 1 ]]; then
|
|
echo "$ENV_FILE already exists; pass --force to replace it" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
echo "docker is required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# The image is only needed to borrow node for hashing, so take the one the app
|
|
# is already running rather than guessing a name. The playbook names the
|
|
# container menu-app.<domain> for the web address menu.<domain>.
|
|
if [[ -z "$IMAGE" ]]; then
|
|
for candidate in "menu-app.${DATA_ROOT##*/menu.}" "menu-app.${DATA_ROOT##*/}"; do
|
|
IMAGE="$(docker inspect --format '{{.Config.Image}}' "$candidate" 2>/dev/null || true)"
|
|
[[ -n "$IMAGE" ]] && break
|
|
done
|
|
fi
|
|
|
|
if [[ -z "$IMAGE" ]]; then
|
|
echo "could not tell which image to use: no running app container to borrow one from." >&2
|
|
echo "Pass it explicitly: $0 $DATA_ROOT registry.tanshu.com/menu-admin:<tag>" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then
|
|
echo "image $IMAGE is not present on this host." >&2
|
|
echo "Pass the one the app runs: $0 $DATA_ROOT registry.tanshu.com/menu-admin:<tag>" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "hashing with the app's image: $IMAGE"
|
|
|
|
read -r -s -p "Admin password: " PASSWORD
|
|
echo
|
|
read -r -s -p "Repeat password: " REPEAT
|
|
echo
|
|
|
|
if [[ -z "$PASSWORD" ]]; then
|
|
echo "the password cannot be empty" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$PASSWORD" != "$REPEAT" ]]; then
|
|
echo "the passwords do not match" >&2
|
|
exit 1
|
|
fi
|
|
if [[ ${#PASSWORD} -lt 10 ]]; then
|
|
echo "use at least 10 characters" >&2
|
|
exit 1
|
|
fi
|
|
|
|
HASH="$(printf '%s' "$PASSWORD" | docker run --rm -i --entrypoint node "$IMAGE" \
|
|
--input-type=module \
|
|
-e "let s='';process.stdin.setEncoding('utf8');process.stdin.on('data',c=>s+=c);process.stdin.on('end',async()=>{const {pathToFileURL}=await import('node:url');const m=await import(pathToFileURL('/app/src/auth.ts').href);process.stdout.write(await m.hashPassword(s))})")"
|
|
|
|
SECRET="$(docker run --rm --entrypoint node "$IMAGE" --input-type=module \
|
|
-e "import {randomBytes} from 'node:crypto';process.stdout.write(randomBytes(32).toString('base64url'))")"
|
|
|
|
if [[ -z "$HASH" || -z "$SECRET" ]]; then
|
|
echo "the image failed to produce the hash or the secret" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# The file is written by a container running as the app's own uid, not by the
|
|
# SSH user: the data root belongs to the app user (0750), so the SSH user
|
|
# usually cannot write into it, and a file they did write would be unreadable by
|
|
# the app. This way the ownership is right the first time and there is no
|
|
# "re-run the playbook afterwards" step.
|
|
if [[ ! -d "$DATA_ROOT" ]]; then
|
|
echo "$DATA_ROOT does not exist; run the menu-admin playbook first" >&2
|
|
exit 1
|
|
fi
|
|
|
|
APP_UID="${APP_UID:-1000}"
|
|
APP_GID="${APP_GID:-1000}"
|
|
|
|
if ! printf 'ADMIN_PASSWORD_HASH=%s\nSESSION_SECRET=%s\n' "$HASH" "$SECRET" | docker run --rm -i \
|
|
--user "$APP_UID:$APP_GID" -v "$DATA_ROOT:/data" --entrypoint sh "$IMAGE" \
|
|
-c 'umask 077 && cat > /data/site.env'; then
|
|
echo "could not write $ENV_FILE as $APP_UID:$APP_GID" >&2
|
|
exit 1
|
|
fi
|
|
|
|
unset PASSWORD REPEAT HASH SECRET
|
|
|
|
docker run --rm --user "$APP_UID:$APP_GID" -v "$DATA_ROOT:/data" --entrypoint sh "$IMAGE" \
|
|
-c 'ls -l /data/site.env'
|
|
|
|
echo "wrote $ENV_FILE, owned by $APP_UID:$APP_GID"
|
|
echo "restart the app container to pick it up; changing the password later means running this again with --force"
|