254 lines
9.8 KiB
Bash
Executable File
254 lines
9.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Verify canonical local/server installation manuals and their base+override Compose paths.
|
|
set -euo pipefail
|
|
|
|
root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
mode="${1:-}"
|
|
|
|
trim() {
|
|
local value="$1"
|
|
value="${value#"${value%%[![:space:]]*}"}"
|
|
value="${value%"${value##*[![:space:]]}"}"
|
|
printf '%s' "$value"
|
|
}
|
|
|
|
is_safe_absolute_path() {
|
|
local value="$1" segment
|
|
local -a segments
|
|
[[ "$value" == /* && "$value" != *//* ]] || return 1
|
|
IFS=/ read -r -a segments <<<"$value"
|
|
for segment in "${segments[@]}"; do
|
|
[[ "$segment" != . && "$segment" != .. ]] || return 1
|
|
done
|
|
}
|
|
|
|
verify_path_variable_values() {
|
|
local source="$1" line trimmed name value
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
trimmed="$(trim "$line")"
|
|
if [[ "$trimmed" == *=* || "$trimmed" == *:* ]]; then
|
|
name="$(trim "${trimmed%%[=:]*}")"
|
|
value="$(trim "${trimmed#"$name"}")"
|
|
value="$(trim "${value#[:=]}")"
|
|
if [[ "$name" =~ ^[A-Za-z_][A-Za-z0-9_]*_(FILE|SOURCE)$ ]]; then
|
|
value="$(trim "${value%%#*}")"
|
|
value="${value#\"}"; value="${value%\"}"
|
|
value="${value#\'}"; value="${value%\'}"
|
|
if [[ -n "$value" ]] && ! is_safe_absolute_path "$value"; then
|
|
echo "unsafe path value for $name in $source" >&2
|
|
return 1
|
|
fi
|
|
fi
|
|
fi
|
|
done <"$source"
|
|
}
|
|
|
|
verify_manual() {
|
|
local profile="$1" manual
|
|
manual="$root/docs/install/$profile-workspace-registry.md"
|
|
local -a headings
|
|
if [[ "$profile" == local ]]; then
|
|
headings=(
|
|
"Prerequisites"
|
|
"Git remote: SSH and HTTPS"
|
|
"Shared Git values, local bindings, and secret files"
|
|
"Direct PostgreSQL, REST, and SSH tunnel bindings"
|
|
"Bootstrap, first pull, and diagnostics"
|
|
"Publish, update, backup, outage recovery, and rollback"
|
|
"Troubleshooting"
|
|
)
|
|
else
|
|
headings=(
|
|
"Service account, storage, and firewall"
|
|
"Gitea and remote Git setup"
|
|
"Git credentials, CA, SSH key, and known-hosts mounts"
|
|
"Shared Git values, local bindings, and secret files"
|
|
"Direct PostgreSQL, REST, and SSH tunnel bindings"
|
|
"Same-origin reverse proxy, bootstrap, and health"
|
|
"Pull, publish, upgrade, backup, and recovery"
|
|
"Troubleshooting and snapshot rollback"
|
|
)
|
|
fi
|
|
for heading in "${headings[@]}"; do
|
|
grep -Fqx "## $heading" "$manual" || {
|
|
echo "missing required heading in $profile manual: $heading" >&2
|
|
return 1
|
|
}
|
|
done
|
|
for expected in \
|
|
'export THT_SOURCE_ROOT=/absolute/path/to/ThothII' \
|
|
'--env-file "$THT_OPERATOR_ENV"' \
|
|
"-f \"\$THT_SOURCE_ROOT/compose.yaml\" -f \"\$THT_SOURCE_ROOT/deploy/compose.$profile.yaml\"" \
|
|
'"$THT_SOURCE_ROOT/scripts/generate-connector-secrets-override.sh"'; do
|
|
grep -Fq -- "$expected" "$manual" || {
|
|
echo "$profile manual lacks canonical operator step: $expected" >&2
|
|
return 1
|
|
}
|
|
done
|
|
if rg -n 'local-compose\.workspace-registry|server-compose\.workspace-registry|connector-secrets\.workspace-registry|docker compose' "$manual"; then
|
|
echo "$profile manual documents a superseded or bypassed Compose path" >&2
|
|
return 1
|
|
fi
|
|
verify_path_variable_values "$manual"
|
|
echo "$profile manual canonical base+override references passed"
|
|
}
|
|
|
|
write_private() {
|
|
local path="$1" value="$2"
|
|
printf '%s\n' "$value" >"$path"
|
|
chmod 0600 "$path"
|
|
}
|
|
|
|
verify_compose_fixtures() {
|
|
local fixture connector_override profile rendered
|
|
fixture="$(mktemp -d "${TMPDIR%/}/thoth-install-fixtures.XXXXXX")"
|
|
trap 'rm -rf "$fixture"' RETURN
|
|
mkdir -p "$fixture/data" "$fixture/pi-state" "$fixture/workspace-registry"
|
|
|
|
write_private "$fixture/pi-auth.json" '{"zai":{"type":"api_key","key":"fixture-native-auth-key"}}'
|
|
write_private "$fixture/thothii.secrets" 'THT_MODEL_API_KEY=fixture-model-api-key'
|
|
write_private "$fixture/git-ssh-key" 'fixture-git-ssh-key'
|
|
write_private "$fixture/git-known-hosts" 'fixture-git-known-hosts'
|
|
write_private "$fixture/dwh-password" 'fixture-dwh-password'
|
|
write_private "$fixture/vector-api-key" 'fixture-vector-api-key'
|
|
write_private "$fixture/session-runtime-password" 'fixture-session-runtime-password'
|
|
write_private "$fixture/session-migrator-password" 'fixture-session-migrator-password'
|
|
write_private "$fixture/session-ca.pem" 'fixture-session-ca'
|
|
cp "$root/deploy/workspaces/server-sessions.yaml.example" "$fixture/server-sessions.yaml"
|
|
|
|
printf '%s\n' \
|
|
'THT_WS_NORTH_STAR_RESEARCH_DWH_TRANSPORT=postgres_direct' \
|
|
'THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_FILE=/run/secrets/north-star-research-dwh-password' \
|
|
'THT_WS_NORTH_STAR_RESEARCH_VECTOR_TRANSPORT=rest_api' \
|
|
'THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_FILE=/run/secrets/north-star-research-vector-api-key' \
|
|
>"$fixture/workspace-bindings.env"
|
|
|
|
printf '%s\n' \
|
|
'THT_WORKSPACE_GIT_REMOTE=ssh://git@git.example.invalid/platform/thoth-workspaces.git' \
|
|
"PI_AUTH_FILE=$fixture/pi-auth.json" \
|
|
"THT_SECRETS_FILE=$fixture/thothii.secrets" \
|
|
"THT_WORKSPACE_BINDINGS_ENV_FILE=$fixture/workspace-bindings.env" \
|
|
"THT_WORKSPACE_GIT_SSH_KEY_FILE=$fixture/git-ssh-key" \
|
|
"THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=$fixture/git-known-hosts" \
|
|
"THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE=$fixture/dwh-password" \
|
|
"THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_SOURCE=$fixture/vector-api-key" \
|
|
"THT_DATA_ROOT=$fixture/data" \
|
|
"THT_PI_STATE_ROOT=$fixture/pi-state" \
|
|
"THT_WORKSPACE_REGISTRY_ROOT=$fixture/workspace-registry" \
|
|
"THT_SERVER_WORKSPACE_CONFIG=$fixture/server-sessions.yaml" \
|
|
'THT_SESSION_DB_HOST=sessions.example.invalid' \
|
|
'THT_SESSION_DB_NAME=thoth_sessions' \
|
|
'THT_SESSION_RUNTIME_USER=thoth_sessions_app' \
|
|
'THT_SESSION_MIGRATOR_USER=thoth_sessions_migrate' \
|
|
"THT_SESSION_RUNTIME_PASSWORD_SOURCE=$fixture/session-runtime-password" \
|
|
"THT_SESSION_MIGRATOR_PASSWORD_SOURCE=$fixture/session-migrator-password" \
|
|
"THT_SESSION_CA_SOURCE=$fixture/session-ca.pem" \
|
|
>"$fixture/operator.env"
|
|
|
|
connector_override="$fixture/connector-secrets.local.yaml"
|
|
"$root/scripts/generate-connector-secrets-override.sh" \
|
|
--bindings-env "$fixture/workspace-bindings.env" \
|
|
--operator-env "$fixture/operator.env" \
|
|
--output "$connector_override" >/dev/null
|
|
|
|
for profile in local server; do
|
|
rendered="$fixture/$profile.json"
|
|
files=(
|
|
-f "$root/compose.yaml"
|
|
-f "$root/deploy/compose.$profile.yaml"
|
|
)
|
|
if [[ "$profile" == server ]]; then
|
|
files+=(-f "$root/deploy/compose.session-server.yaml.example")
|
|
fi
|
|
files+=(
|
|
-f "$root/deploy/compose.git-ssh.yaml"
|
|
-f "$connector_override"
|
|
)
|
|
"$root/scripts/compose-with-preflight.sh" --env-file "$fixture/operator.env" \
|
|
"${files[@]}" config --format json >"$rendered"
|
|
|
|
node - "$rendered" "$profile" <<'NODE'
|
|
const fs = require("fs");
|
|
const [path, profile] = process.argv.slice(2);
|
|
const config = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
if (Object.keys(config.services).sort().join(",") !== "core,frontend") {
|
|
throw new Error(profile + ": mandatory stack must be exactly core,frontend");
|
|
}
|
|
const core = config.services.core;
|
|
for (const target of [
|
|
"/home/thoth/.pi/agent/auth.json",
|
|
"/home/thoth/.pi/agent/models.json",
|
|
"/home/thoth/.pi/agent/settings.json",
|
|
]) {
|
|
if (!(core.volumes || []).some((mount) => mount.target === target && mount.read_only)) {
|
|
throw new Error(profile + ": missing read-only Pi mount " + target);
|
|
}
|
|
}
|
|
for (const [name, value] of Object.entries({
|
|
THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_FILE: "/run/secrets/north-star-research-dwh-password",
|
|
THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_FILE: "/run/secrets/north-star-research-vector-api-key",
|
|
})) {
|
|
if (core.environment?.[name] !== value) throw new Error(profile + ": missing binding " + name);
|
|
}
|
|
const secretTargets = new Set((core.secrets || []).map((secret) => secret.target));
|
|
for (const target of [
|
|
"thothii.secrets",
|
|
"north-star-research-dwh-password",
|
|
"north-star-research-vector-api-key",
|
|
]) {
|
|
if (!secretTargets.has(target)) throw new Error(profile + ": missing secret target " + target);
|
|
}
|
|
if (profile === "server") {
|
|
for (const target of ["session_runtime_password", "session_ca.pem"]) {
|
|
if (!secretTargets.has(target)) throw new Error("server: missing session secret " + target);
|
|
}
|
|
}
|
|
if ((config.services.frontend.secrets || []).length !== 0) {
|
|
throw new Error(profile + ": frontend received a runtime secret");
|
|
}
|
|
const rendered = JSON.stringify(config);
|
|
for (const value of [
|
|
"fixture-native-auth-key", "fixture-model-api-key", "fixture-git-ssh-key",
|
|
"fixture-git-known-hosts", "fixture-dwh-password", "fixture-vector-api-key",
|
|
"fixture-session-runtime-password", "fixture-session-migrator-password", "fixture-session-ca",
|
|
]) {
|
|
if (rendered.includes(value)) throw new Error(profile + ": rendered Compose leaked " + value);
|
|
}
|
|
NODE
|
|
echo "canonical $profile base+override fixture passed"
|
|
done
|
|
|
|
printf 'THT_WS_EXAMPLE_DWH_PASSWORD_SOURCE=relative/secret\n' >"$fixture/unsafe.env"
|
|
if verify_path_variable_values "$fixture/unsafe.env" >/dev/null 2>&1; then
|
|
echo "relative secret-source fixture was accepted" >&2
|
|
return 1
|
|
fi
|
|
echo "relative secret-source fixture rejected passed"
|
|
}
|
|
|
|
case "$mode" in
|
|
--fixtures-only)
|
|
[[ $# -eq 1 ]] || { echo "usage: $0 --fixtures-only" >&2; exit 2; }
|
|
verify_manual local
|
|
verify_manual server
|
|
verify_compose_fixtures
|
|
;;
|
|
--profile)
|
|
profile="${2:-}"
|
|
[[ $# -eq 2 && "$profile" =~ ^(local|server)$ ]] \
|
|
|| { echo "usage: $0 --profile {local|server}" >&2; exit 2; }
|
|
verify_manual "$profile"
|
|
verify_compose_fixtures
|
|
echo "== Run isolated workspace-registry bootstrap and recovery smoke =="
|
|
(
|
|
cd "$root"
|
|
env -u WORKSPACE_GIT_REMOTE ./scripts/workspace-registry-smoke.sh
|
|
)
|
|
echo "$profile installation documentation verification passed"
|
|
;;
|
|
*)
|
|
echo "usage: $0 --fixtures-only | --profile {local|server}" >&2
|
|
exit 2
|
|
;;
|
|
esac
|