|
|
|
@@ -90,6 +90,7 @@ service_is_allowed() {
|
|
|
|
|
dankclaw:authentik-postgres|dankclaw:authentik-server|\
|
|
|
|
|
dankclaw:authentik-worker|dankclaw:authentik-outpost|dankclaw:authentik-outpost-ldap|dankclaw:anubis|\
|
|
|
|
|
dankclaw:caddy|dankclaw:homepage|dankclaw:seerr|dankclaw:marreta|\
|
|
|
|
|
dankclaw:open-notebook|\
|
|
|
|
|
dankclaw:frank-dragoon|dankclaw:frank-zealot|\
|
|
|
|
|
dankserver:romm-mariadb|dankserver:romm-redis|dankserver:romm|\
|
|
|
|
|
dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|dankserver:calibre-server|dankserver:plex|dankserver:tautulli|\
|
|
|
|
@@ -646,6 +647,103 @@ deploy_open_webui() {
|
|
|
|
|
systemctl --user restart open-webui.service
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# open-notebook is the fleet's podcast generator: one app container (API,
|
|
|
|
|
# worker and web UI under supervisord) in front of a SurrealDB container, with
|
|
|
|
|
# a private bridge between them. One handler owns the pair, because restarting
|
|
|
|
|
# the app without its database yields a UI that answers /health and fails every
|
|
|
|
|
# write; scripts/deploy-plan.py therefore maps every path of the pair here.
|
|
|
|
|
#
|
|
|
|
|
# Credentials follow the marreta convention (per-service file under the deploy
|
|
|
|
|
# user's home, mode 600, seeded from dankclaw/.env) with one difference: they
|
|
|
|
|
# are not optional. An empty OPEN_NOTEBOOK_PASSWORD is "authentication OFF",
|
|
|
|
|
# not "unconfigured" — the API, episode audio included, then answers anyone on
|
|
|
|
|
# the LAN — and an empty SURREAL_PASS lets the database come up with different
|
|
|
|
|
# root credentials than the app will present. So this handler refuses to start
|
|
|
|
|
# either unit until every key carries a value. An existing file is NEVER
|
|
|
|
|
# rewritten: regenerating an operator-set password would desync it from the
|
|
|
|
|
# credentials already stored in the database.
|
|
|
|
|
seed_open_notebook_env() {
|
|
|
|
|
local dest=/home/dank/Containers/open-notebook/env
|
|
|
|
|
local var missing=0
|
|
|
|
|
for var in OPEN_NOTEBOOK_ENCRYPTION_KEY OPEN_NOTEBOOK_PASSWORD SURREAL_PASSWORD SURREAL_PASS; do
|
|
|
|
|
grep -q "^${var}=." "$dest" 2>/dev/null || missing=1
|
|
|
|
|
done
|
|
|
|
|
if [[ "$missing" == 0 ]]; then
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
if [[ ! -e "$dest" ]]; then
|
|
|
|
|
if [[ ! -f dankclaw/.env ]]; then
|
|
|
|
|
echo "open-notebook: neither $dest nor dankclaw/.env exists; put OPEN_NOTEBOOK_ENCRYPTION_KEY, OPEN_NOTEBOOK_PASSWORD and SURREAL_PASSWORD in dankclaw/.env (see dankclaw/.env.example)" >&2
|
|
|
|
|
return 1
|
|
|
|
|
fi
|
|
|
|
|
mkdir -p /home/dank/Containers/open-notebook
|
|
|
|
|
{
|
|
|
|
|
awk -F= '/^OPEN_NOTEBOOK_ENCRYPTION_KEY=/ || /^OPEN_NOTEBOOK_PASSWORD=/ || /^SURREAL_PASSWORD=/ { print }' \
|
|
|
|
|
dankclaw/.env
|
|
|
|
|
# The `surreal` CLI reads SURREAL_PASS from the environment while
|
|
|
|
|
# the app reads SURREAL_PASSWORD. One value, written twice, so the
|
|
|
|
|
# pair can never be seeded with two different root passwords.
|
|
|
|
|
sed -n 's/^SURREAL_PASSWORD=/SURREAL_PASS=/p' dankclaw/.env
|
|
|
|
|
} > "$dest"
|
|
|
|
|
chmod 600 "$dest"
|
|
|
|
|
fi
|
|
|
|
|
for var in OPEN_NOTEBOOK_ENCRYPTION_KEY OPEN_NOTEBOOK_PASSWORD SURREAL_PASSWORD SURREAL_PASS; do
|
|
|
|
|
grep -q "^${var}=." "$dest" \
|
|
|
|
|
|| { echo "open-notebook: $dest carries no value for ${var}; fill it in directly (or in dankclaw/.env before the first seed) — a blank value here means an unauthenticated service or a mismatched database password" >&2; return 1; }
|
|
|
|
|
done
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Both units ship Pull=never: the digest pins in the .container files are the
|
|
|
|
|
# contract, and the generator must never re-resolve a moving tag at start. That
|
|
|
|
|
# makes fetching a pin a precondition the deployer satisfies once per pin
|
|
|
|
|
# change; the existence check keeps every later re-deploy offline-safe.
|
|
|
|
|
ensure_pinned_image() {
|
|
|
|
|
local image="$1"
|
|
|
|
|
if podman image exists "$image"; then
|
|
|
|
|
echo "open-notebook: $image is already in local storage"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
echo "open-notebook: pulling pinned image $image"
|
|
|
|
|
podman pull "$image"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deploy_open_notebook() {
|
|
|
|
|
seed_open_notebook_env || return 1
|
|
|
|
|
|
|
|
|
|
ensure_pinned_image 'docker.io/surrealdb/surrealdb:v2@sha256:e5d7387e3727bbe2f7cd6d869e48cd34777272258572ead37c9656b25f745723' || return 1
|
|
|
|
|
ensure_pinned_image 'docker.io/lfnovo/open_notebook:v1-latest@sha256:54b2025a4a4853c188f74b9b8246eca8c5480e5dd1babfa92db1038ff2f30212' || return 1
|
|
|
|
|
|
|
|
|
|
# install_quadlet copies the sibling open-notebook.network with the app
|
|
|
|
|
# unit. That cp is load-bearing, not tidiness: when a referenced Network=
|
|
|
|
|
# file is absent the v1 generator refuses to produce the container unit at
|
|
|
|
|
# all ("requested Quadlet unit open-notebook.network was not found").
|
|
|
|
|
install_quadlet open-notebook-surrealdb dankclaw/open-notebook/quadlet open-notebook-surrealdb
|
|
|
|
|
install_quadlet open-notebook dankclaw/open-notebook/quadlet open-notebook
|
|
|
|
|
systemctl --user start open-notebook-network.service
|
|
|
|
|
|
|
|
|
|
# Interim episode destination (Gitea issue #7, `human-only`): finished
|
|
|
|
|
# episode audio is bind-mounted out to this host directory. Created here as
|
|
|
|
|
# the deploy user so the container's mapped root writes files dank can
|
|
|
|
|
# read, and so the dependency is explicit rather than podman's default.
|
|
|
|
|
mkdir -p /home/dank/media/podcasts
|
|
|
|
|
|
|
|
|
|
# No `systemctl --user enable`: generated Quadlet units cannot be enabled
|
|
|
|
|
# on this host (see deploy_authentik); boot persistence is the [Install]
|
|
|
|
|
# section inside each .container file. Database first, and both on every
|
|
|
|
|
# run — a stale app left attached to a recreated database is exactly the
|
|
|
|
|
# half-deployed state this handler exists to prevent.
|
|
|
|
|
systemctl --user restart open-notebook-surrealdb.service
|
|
|
|
|
wait_container_healthy open-notebook-surrealdb 90 || return 1
|
|
|
|
|
systemctl --user restart open-notebook.service
|
|
|
|
|
# The gate is the API's own health route: upstream serves `GET /health` on
|
|
|
|
|
# 5055 with {"status":"healthy"} and exempts it from the password
|
|
|
|
|
# middleware, so it stays a real answer once OPEN_NOTEBOOK_PASSWORD is set.
|
|
|
|
|
# Host port 5056, because seerr already holds 127.0.0.1:5055. The UI on
|
|
|
|
|
# 8502 is deliberately not gated: supervisord starts it only after the API
|
|
|
|
|
# answers (the image's wait-for-api.sh), so this gate already implies it.
|
|
|
|
|
wait_for_health open-notebook http://127.0.0.1:5056/health 180
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deploy_comfyui() {
|
|
|
|
|
# Build first: the unit's Image= resolves against local storage, so the
|
|
|
|
|
# image has to exist before the generator starts a container from it.
|
|
|
|
@@ -1573,6 +1671,7 @@ deploy_service() {
|
|
|
|
|
gluetun) deploy_gluetun ;;
|
|
|
|
|
searxng-valkey) deploy_searxng_valkey ;;
|
|
|
|
|
open-webui) deploy_open_webui ;;
|
|
|
|
|
open-notebook) deploy_open_notebook ;;
|
|
|
|
|
pihole) deploy_pihole ;;
|
|
|
|
|
gitea-postgres) deploy_gitea_unit gitea-postgres gitea-postgres ;;
|
|
|
|
|
gitea-server) deploy_gitea_unit gitea-server gitea-server ;;
|
|
|
|
|