testsAndMisc/linux_configuration/features/lib/syncyomi_stack.sh
Krzysztof kuhy Rudnicki 924630c029
Some checks are pending
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Run SyncYomi from the local fork build and stop clobbering the Caddy edge
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 14:44:40 +02:00

177 lines
6.7 KiB
Bash
Executable File

#!/bin/bash
# Compose file, container startup and readiness polling for SyncYomi.
#
# Sourced by setup_syncyomi.sh; split out to keep it under the 250-line cap.
# Sourced rather than run, so it inherits the caller's strict mode and
# the helper functions and variables defined above the source line.
# --- Step 3: docker-compose stack (host networking, loopback bind) -----------
write_compose() {
ensure_dir "${DATA_DIR}/config"
# network_mode: host + SyncYomi's host=127.0.0.1 keeps it loopback-only
# (reached by the host-networked Caddy) and dodges this host's default-drop
# nftables FORWARD chain that would kill a bridge-networked container.
umask 077
cat >"${COMPOSE_FILE}" <<EOF
# Generated by setup_syncyomi.sh — do not edit by hand.
services:
syncyomi:
container_name: syncyomi
image: ${SYNCYOMI_IMAGE}
restart: unless-stopped
network_mode: host
environment:
- TZ=${TZ_VALUE}
volumes:
- ./config:/config
EOF
log_ok "Wrote ${COMPOSE_FILE}"
}
# Build the local image the compose file asks for. A local tag exists in no
# registry, so `docker compose up` would fail with a pull error rather than an
# explanation -- and a fork whose changes are never rebuilt is worse than no
# fork at all. Builds whatever is checked out in ${SYNCYOMI_SRC}; never moves
# that working tree. Set SYNCYOMI_REBUILD=1 to rebuild after editing it.
ensure_local_image() {
if [[ ${SYNCYOMI_IMAGE} == */* ]]; then
log_info "Using registry image ${SYNCYOMI_IMAGE}"
return 0
fi
if [[ ${SYNCYOMI_REBUILD:-0} != "1" ]] && docker image inspect "${SYNCYOMI_IMAGE}" >/dev/null 2>&1; then
log_ok "Local image ${SYNCYOMI_IMAGE} already built"
return 0
fi
if [[ ! -d ${SYNCYOMI_SRC}/.git ]]; then
log_info "Cloning ${SYNCYOMI_FORK_URL} into ${SYNCYOMI_SRC}"
git clone "${SYNCYOMI_FORK_URL}" "${SYNCYOMI_SRC}"
fi
local version revision
version="$(git -C "${SYNCYOMI_SRC}" describe --tags --always)"
revision="$(git -C "${SYNCYOMI_SRC}" rev-parse HEAD)"
log_info "Building ${SYNCYOMI_IMAGE} from ${SYNCYOMI_SRC} (${version})"
# BuildKit is required: the Dockerfile reads $BUILDPLATFORM, which the
# legacy builder leaves empty and then rejects as an invalid platform.
DOCKER_BUILDKIT=1 docker build \
-t "${SYNCYOMI_IMAGE}" \
--build-arg VERSION="${version}-kuhy" \
--build-arg REVISION="${revision}" \
--build-arg BUILDTIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
"${SYNCYOMI_SRC}"
log_ok "Built ${SYNCYOMI_IMAGE} from ${version}"
}
start_containers() {
ensure_local_image
log_info "Starting SyncYomi container"
docker compose -f "${COMPOSE_FILE}" up -d
}
# Readiness probe. SyncYomi (v1.1.x) has no public /healthz, but GET
# /api/auth/onboard answers 204 (no user yet) or 403 (a user exists) once the
# API is serving — either means "up". Echoes the HTTP code; returns 0 if ready.
# Usage: syncyomi_ready "http://127.0.0.1:8282"
syncyomi_ready() {
local base="$1" code
code="$(curl -sS -o /dev/null -w '%{http_code}' "${base}/api/auth/onboard" 2>/dev/null || true)"
echo "${code}"
[[ ${code} == "204" || ${code} == "403" ]]
}
# --- Step 4: wait until the local API answers --------------------------------
wait_for_syncyomi() {
log_info "Waiting for SyncYomi to come up on ${SYNCYOMI_LOCAL}"
local i
for ((i = 0; i < 60; i++)); do
if syncyomi_ready "${SYNCYOMI_LOCAL}" >/dev/null; then
log_ok "SyncYomi is up (API answering)"
return 0
fi
sleep 2
done
die "SyncYomi did not become healthy in 120s — check 'docker logs syncyomi'."
}
# Defensive: SyncYomi generates config/config.toml on first run. Ensure it binds
# loopback-only (never 0.0.0.0, which would bypass Caddy/TLS) and serves at root.
ensure_local_config() {
[[ -f ${CONFIG_TOML} ]] || return 0
local changed=0
if grep -qE '^\s*host\s*=' "${CONFIG_TOML}"; then
if ! grep -qE '^\s*host\s*=\s*"127\.0\.0\.1"' "${CONFIG_TOML}"; then
sed -i -E 's|^\s*host\s*=.*|host = "127.0.0.1"|' "${CONFIG_TOML}"
changed=1
fi
fi
# Force an empty baseUrl (dedicated subdomain, not a sub-path).
if grep -qE '^\s*baseUrl\s*=' "${CONFIG_TOML}" &&
! grep -qE '^\s*baseUrl\s*=\s*""' "${CONFIG_TOML}"; then
sed -i -E 's|^\s*baseUrl\s*=.*|baseUrl = ""|' "${CONFIG_TOML}"
changed=1
fi
if [[ ${changed} -eq 1 ]]; then
log_info "Patched ${CONFIG_TOML} (host=127.0.0.1, baseUrl empty) — restarting"
docker compose -f "${COMPOSE_FILE}" restart
wait_for_syncyomi
fi
}
# --- Step 5: create first account + mint API key over the local REST API -----
mint_api_key() {
if [[ -f ${TOKEN_FILE} ]]; then
log_ok "API key already minted ($(<"${TOKEN_FILE}") stored in ${TOKEN_FILE})"
return 0
fi
log_info "Creating SyncYomi account and minting API key"
local jar body http key
jar="$(mktemp)"
# shellcheck disable=SC2064
trap "rm -f '${jar}'" RETURN
# JSON with proper escaping (password may contain +,/,= from base64).
body="$(jq -n --arg u "${SYNCYOMI_USER}" --arg p "${SYNCYOMI_PASSWORD}" \
'{username:$u,password:$p}')"
# Onboard the first user only if none exists yet (GET returns 204 when empty).
http="$(curl -fsS -o /dev/null -w '%{http_code}' "${SYNCYOMI_LOCAL}/api/auth/onboard" 2>/dev/null || true)"
if [[ ${http} == "204" ]]; then
http="$(curl -sS -o /dev/null -w '%{http_code}' \
-H 'Content-Type: application/json' -d "${body}" \
"${SYNCYOMI_LOCAL}/api/auth/onboard" 2>/dev/null || true)"
[[ ${http} =~ ^2 ]] || die "Onboard failed (HTTP ${http}) — check 'docker logs syncyomi'."
log_ok "Created first account '${SYNCYOMI_USER}'"
else
log_warn "An account already exists (onboard HTTP ${http}); trying stored credentials"
fi
# Log in to get a session cookie.
http="$(curl -sS -c "${jar}" -o /dev/null -w '%{http_code}' \
-H 'Content-Type: application/json' -d "${body}" \
"${SYNCYOMI_LOCAL}/api/auth/login" 2>/dev/null || true)"
if [[ ! ${http} =~ ^2 ]]; then
log_warn "Login failed (HTTP ${http}). A different account may already own this server."
log_warn "Create an API key manually at ${SYNCYOMI_LOCAL} → Settings → API Keys."
return 0
fi
# Reuse an existing 'tachiyomisy' key if one exists (idempotent); otherwise
# create one. scopes must be a non-empty array — SyncYomi rejects a null.
key="$(curl -sS -b "${jar}" "${SYNCYOMI_LOCAL}/api/keys" 2>/dev/null |
jq -r '[.[] | select(.name=="tachiyomisy")][0].key // empty')"
if [[ -n ${key} ]]; then
log_ok "Reusing existing 'tachiyomisy' API key"
else
key="$(curl -sS -b "${jar}" \
-H 'Content-Type: application/json' \
-d '{"name":"tachiyomisy","scopes":["read","write"]}' \
"${SYNCYOMI_LOCAL}/api/keys" 2>/dev/null | jq -r '.key // empty')"
[[ -n ${key} ]] || die "API key creation returned no key — check 'docker logs syncyomi'."
fi
umask 077
printf '%s\n' "${key}" >"${TOKEN_FILE}"
chmod 600 "${TOKEN_FILE}"
log_ok "Minted API key and stored it in ${TOKEN_FILE} (0600)"
}