fix: close deployment decoupling review
This commit is contained in:
@@ -3,11 +3,11 @@ $ErrorActionPreference = "Continue"
|
||||
$repositoryRoot = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location $repositoryRoot
|
||||
|
||||
& docker compose -f compose.yaml -f deploy/compose.local.yaml build --pull
|
||||
& docker compose --env-file deploy/env/local.env -f compose.yaml -f deploy/compose.local.yaml build --pull
|
||||
$exitCode = $LASTEXITCODE
|
||||
|
||||
if ($exitCode -eq 0) {
|
||||
Write-Output "Next: docker compose -f compose.yaml -f deploy/compose.local.yaml up -d"
|
||||
Write-Output "Next: docker compose --env-file deploy/env/local.env -f compose.yaml -f deploy/compose.local.yaml up -d"
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
@@ -3,11 +3,12 @@ set -u
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
docker compose -f compose.yaml -f deploy/compose.local.yaml build --pull
|
||||
docker compose --env-file deploy/env/local.env \
|
||||
-f compose.yaml -f deploy/compose.local.yaml build --pull
|
||||
status=$?
|
||||
|
||||
if [[ "$status" -eq 0 ]]; then
|
||||
printf '%s\n' 'Next: docker compose -f compose.yaml -f deploy/compose.local.yaml up -d'
|
||||
printf '%s\n' 'Next: docker compose --env-file deploy/env/local.env -f compose.yaml -f deploy/compose.local.yaml up -d'
|
||||
fi
|
||||
|
||||
exit "$status"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# Smoke test del deploy standalone ThothII (core + frontend).
|
||||
# Usa docker-compose.dev.yml (rete propria, porte host).
|
||||
# Prereq: deploy/thothii.env popolato, endpoint esterni configurati e profilo Pi locale.
|
||||
# Prereq: deploy/env/local.env popolato, endpoint esterni e file Pi/segreti configurati.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
DC="docker compose -f docker-compose.dev.yml"
|
||||
DC=(docker compose --env-file deploy/env/local.env -f docker-compose.dev.yml)
|
||||
WS="/app/harness/workspaces/local.yaml"
|
||||
|
||||
echo "== ThothII standalone smoke =="
|
||||
$DC config --quiet
|
||||
"${DC[@]}" config --quiet
|
||||
|
||||
echo "== Build =="
|
||||
$DC build
|
||||
"${DC[@]}" build
|
||||
|
||||
echo "== Up (wait health) =="
|
||||
$DC up -d --wait
|
||||
"${DC[@]}" up -d --wait
|
||||
|
||||
echo "== Core health =="
|
||||
curl -fsS http://localhost:8787/health && echo
|
||||
@@ -24,12 +24,12 @@ echo "== Frontend serve =="
|
||||
curl -fsSI http://localhost:8090/ | head -1
|
||||
|
||||
echo "== Wiring check (config + DWH ping; -c è per-command) =="
|
||||
$DC exec -T core tht config check -c "$WS" || \
|
||||
"${DC[@]}" exec -T core tht config check -c "$WS" || \
|
||||
echo "(config check non verde: verificare .env/ruoli DB)"
|
||||
$DC exec -T core tht db ping -c "$WS" || \
|
||||
"${DC[@]}" exec -T core tht db ping -c "$WS" || \
|
||||
echo "(db ping non verde: verificare ruolo thoth_dwh_reader + rete)"
|
||||
|
||||
echo "== Down =="
|
||||
$DC down
|
||||
"${DC[@]}" down
|
||||
|
||||
echo "OK: smoke standalone passato."
|
||||
|
||||
@@ -101,7 +101,13 @@ done < <(
|
||||
|
||||
{
|
||||
printf '%s\n' '# Generated by scripts/generate-connector-secrets-override.sh; keep this file untracked.'
|
||||
printf '%s\n' 'services:' ' core:' ' secrets:'
|
||||
printf '%s\n' \
|
||||
'services:' \
|
||||
' core:' \
|
||||
' env_file:' \
|
||||
' - path: ${THT_WORKSPACE_BINDINGS_ENV_FILE:?set THT_WORKSPACE_BINDINGS_ENV_FILE}' \
|
||||
' required: true' \
|
||||
' secrets:'
|
||||
for ((index = 0; index < ${#names[@]}; index += 1)); do
|
||||
printf ' - source: connector_secret_%d\n' "$((index + 1))"
|
||||
printf ' target: %s\n' "${targets[index]}"
|
||||
|
||||
@@ -39,6 +39,13 @@ write_bundle() {
|
||||
}
|
||||
write_bundle
|
||||
export THT_SECRETS_FILE="$bundle"
|
||||
printf '%s\n' '{}' >"$secret_dir/pi-auth.json"
|
||||
chmod 0600 "$secret_dir/pi-auth.json"
|
||||
operator_env="$secret_dir/operator.env"
|
||||
printf '%s\n' \
|
||||
'THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git' \
|
||||
"PI_AUTH_FILE=$secret_dir/pi-auth.json" \
|
||||
"THT_SECRETS_FILE=$bundle" >"$operator_env"
|
||||
# The rotation helper has an old/new file interface; these are test-only
|
||||
# scratch files and are never mounted into a Compose service.
|
||||
printf '%s' "$bootstrap_password" >"$secret_dir/bootstrap"
|
||||
@@ -47,7 +54,7 @@ export THT_VECTOR_BOOTSTRAP_USER=thoth_bootstrap_smoke
|
||||
export THOTH_SMOKE_OWNER="$smoke_owner"
|
||||
|
||||
compose() {
|
||||
docker compose -f compose.yaml -f deploy/compose.local-vector.yaml \
|
||||
docker compose --env-file "$operator_env" -f compose.yaml -f deploy/compose.local-vector.yaml \
|
||||
--project-name "$smoke_project" --profile local-vector "$@"
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,13 @@ bundle="$tmp/thothii.secrets"
|
||||
chmod 0600 "$bundle"
|
||||
export THT_SECRETS_FILE="$bundle"
|
||||
export THT_OLLAMA_URL=http://mock-embeddings:8081
|
||||
printf '%s\n' '{}' >"$tmp/pi-auth.json"
|
||||
chmod 0600 "$tmp/pi-auth.json"
|
||||
printf '%s\n' \
|
||||
'THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git' \
|
||||
"PI_AUTH_FILE=$tmp/pi-auth.json" \
|
||||
"THT_SECRETS_FILE=$bundle" \
|
||||
'THT_OLLAMA_URL=http://mock-embeddings:8081' >"$tmp/operator.env"
|
||||
|
||||
cat >"$tmp/smoke.yaml" <<YAML
|
||||
services:
|
||||
@@ -83,7 +90,7 @@ services:
|
||||
mock-embeddings: {condition: service_started}
|
||||
YAML
|
||||
|
||||
compose="docker compose -f compose.yaml -f deploy/compose.local-vector.yaml -f deploy/compose.preprocess.yaml -f deploy/compose.preprocess-local-vector.yaml -f $tmp/smoke.yaml --project-name $project --profile local-vector --profile preprocess"
|
||||
compose="docker compose --env-file $tmp/operator.env -f compose.yaml -f deploy/compose.local-vector.yaml -f deploy/compose.preprocess.yaml -f deploy/compose.preprocess-local-vector.yaml -f $tmp/smoke.yaml --project-name $project --profile local-vector --profile preprocess"
|
||||
$compose build preprocess-evidence
|
||||
if [ "${PREPROCESS_SMOKE_INJECT_FAILURE:-0}" = "1" ]; then
|
||||
sh -c 'exit 97'
|
||||
|
||||
Executable
+99
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
# Active installation manuals must drive the canonical two-service base+profile stack.
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
tmp="$(mktemp -d "${TMPDIR%/}/thoth-canonical-install.XXXXXX")"
|
||||
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
|
||||
|
||||
for retired_example in \
|
||||
"$root/docs/install/examples/local-compose.workspace-registry.yaml" \
|
||||
"$root/docs/install/examples/server-compose.workspace-registry.yaml" \
|
||||
"$root/docs/install/examples/git-ssh.workspace-registry.yaml" \
|
||||
"$root/docs/install/examples/git-https.workspace-registry.yaml"; do
|
||||
if [[ -e "$retired_example" ]]; then
|
||||
echo "superseded one-service install example remains active: ${retired_example#"$root/"}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
printf '%s\n' '{}' >"$tmp/pi-auth.json"
|
||||
printf '%s\n' 'THT_MODEL_API_KEY=fixture-model-api-key' >"$tmp/thothii.secrets"
|
||||
chmod 0600 "$tmp/pi-auth.json" "$tmp/thothii.secrets"
|
||||
mkdir -p "$tmp/data" "$tmp/pi-state" "$tmp/workspace-registry"
|
||||
printf '%s\n' 'fixture-session-password' >"$tmp/session-runtime-password"
|
||||
printf '%s\n' 'fixture-session-migrator-password' >"$tmp/session-migrator-password"
|
||||
printf '%s\n' 'fixture-session-ca' >"$tmp/session-ca.pem"
|
||||
cp "$root/deploy/workspaces/server-sessions.yaml.example" "$tmp/server-sessions.yaml"
|
||||
chmod 0600 "$tmp/session-runtime-password" "$tmp/session-migrator-password" "$tmp/session-ca.pem"
|
||||
|
||||
for profile in local server; do
|
||||
env_file="$tmp/$profile.env"
|
||||
{
|
||||
printf '%s\n' \
|
||||
'THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git' \
|
||||
"PI_AUTH_FILE=$tmp/pi-auth.json" \
|
||||
"THT_SECRETS_FILE=$tmp/thothii.secrets"
|
||||
if [[ "$profile" == server ]]; then
|
||||
printf '%s\n' \
|
||||
"THT_DATA_ROOT=$tmp/data" \
|
||||
"THT_PI_STATE_ROOT=$tmp/pi-state" \
|
||||
"THT_WORKSPACE_REGISTRY_ROOT=$tmp/workspace-registry" \
|
||||
"THT_SERVER_WORKSPACE_CONFIG=$tmp/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=$tmp/session-runtime-password" \
|
||||
"THT_SESSION_MIGRATOR_PASSWORD_SOURCE=$tmp/session-migrator-password" \
|
||||
"THT_SESSION_CA_SOURCE=$tmp/session-ca.pem"
|
||||
fi
|
||||
} >"$env_file"
|
||||
|
||||
compose_files=(-f "$root/compose.yaml" -f "$root/deploy/compose.$profile.yaml")
|
||||
if [[ "$profile" == server ]]; then
|
||||
compose_files+=(-f "$root/deploy/compose.session-server.yaml.example")
|
||||
fi
|
||||
docker compose --env-file "$env_file" "${compose_files[@]}" \
|
||||
config --format json >"$tmp/$profile.json"
|
||||
node - "$tmp/$profile.json" "$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 + ": install stack must be exactly core,frontend");
|
||||
}
|
||||
if (!config.services.core.secrets?.some((secret) => secret.target === "thothii.secrets")) {
|
||||
throw new Error(profile + ": install stack lacks the runtime secret bundle");
|
||||
}
|
||||
if (!config.services.core.volumes?.some(
|
||||
(mount) => mount.target === "/home/thoth/.pi/agent/auth.json" && mount.read_only,
|
||||
)) {
|
||||
throw new Error(profile + ": install stack lacks the read-only Pi auth file");
|
||||
}
|
||||
if ((config.services.frontend.secrets || []).length !== 0) {
|
||||
throw new Error(profile + ": frontend received runtime secrets");
|
||||
}
|
||||
if (profile === "server" && config.services.core.environment?.THT_SESSION_STORAGE !== "postgres") {
|
||||
throw new Error("server: public startup must include the PostgreSQL session override");
|
||||
}
|
||||
if (JSON.stringify(config).includes("fixture-model-api-key")) {
|
||||
throw new Error(profile + ": rendered Compose leaked a secret value");
|
||||
}
|
||||
NODE
|
||||
done
|
||||
|
||||
for profile in local server; do
|
||||
manual="$root/docs/install/$profile-workspace-registry.md"
|
||||
grep -Fq -- '--env-file "$THT_OPERATOR_ENV"' "$manual" \
|
||||
&& grep -Fq -- "-f \"\$THT_SOURCE_ROOT/compose.yaml\" -f \"\$THT_SOURCE_ROOT/deploy/compose.$profile.yaml\"" "$manual" || {
|
||||
echo "$profile manual lacks the canonical base+profile command" >&2
|
||||
exit 1
|
||||
}
|
||||
if rg -q 'local-compose\.workspace-registry|server-compose\.workspace-registry' "$manual"; then
|
||||
echo "$profile manual still references a superseded standalone Compose example" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "canonical install Compose contract passed."
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fresh Compose flow: mounted Pi policy/auth must produce a selectable, credential-ready provider.
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
tmp="$(mktemp -d "${TMPDIR%/}/thoth-provider-readiness.XXXXXX")"
|
||||
project="thothii-provider-readiness-$$"
|
||||
compose=(
|
||||
docker compose --project-name "$project" --env-file "$tmp/local.env"
|
||||
-f "$root/compose.yaml" -f "$root/deploy/compose.local.yaml"
|
||||
)
|
||||
cleanup() {
|
||||
"${compose[@]}" down --volumes --remove-orphans >/dev/null 2>&1 || true
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
|
||||
printf '%s\n' '{"zai":{"type":"api_key","key":"fixture-native-auth-key"}}' >"$tmp/pi-auth.json"
|
||||
printf '%s\n' 'THT_MODEL_API_KEY=fixture-model-api-key' >"$tmp/thothii.secrets"
|
||||
chmod 0600 "$tmp/pi-auth.json" "$tmp/thothii.secrets"
|
||||
printf '%s\n' \
|
||||
'THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git' \
|
||||
"PI_AUTH_FILE=$tmp/pi-auth.json" \
|
||||
"THT_SECRETS_FILE=$tmp/thothii.secrets" \
|
||||
'THOTH_CORE_HTTP_PORT=0' \
|
||||
'THOTH_HTTP_PORT=0' \
|
||||
>"$tmp/local.env"
|
||||
|
||||
"${compose[@]}" up --detach --wait --wait-timeout 90 --build core
|
||||
core_id="$("${compose[@]}" ps -q core)"
|
||||
core_address="$("${compose[@]}" port core 8787 | head -n 1)"
|
||||
|
||||
"${compose[@]}" exec -T core sh -ceu '
|
||||
test -r /home/thoth/.pi/agent/auth.json
|
||||
test -r /home/thoth/.pi/agent/models.json
|
||||
test -r /home/thoth/.pi/agent/settings.json
|
||||
test -r /run/secrets/thothii.secrets
|
||||
'
|
||||
|
||||
curl --fail --silent --show-error "http://$core_address/models" >"$tmp/models.json"
|
||||
node - "$tmp/models.json" <<'NODE'
|
||||
const fs = require("fs");
|
||||
const body = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||
if (!body.models?.some((model) => model.provider === "zai" && model.id === "glm-5.2")) {
|
||||
throw new Error("fresh Compose did not expose the mounted Pi-enabled model");
|
||||
}
|
||||
NODE
|
||||
|
||||
curl --fail --silent --show-error -X PUT \
|
||||
-H 'content-type: application/json' \
|
||||
--data '{"provider":"zai","model":"glm-5.2","reasoning":"low"}' \
|
||||
"http://$core_address/pi-management/config" >"$tmp/configured.json"
|
||||
curl --fail --silent --show-error \
|
||||
"http://$core_address/pi-management/status" >"$tmp/status.json"
|
||||
node - "$tmp/status.json" <<'NODE'
|
||||
const fs = require("fs");
|
||||
const body = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||
if (!body.ready || body.credentials !== "present") {
|
||||
throw new Error("mounted Pi provider is not credential-ready");
|
||||
}
|
||||
if (body.config?.provider !== "zai" || body.config?.model !== "glm-5.2") {
|
||||
throw new Error("Pi provider configuration was not persisted");
|
||||
}
|
||||
NODE
|
||||
|
||||
inspect="$(docker inspect "$core_id")"
|
||||
for secret in fixture-native-auth-key fixture-model-api-key; do
|
||||
if grep -Fq "$secret" <<<"$inspect"; then
|
||||
echo "container inspection leaked $secret" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Compose provider-readiness contract passed."
|
||||
@@ -48,7 +48,13 @@ for (const mount of (core.volumes || []).filter((item) => item.target?.startsWit
|
||||
const secretTargets = (core.secrets || []).map((secret) => secret.target).sort();
|
||||
const expectedSecrets = expectedSecretTargets ? expectedSecretTargets.split(",").filter(Boolean).sort() : [];
|
||||
if (secretTargets.join(",") !== expectedSecrets.join(",")) {
|
||||
throw new Error(`${name}: connector targets do not match generated THT_WS_*_FILE bindings`);
|
||||
throw new Error(`${name}: Docker secret targets do not match the deployment contract`);
|
||||
}
|
||||
if (core.environment?.THT_SECRETS_FILE !== "/run/secrets/thothii.secrets") {
|
||||
throw new Error(`${name}: core does not use the canonical /run/secrets bundle path`);
|
||||
}
|
||||
if ((config.services.frontend?.secrets || []).length !== 0) {
|
||||
throw new Error(`${name}: frontend must not receive runtime secrets`);
|
||||
}
|
||||
|
||||
if (name === "ssh") {
|
||||
@@ -62,7 +68,7 @@ if (name === "https" && core.environment?.GIT_CONFIG_VALUE_1 !== "/run/secrets/w
|
||||
}
|
||||
|
||||
const rendered = JSON.stringify(config);
|
||||
for (const secret of ["fixture-ssh-private-key", "fixture-ssh-known-hosts", "fixture-https-credentials", "fixture-https-ca", "fixture-dwh-password", "fixture-vector-api-key"]) {
|
||||
for (const secret of ["fixture-model-api-key", "fixture-ssh-private-key", "fixture-ssh-known-hosts", "fixture-https-credentials", "fixture-https-ca", "fixture-dwh-password", "fixture-vector-api-key"]) {
|
||||
if (rendered.includes(secret)) throw new Error(`${name}: rendered Compose leaked fixture secret value`);
|
||||
}
|
||||
NODE
|
||||
@@ -97,6 +103,7 @@ assert_unsafe_source_rejected() {
|
||||
}
|
||||
|
||||
write_secret "$fixture_root/pi-auth.json" 'fixture-pi-auth'
|
||||
write_secret "$fixture_root/thothii.secrets" 'THT_MODEL_API_KEY=fixture-model-api-key'
|
||||
write_secret "$fixture_root/ssh-private-key" 'fixture-ssh-private-key'
|
||||
write_secret "$fixture_root/ssh-known-hosts" 'fixture-ssh-known-hosts'
|
||||
write_secret "$fixture_root/https-credentials" 'fixture-https-credentials'
|
||||
@@ -107,6 +114,8 @@ write_secret "$fixture_root/vector-api-key" 'fixture-vector-api-key'
|
||||
printf '%s\n' \
|
||||
'THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git' \
|
||||
"PI_AUTH_FILE=$fixture_root/pi-auth.json" \
|
||||
"THT_SECRETS_FILE=$fixture_root/thothii.secrets" \
|
||||
"THT_WORKSPACE_BINDINGS_ENV_FILE=$fixture_root/workspace-bindings.env" \
|
||||
"THT_WORKSPACE_GIT_SSH_KEY_FILE=$fixture_root/ssh-private-key" \
|
||||
"THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=$fixture_root/ssh-known-hosts" \
|
||||
"THT_WORKSPACE_GIT_CREDENTIALS_FILE=$fixture_root/https-credentials" \
|
||||
@@ -127,13 +136,13 @@ connector_override="$fixture_root/compose.connector-secrets.local.yaml"
|
||||
--output "$connector_override"
|
||||
|
||||
render base
|
||||
assert_render_contract base '' ''
|
||||
assert_render_contract base '' 'thothii.secrets'
|
||||
render ssh -f "$root/deploy/compose.git-ssh.yaml"
|
||||
assert_render_contract ssh '/run/secrets/workspace-registry-git-known-hosts,/run/secrets/workspace-registry-git-ssh-key' ''
|
||||
assert_render_contract ssh '/run/secrets/workspace-registry-git-known-hosts,/run/secrets/workspace-registry-git-ssh-key' 'thothii.secrets'
|
||||
render https -f "$root/deploy/compose.git-https.yaml"
|
||||
assert_render_contract https '/run/secrets/workspace-registry-git-ca,/run/secrets/workspace-registry-git-credentials' ''
|
||||
assert_render_contract https '/run/secrets/workspace-registry-git-ca,/run/secrets/workspace-registry-git-credentials' 'thothii.secrets'
|
||||
render connector -f "$connector_override"
|
||||
assert_render_contract connector '' 'north-star-research-dwh-password,north-star-research-vector-api-key'
|
||||
assert_render_contract connector '' 'north-star-research-dwh-password,north-star-research-vector-api-key,thothii.secrets'
|
||||
|
||||
assert_missing_source_rejected THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE
|
||||
assert_missing_source_rejected THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_SOURCE
|
||||
|
||||
@@ -9,10 +9,13 @@ expected_pi_version=$(sed -n 's/^ARG PI_VERSION=//p' docker/core.Dockerfile)
|
||||
trap 'docker compose --project-name "$project" -f compose.yaml -f deploy/compose.local.yaml down --volumes --remove-orphans >/dev/null 2>&1 || true; rm -rf "$tmp"' EXIT HUP INT TERM
|
||||
|
||||
test -n "$expected_pi_version"
|
||||
printf '{}\n' >"$tmp/pi-auth.json"
|
||||
printf '%s\n' '{"zai":{"type":"api_key","key":"fixture-native-auth-key"}}' >"$tmp/pi-auth.json"
|
||||
chmod 0600 "$tmp/pi-auth.json"
|
||||
printf '%s\n' 'THT_MODEL_API_KEY=fixture-model-api-key' >"$tmp/thothii.secrets"
|
||||
chmod 0600 "$tmp/thothii.secrets"
|
||||
|
||||
export PI_AUTH_FILE="$tmp/pi-auth.json"
|
||||
export THT_SECRETS_FILE="$tmp/thothii.secrets"
|
||||
export THT_WORKSPACE_GIT_REMOTE="https://git.example.invalid/thothii/workspaces.git"
|
||||
# Let Docker assign loopback ports so this isolated contract test never collides with an operator stack.
|
||||
export THOTH_CORE_HTTP_PORT=0
|
||||
@@ -62,6 +65,10 @@ docker compose --project-name "$project" -f compose.yaml -f deploy/compose.local
|
||||
test "$(pi --version)" = "$PI_VERSION"
|
||||
command -v pi >/dev/null
|
||||
test ! -e /var/run/docker.sock
|
||||
test -r /home/thoth/.pi/agent/auth.json
|
||||
test -r /home/thoth/.pi/agent/models.json
|
||||
test -r /home/thoth/.pi/agent/settings.json
|
||||
test -r /run/secrets/thothii.secrets
|
||||
touch /data/.task5-writable
|
||||
rm /data/.task5-writable
|
||||
if find /app /home /data -xdev \( -iname "*chirone*" -o -iname "*omics*portal*" \) -print -quit | grep -q .; then
|
||||
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prevent active operator-facing startup examples from bypassing required env/profile inputs.
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
cd "$root"
|
||||
|
||||
targets=(
|
||||
README.md
|
||||
.env.example
|
||||
docker-compose.dev.yml
|
||||
deploy/env.example
|
||||
deploy/secrets/README.md
|
||||
docs/install
|
||||
docs/index.md
|
||||
docs/installazione-docker-4-contesti.md
|
||||
scripts/build-local.sh
|
||||
scripts/build-local.ps1
|
||||
scripts/docker-smoke.sh
|
||||
scripts/local-vector-smoke.sh
|
||||
scripts/preprocess-smoke.sh
|
||||
scripts/vector-rotate-bootstrap-password.sh
|
||||
)
|
||||
|
||||
existing=()
|
||||
for target in "${targets[@]}"; do
|
||||
[[ ! -e "$target" ]] || existing+=("$target")
|
||||
done
|
||||
|
||||
set +e
|
||||
matches="$(rg -n \
|
||||
'docker compose (up|build|run|config|ps|exec|-f)|DC="docker compose -f|compose="docker compose -f' \
|
||||
"${existing[@]}" 2>&1)"
|
||||
rg_status=$?
|
||||
set -e
|
||||
case "$rg_status" in
|
||||
0)
|
||||
echo "active deployment command omits --env-file before its action/overrides:" >&2
|
||||
printf '%s\n' "$matches" >&2
|
||||
exit 1
|
||||
;;
|
||||
1) ;;
|
||||
*)
|
||||
printf '%s\n' "$matches" >&2
|
||||
exit "$rg_status"
|
||||
;;
|
||||
esac
|
||||
|
||||
for document in README.md docs/installazione-docker-4-contesti.md; do
|
||||
grep -Fq -- '-f deploy/compose.session-server.yaml.example' "$document" || {
|
||||
echo "$document omits the required public-server session override" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
for required in \
|
||||
THT_SERVER_WORKSPACE_CONFIG \
|
||||
THT_SESSION_RUNTIME_PASSWORD_SOURCE \
|
||||
THT_SESSION_MIGRATOR_PASSWORD_SOURCE \
|
||||
THT_SESSION_CA_SOURCE; do
|
||||
grep -q "^$required=" deploy/env/server.env.example || {
|
||||
echo "server env example omits $required" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
echo "deployment command contract passed."
|
||||
@@ -8,6 +8,7 @@ trap cleanup EXIT HUP INT TERM
|
||||
|
||||
export THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git
|
||||
export PI_AUTH_FILE=/dev/null
|
||||
export THT_SECRETS_FILE=/dev/null
|
||||
|
||||
unset THT_VECTOR_BOOTSTRAP_PASSWORD_SECRET_FILE THT_VECTOR_MIGRATOR_PASSWORD_SECRET_FILE
|
||||
unset THT_VECTOR_READER_PASSWORD_SECRET_FILE THT_VECTOR_WRITER_PASSWORD_SECRET_FILE
|
||||
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# Model providers are external endpoints reached through the ordinary application network.
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
tmp="$(mktemp -d "${TMPDIR%/}/thoth-external-llm.XXXXXX")"
|
||||
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
|
||||
printf '%s\n' '{}' >"$tmp/pi-auth.json"
|
||||
printf '%s\n' 'THT_MODEL_API_KEY=fixture-model-api-key' >"$tmp/thothii.secrets"
|
||||
chmod 0600 "$tmp/pi-auth.json" "$tmp/thothii.secrets"
|
||||
|
||||
THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git \
|
||||
PI_AUTH_FILE="$tmp/pi-auth.json" \
|
||||
THT_SECRETS_FILE="$tmp/thothii.secrets" \
|
||||
THT_LLM_URL=https://llm.example.invalid/v1 \
|
||||
docker compose -f "$root/compose.yaml" config --format json >"$tmp/config.json"
|
||||
|
||||
node - "$tmp/config.json" <<'NODE'
|
||||
const fs = require("fs");
|
||||
const config = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||
if (Object.keys(config.services).sort().join(",") !== "core,frontend") {
|
||||
throw new Error("external LLM deployment must retain the mandatory two-service stack");
|
||||
}
|
||||
if (Object.keys(config.networks || {}).join(",") !== "thothii") {
|
||||
throw new Error("external LLM endpoint must not require a provider-owned Docker network");
|
||||
}
|
||||
if (config.services.core.environment?.THT_LLM_URL !== "https://llm.example.invalid/v1") {
|
||||
throw new Error("core did not receive the generic external LLM endpoint");
|
||||
}
|
||||
const joins = (service, network) => Array.isArray(service.networks)
|
||||
? service.networks.includes(network)
|
||||
: Object.hasOwn(service.networks || {}, network);
|
||||
if (!joins(config.services.core, "thothii") || !joins(config.services.frontend, "thothii")) {
|
||||
throw new Error("frontend and core must share only the application network");
|
||||
}
|
||||
NODE
|
||||
|
||||
echo "external LLM network contract passed."
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression coverage for coupling-scan categories, exact exclusions, and scanner failures.
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
fixture="$(mktemp -d "${TMPDIR%/}/thoth-coupling-scope.XXXXXX")"
|
||||
trap 'rm -rf "$fixture"' EXIT HUP INT TERM
|
||||
|
||||
new_fixture() {
|
||||
rm -rf "$fixture/repository"
|
||||
mkdir -p \
|
||||
"$fixture/repository/deploy/env" \
|
||||
"$fixture/repository/deploy/workspaces" \
|
||||
"$fixture/repository/docker/smoke" \
|
||||
"$fixture/repository/docs/install" \
|
||||
"$fixture/repository/docs/superpowers/plans" \
|
||||
"$fixture/repository/frontend" \
|
||||
"$fixture/repository/scripts"
|
||||
|
||||
printf '%s\n' 'services: {}' >"$fixture/repository/compose.yaml"
|
||||
printf '%s\n' '# generic runtime image' >"$fixture/repository/docker/core.Dockerfile"
|
||||
printf '%s\n' '# generic smoke' >"$fixture/repository/docker/smoke/core-smoke.sh"
|
||||
printf '%s\n' '# generic install' >"$fixture/repository/docs/install/local.md"
|
||||
printf '%s\n' 'THT_LLM_URL=https://llm.example.invalid' >"$fixture/repository/deploy/env/local.env.example"
|
||||
printf '%s\n' '# generic launcher' >"$fixture/repository/scripts/run-stack.sh"
|
||||
printf '%s\n' '// generic frontend configuration' >"$fixture/repository/frontend/vite.config.ts"
|
||||
|
||||
# These are the three intentionally allowed categories from the Task 10 boundary.
|
||||
printf '%s\n' 'historical omics_portal and Chirone record' \
|
||||
>"$fixture/repository/docs/superpowers/plans/legacy.md"
|
||||
printf '%s\n' 'id: psd' >"$fixture/repository/deploy/workspaces/psd.yaml.example"
|
||||
printf '%s\n' '# migrate PSD sessions from /home/chirone' \
|
||||
>"$fixture/repository/docker/session-migrate.sh"
|
||||
}
|
||||
|
||||
assert_clean() {
|
||||
"$root/scripts/test-no-deployment-coupling.sh" --root "$fixture/repository" >/dev/null
|
||||
}
|
||||
|
||||
assert_detected() {
|
||||
local relative_path="$1" content="$2" output status
|
||||
new_fixture
|
||||
mkdir -p "$(dirname "$fixture/repository/$relative_path")"
|
||||
printf '%s\n' "$content" >"$fixture/repository/$relative_path"
|
||||
set +e
|
||||
output="$("$root/scripts/test-no-deployment-coupling.sh" --root "$fixture/repository" 2>&1)"
|
||||
status=$?
|
||||
set -e
|
||||
if [[ $status -ne 1 ]] || ! grep -Fq "$relative_path" <<<"$output"; then
|
||||
echo "coupling scan missed $relative_path" >&2
|
||||
printf '%s\n' "$output" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
new_fixture
|
||||
assert_clean
|
||||
|
||||
assert_detected compose.yaml 'services: # Chirone runtime coupling'
|
||||
assert_detected docker/smoke/core-smoke.sh 'test -d /home/chirone'
|
||||
assert_detected docs/install/local.md 'Install the PSD deployment profile.'
|
||||
assert_detected deploy/env/local.env.example 'NETWORK=omics_portal'
|
||||
assert_detected scripts/run-stack.sh 'exec datamart-builder'
|
||||
assert_detected frontend/vite.config.ts 'const base = "/omics_portal";'
|
||||
assert_detected scripts/test-qwen-network-config.sh 'require localllm_default'
|
||||
assert_detected scripts/test-provider-network.sh 'if (!config.networks?.localllm_default?.external) exit 1'
|
||||
assert_detected deploy/compose.psd-local.yaml 'services: {}'
|
||||
|
||||
new_fixture
|
||||
mkdir -p "$fixture/bin"
|
||||
printf '%s\n' '#!/bin/sh' 'exit 2' >"$fixture/bin/rg"
|
||||
chmod +x "$fixture/bin/rg"
|
||||
set +e
|
||||
PATH="$fixture/bin:$PATH" "$root/scripts/test-no-deployment-coupling.sh" \
|
||||
--root "$fixture/repository" >"$fixture/rg.out" 2>"$fixture/rg.err"
|
||||
status=$?
|
||||
set -e
|
||||
if [[ $status -ne 2 ]]; then
|
||||
echo "coupling scan masked an rg failure (status $status)" >&2
|
||||
cat "$fixture/rg.out" "$fixture/rg.err" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "no-coupling scope regression tests passed."
|
||||
@@ -1,69 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
# Category-based guard for active build, runtime, install, and launch coupling.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
script_root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
scan_root="$script_root"
|
||||
if [[ "${1:-}" == --root ]]; then
|
||||
[[ $# -eq 2 ]] || { echo "usage: $0 [--root PATH]" >&2; exit 2; }
|
||||
scan_root="$2"
|
||||
elif [[ $# -ne 0 ]]; then
|
||||
echo "usage: $0 [--root PATH]" >&2
|
||||
exit 2
|
||||
fi
|
||||
[[ -d "$scan_root" ]] || { echo "coupling scan root is not a directory: $scan_root" >&2; exit 2; }
|
||||
cd "$scan_root"
|
||||
|
||||
content_targets=(
|
||||
.dockerignore
|
||||
compose.yaml
|
||||
docker-compose.dev.yml
|
||||
deploy
|
||||
docker
|
||||
frontend/vite.config.ts
|
||||
README.md
|
||||
docs/install
|
||||
docs/installazione-docker-4-contesti.md
|
||||
.env.example
|
||||
scripts/run-stack.sh
|
||||
scripts/docker-smoke.sh
|
||||
)
|
||||
runtime_files=()
|
||||
install_files=()
|
||||
operator_files=()
|
||||
contract_test_files=()
|
||||
add_file() {
|
||||
local array_name="$1" file="$2"
|
||||
[[ ! -f "$file" ]] || eval "$array_name+=(\"\$file\")"
|
||||
}
|
||||
|
||||
matches=$(
|
||||
rg -n -i \
|
||||
-g '!deploy/workspaces/**' \
|
||||
-g '!docker/session-migrate.sh' \
|
||||
-g '!docker/cutover-legacy-sessions.sh' \
|
||||
-g '!docker/smoke/**' \
|
||||
'omics_portal|chirone|localllm_default|datamart-builder|compose\.production\.yaml|compose\.psd-local\.yaml' \
|
||||
"${content_targets[@]}" || true
|
||||
)
|
||||
for file in .dockerignore compose.yaml docker-compose.dev.yml frontend/vite.config.ts; do
|
||||
add_file runtime_files "$file"
|
||||
done
|
||||
if [[ -d deploy ]]; then
|
||||
while IFS= read -r -d '' file; do runtime_files+=("${file#./}"); done < <(
|
||||
find deploy -type f ! -path 'deploy/workspaces/*' -print0
|
||||
)
|
||||
fi
|
||||
if [[ -d docker ]]; then
|
||||
while IFS= read -r -d '' file; do
|
||||
case "$file" in
|
||||
docker/session-migrate.sh|docker/cutover-legacy-sessions.sh) continue ;;
|
||||
esac
|
||||
runtime_files+=("${file#./}")
|
||||
done < <(find docker -type f -print0)
|
||||
fi
|
||||
|
||||
runtime_psd_matches=$(
|
||||
rg -n -i \
|
||||
-g '!deploy/workspaces/**' \
|
||||
-g '!docker/session-migrate.sh' \
|
||||
-g '!docker/cutover-legacy-sessions.sh' \
|
||||
-g '!docker/smoke/**' \
|
||||
'\bpsd\b' \
|
||||
.dockerignore compose.yaml docker-compose.dev.yml deploy docker frontend/vite.config.ts \
|
||||
.env.example scripts/run-stack.sh scripts/docker-smoke.sh || true
|
||||
)
|
||||
for file in README.md .env.example docs/installazione-docker-4-contesti.md; do
|
||||
add_file install_files "$file"
|
||||
done
|
||||
if [[ -d docs/install ]]; then
|
||||
while IFS= read -r -d '' file; do install_files+=("${file#./}"); done < <(
|
||||
find docs/install -type f -print0
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ -d scripts ]]; then
|
||||
while IFS= read -r -d '' file; do
|
||||
case "${file#scripts/}" in
|
||||
test-no-deployment-coupling.sh|test-no-deployment-coupling-scope.sh) continue ;;
|
||||
test-*.sh)
|
||||
contract_test_files+=("${file#./}")
|
||||
continue
|
||||
;;
|
||||
verify-*.sh) continue ;;
|
||||
esac
|
||||
operator_files+=("${file#./}")
|
||||
done < <(find scripts -maxdepth 1 -type f -print0)
|
||||
fi
|
||||
|
||||
offenders=()
|
||||
for superseded_file in \
|
||||
scan_category() {
|
||||
local label="$1" pattern="$2"; shift 2
|
||||
local output rg_status
|
||||
(($#)) || return 0
|
||||
set +e
|
||||
output="$(rg -n -i --with-filename -- "$pattern" "$@" 2>&1)"
|
||||
rg_status=$?
|
||||
set -e
|
||||
case "$rg_status" in
|
||||
0)
|
||||
while IFS= read -r match; do offenders+=("$label: $match"); done <<<"$output"
|
||||
;;
|
||||
1) ;;
|
||||
*)
|
||||
echo "coupling scan failed in $label (rg status $rg_status)" >&2
|
||||
printf '%s\n' "$output" >&2
|
||||
exit "$rg_status"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
for forbidden_file in \
|
||||
deploy/compose.production.yaml \
|
||||
deploy/compose.psd-local.yaml.example \
|
||||
deploy/compose.psd-local.yaml \
|
||||
scripts/bootstrap-local-psd-docker-config.sh \
|
||||
harness/tests/test_psd_local_compose_contract.py
|
||||
do
|
||||
[[ ! -e "$superseded_file" ]] || offenders+=("$superseded_file (forbidden active deployment filename)")
|
||||
scripts/test-qwen-network-config.sh \
|
||||
harness/tests/test_psd_local_compose_contract.py; do
|
||||
[[ ! -e "$forbidden_file" ]] \
|
||||
|| offenders+=("active filename: $forbidden_file (superseded deployment contract)")
|
||||
done
|
||||
|
||||
if [[ -n "$matches" ]]; then
|
||||
while IFS= read -r match; do
|
||||
offenders+=("$match")
|
||||
done <<<"$matches"
|
||||
fi
|
||||
forbidden='omics_portal|chirone|localllm_default|datamart-builder|compose\.production\.yaml|compose\.psd-local\.yaml|\bpsd\b'
|
||||
scan_category runtime "$forbidden" "${runtime_files[@]}"
|
||||
scan_category install "$forbidden" "${install_files[@]}"
|
||||
scan_category operator "$forbidden" "${operator_files[@]}"
|
||||
# Contract tests legitimately quote forbidden names in negative assertions. Scan their positive
|
||||
# deployment wiring constructs instead, so a provider-owned network or retired overlay cannot be
|
||||
# required under a different test filename.
|
||||
positive_contract='networks(\?|\.)?\.?localllm_default|services(\?|\.)?\.?core(\?|\.)?\.?networks(\?|\.)?\.?localllm_default|docker compose[^\n]*(compose\.psd-local|compose\.production)|THT_PSD_[A-Z0-9_]*='
|
||||
scan_category contract-test "$positive_contract" "${contract_test_files[@]}"
|
||||
|
||||
if [[ -n "$runtime_psd_matches" ]]; then
|
||||
while IFS= read -r match; do
|
||||
offenders+=("$match")
|
||||
done <<<"$runtime_psd_matches"
|
||||
fi
|
||||
|
||||
if rg -n 'command -v pi|PI_BIN="pi"|PI_BIN=pi' scripts/run-stack.sh >/dev/null; then
|
||||
offenders+=("scripts/run-stack.sh (requires a host Pi binary)")
|
||||
if [[ -f scripts/run-stack.sh ]]; then
|
||||
set +e
|
||||
host_pi="$(rg -n 'command -v pi|PI_BIN="pi"|PI_BIN=pi' scripts/run-stack.sh 2>&1)"
|
||||
host_pi_status=$?
|
||||
set -e
|
||||
case "$host_pi_status" in
|
||||
0) offenders+=("operator: $host_pi") ;;
|
||||
1) ;;
|
||||
*)
|
||||
echo "coupling scan failed in host-Pi contract (rg status $host_pi_status)" >&2
|
||||
printf '%s\n' "$host_pi" >&2
|
||||
exit "$host_pi_status"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if ((${#offenders[@]})); then
|
||||
|
||||
@@ -8,13 +8,42 @@ trap 'rm -rf "$tmp"' EXIT HUP INT TERM
|
||||
auth_file="$tmp/auth.json"
|
||||
printf '%s\n' '{}' >"$auth_file"
|
||||
chmod 0600 "$auth_file"
|
||||
secrets_file="$tmp/thothii.secrets"
|
||||
printf '%s\n' 'THT_MODEL_API_KEY=fixture-model-api-key' >"$secrets_file"
|
||||
chmod 0600 "$secrets_file"
|
||||
|
||||
rendered=$(THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git \
|
||||
PI_AUTH_FILE="$auth_file" docker compose config)
|
||||
PI_AUTH_FILE="$auth_file" THT_SECRETS_FILE="$secrets_file" docker compose config)
|
||||
printf '%s\n' "$rendered" | grep -q "source: $auth_file"
|
||||
printf '%s\n' "$rendered" | grep -q 'target: /home/thoth/.pi/agent/auth.json'
|
||||
printf '%s\n' "$rendered" | grep -A4 'target: /home/thoth/.pi/agent/auth.json' \
|
||||
| grep -q 'read_only: true'
|
||||
for target in \
|
||||
/home/thoth/.pi/agent/models.json \
|
||||
/home/thoth/.pi/agent/settings.json; do
|
||||
printf '%s\n' "$rendered" | grep -q "target: $target"
|
||||
printf '%s\n' "$rendered" | grep -A4 "target: $target" | grep -q 'read_only: true'
|
||||
done
|
||||
printf '%s\n' "$rendered" | grep -q "file: $secrets_file"
|
||||
printf '%s\n' "$rendered" | grep -q 'target: thothii.secrets'
|
||||
if grep -Fq 'fixture-model-api-key' <<<"$rendered"; then
|
||||
echo "rendered base Compose leaked the model key" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dev_rendered=$(THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git \
|
||||
PI_AUTH_FILE="$auth_file" THT_SECRETS_FILE="$secrets_file" \
|
||||
docker compose --env-file deploy/env/local.env.example -f docker-compose.dev.yml config)
|
||||
printf '%s\n' "$dev_rendered" | grep -q "source: $auth_file"
|
||||
printf '%s\n' "$dev_rendered" | grep -q 'target: /home/thoth/.pi/agent/auth.json'
|
||||
printf '%s\n' "$dev_rendered" | grep -q 'target: /home/thoth/.pi/agent/models.json'
|
||||
printf '%s\n' "$dev_rendered" | grep -q 'target: /home/thoth/.pi/agent/settings.json'
|
||||
printf '%s\n' "$dev_rendered" | grep -q "file: $secrets_file"
|
||||
printf '%s\n' "$dev_rendered" | grep -q 'target: thothii.secrets'
|
||||
if grep -Fq 'fixture-model-api-key' <<<"$dev_rendered"; then
|
||||
echo "rendered development Compose leaked the model key" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
@@ -32,5 +61,7 @@ PY
|
||||
grep -q '^ARG PI_VERSION=0.80.3$' docker/core.Dockerfile
|
||||
grep -q '^PI_AUTH_FILE=/absolute/path/to/pi-auth.json$' deploy/env/local.env.example
|
||||
grep -q '^PI_AUTH_FILE=/absolute/path/to/pi-auth.json$' deploy/env/server.env.example
|
||||
grep -q '^THT_SECRETS_FILE=/absolute/path/to/thothii.secrets$' deploy/env/local.env.example
|
||||
grep -q '^THT_SECRETS_FILE=/absolute/path/to/thothii.secrets$' deploy/env/server.env.example
|
||||
|
||||
echo "Pi user-auth Compose contract passed."
|
||||
|
||||
@@ -4,7 +4,8 @@ set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
tmp_bundle=$(mktemp)
|
||||
trap 'rm -f "$tmp_bundle"' EXIT HUP INT TERM
|
||||
tmp_auth=$(mktemp)
|
||||
trap 'rm -f "$tmp_bundle" "$tmp_auth"' EXIT HUP INT TERM
|
||||
cat >"$tmp_bundle" <<'EOF'
|
||||
THT_VECTOR_BOOTSTRAP_PASSWORD=test-bootstrap
|
||||
THT_VECTOR_MIGRATOR_PASSWORD=test-migrator
|
||||
@@ -12,7 +13,11 @@ THT_VECTOR_READER_PASSWORD=test-reader
|
||||
THT_VECTOR_WRITER_PASSWORD=test-writer
|
||||
EOF
|
||||
chmod 0600 "$tmp_bundle"
|
||||
printf '%s\n' '{}' >"$tmp_auth"
|
||||
chmod 0600 "$tmp_auth"
|
||||
export THT_SECRETS_FILE="$tmp_bundle"
|
||||
export PI_AUTH_FILE="$tmp_auth"
|
||||
export THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git
|
||||
|
||||
local_files="-f compose.yaml -f deploy/compose.local-vector.yaml -f deploy/compose.preprocess.yaml -f deploy/compose.preprocess-local-vector.yaml"
|
||||
local_json=$(docker compose $local_files --profile local-vector --profile preprocess config --format json)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
|
||||
mkdir -p "$tmp/deploy"
|
||||
cp compose.yaml "$tmp/compose.yaml"
|
||||
: >"$tmp/deploy/thothii.env"
|
||||
|
||||
docker compose --project-directory "$tmp" -f "$tmp/compose.yaml" config --format json >"$tmp/config.json"
|
||||
node - "$tmp/config.json" <<'NODE'
|
||||
const fs = require("fs");
|
||||
const config = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||
if (!config.networks?.localllm_default?.external) {
|
||||
throw new Error("localllm_default must be an external network");
|
||||
}
|
||||
if (!config.services?.core?.networks?.localllm_default) {
|
||||
throw new Error("core must join localllm_default");
|
||||
}
|
||||
if (config.services?.frontend?.networks?.localllm_default) {
|
||||
throw new Error("frontend must not join the model network");
|
||||
}
|
||||
NODE
|
||||
@@ -11,8 +11,12 @@ render_profile() {
|
||||
local env_file=$2
|
||||
local compose_file=$3
|
||||
local rendered="$tmp/$profile.json"
|
||||
local -a files=(-f compose.yaml -f "$compose_file")
|
||||
if [[ "$profile" == server ]]; then
|
||||
files+=(-f deploy/compose.session-server.yaml.example)
|
||||
fi
|
||||
|
||||
docker compose --env-file "$env_file" -f compose.yaml -f "$compose_file" \
|
||||
docker compose --env-file "$env_file" "${files[@]}" \
|
||||
config --format json >"$rendered"
|
||||
|
||||
node - "$rendered" "$profile" <<'NODE'
|
||||
@@ -38,6 +42,28 @@ const piAuthMounts = (config.services.core.volumes || []).filter(
|
||||
if (piAuthMounts.length !== 1 || piAuthMounts[0].type !== "bind" || !piAuthMounts[0].read_only) {
|
||||
throw new Error("Pi auth must be one read-only file bind");
|
||||
}
|
||||
if (config.services.core.environment?.THT_SECRETS_FILE !== "/run/secrets/thothii.secrets") {
|
||||
throw new Error("core must read the canonical runtime secret bundle from /run/secrets");
|
||||
}
|
||||
const runtimeSecrets = config.services.core.secrets || [];
|
||||
const bundleSecrets = runtimeSecrets.filter(
|
||||
(secret) => secret.source === "thothii_secrets" && secret.target === "thothii.secrets",
|
||||
);
|
||||
if (bundleSecrets.length !== 1) {
|
||||
throw new Error("core must receive exactly one canonical runtime secret bundle");
|
||||
}
|
||||
if (profile === "local" && runtimeSecrets.length !== 1) {
|
||||
throw new Error("local core must receive only the canonical runtime secret bundle");
|
||||
}
|
||||
if (profile === "server") {
|
||||
const targets = new Set(runtimeSecrets.map((secret) => secret.target));
|
||||
for (const target of ["session_runtime_password", "session_ca.pem"]) {
|
||||
if (!targets.has(target)) throw new Error("server core lacks " + target);
|
||||
}
|
||||
}
|
||||
if ((config.services.frontend.secrets || []).length !== 0) {
|
||||
throw new Error("frontend must not receive runtime secrets");
|
||||
}
|
||||
|
||||
const ports = Object.fromEntries(
|
||||
Object.entries(config.services).map(([name, service]) => [name, service.ports || []]),
|
||||
@@ -60,9 +86,12 @@ assert_remote_required() {
|
||||
local env_file=$1
|
||||
local compose_file=$2
|
||||
local without_remote="$tmp/without-remote.env"
|
||||
local -a files=(-f compose.yaml -f "$compose_file")
|
||||
[[ "$compose_file" != deploy/compose.server.yaml ]] \
|
||||
|| files+=(-f deploy/compose.session-server.yaml.example)
|
||||
grep -v '^THT_WORKSPACE_GIT_REMOTE=' "$env_file" >"$without_remote"
|
||||
|
||||
if env -u THT_WORKSPACE_GIT_REMOTE docker compose --env-file "$without_remote" -f compose.yaml -f "$compose_file" \
|
||||
if env -u THT_WORKSPACE_GIT_REMOTE docker compose --env-file "$without_remote" "${files[@]}" \
|
||||
config --format json >"$tmp/missing-remote.out" 2>"$tmp/missing-remote.err"; then
|
||||
echo "Compose must require THT_WORKSPACE_GIT_REMOTE" >&2
|
||||
exit 1
|
||||
@@ -72,6 +101,7 @@ assert_remote_required() {
|
||||
|
||||
THT_WORKSPACE_GIT_REMOTE=https://git.example.invalid/platform/thoth-workspaces.git \
|
||||
PI_AUTH_FILE=/dev/null \
|
||||
THT_SECRETS_FILE=/dev/null \
|
||||
docker compose -f compose.yaml config --format json >"$tmp/base.json"
|
||||
node - "$tmp/base.json" <<'NODE'
|
||||
const fs = require("fs");
|
||||
@@ -98,6 +128,18 @@ const piAuthMounts = (config.services.core.volumes || []).filter(
|
||||
if (piAuthMounts.length !== 1 || piAuthMounts[0].type !== "bind" || !piAuthMounts[0].read_only) {
|
||||
throw new Error("Pi auth must be one read-only file bind");
|
||||
}
|
||||
if (config.services.core.environment?.THT_SECRETS_FILE !== "/run/secrets/thothii.secrets") {
|
||||
throw new Error("core must read the canonical runtime secret bundle from /run/secrets");
|
||||
}
|
||||
const runtimeSecrets = config.services.core.secrets || [];
|
||||
if (runtimeSecrets.length !== 1
|
||||
|| runtimeSecrets[0].source !== "thothii_secrets"
|
||||
|| runtimeSecrets[0].target !== "thothii.secrets") {
|
||||
throw new Error("core must receive exactly the canonical runtime secret bundle");
|
||||
}
|
||||
if ((config.services.frontend.secrets || []).length !== 0) {
|
||||
throw new Error("frontend must not receive runtime secrets");
|
||||
}
|
||||
NODE
|
||||
|
||||
render_profile local deploy/env/local.env.example deploy/compose.local.yaml
|
||||
|
||||
@@ -9,20 +9,11 @@ trap 'rm -f "$output"' EXIT HUP INT TERM
|
||||
"$root/scripts/verify-workspace-install-docs.sh" --fixtures-only >"$output"
|
||||
|
||||
for fixture in \
|
||||
"local manual requires generated connector override and Compose preflight" \
|
||||
"server manual requires generated connector override and Compose preflight" \
|
||||
"local documented shell environment fixture" \
|
||||
"server documented shell environment fixture" \
|
||||
"copied local base fixture" \
|
||||
"copied server PostgreSQL/TLS fixture" \
|
||||
"copied HTTPS Git override fixture" \
|
||||
"copied SSH Git override fixture" \
|
||||
"copied connector binding/secret fixture" \
|
||||
"core process sees connector bindings and secret files" \
|
||||
"non-path secret-file fixture rejected" \
|
||||
"literal secret-source fixture rejected" \
|
||||
"relative secret-source fixture rejected" \
|
||||
"non-normalized secret-source fixture rejected"; do
|
||||
"local manual canonical base+override references" \
|
||||
"server manual canonical base+override references" \
|
||||
"canonical local base+override fixture" \
|
||||
"canonical server base+override fixture" \
|
||||
"relative secret-source fixture rejected"; do
|
||||
grep -Fqx "$fixture passed" "$output" >/dev/null || {
|
||||
echo "missing fixture verification: $fixture" >&2
|
||||
cat "$output" >&2
|
||||
@@ -37,7 +28,7 @@ for manual in \
|
||||
echo "installation manual does not publish a self-contained THT_SOURCE_ROOT export: $manual" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -Fq 'export THT_WORKSPACE_BINDINGS_ENV_FILE="$(pwd -P)/workspace-bindings.env"' "$manual" || {
|
||||
grep -Fq 'export THT_WORKSPACE_BINDINGS_ENV_FILE=' "$manual" || {
|
||||
echo "installation manual does not publish a self-contained bindings export: $manual" >&2
|
||||
exit 1
|
||||
}
|
||||
@@ -47,7 +38,7 @@ for manual in \
|
||||
fi
|
||||
done
|
||||
|
||||
if rg -n 'connector-secrets\.workspace-registry|docker compose' \
|
||||
if rg -n 'local-compose\.workspace-registry|server-compose\.workspace-registry|connector-secrets\.workspace-registry|docker compose' \
|
||||
"$root/docs/install/local-workspace-registry.md" \
|
||||
"$root/docs/install/server-workspace-registry.md"; then
|
||||
echo "installation manuals still document a bypassed Compose or copied connector override path" >&2
|
||||
|
||||
@@ -29,7 +29,7 @@ trap 'rm -f "$replacement"' EXIT HUP INT TERM
|
||||
cp "$new_secret" "$replacement"
|
||||
chmod 0600 "$replacement"
|
||||
|
||||
docker compose -f compose.yaml -f deploy/compose.local-vector.yaml \
|
||||
docker compose --env-file deploy/env/local.env -f compose.yaml -f deploy/compose.local-vector.yaml \
|
||||
--project-name "$project" --profile local-vector run --rm --no-deps \
|
||||
--user 0:0 \
|
||||
--entrypoint /opt/venv/bin/python \
|
||||
@@ -43,4 +43,4 @@ mv -f "$replacement" "$old_secret"
|
||||
trap - EXIT HUP INT TERM
|
||||
|
||||
echo "Deployment bootstrap secret atomically replaced only after verified database login."
|
||||
echo "Re-run: docker compose -f compose.yaml -f deploy/compose.local-vector.yaml --project-name $project --profile local-vector up --wait vector-reconcile vector-migrate core"
|
||||
echo "Re-run: docker compose --env-file deploy/env/local.env -f compose.yaml -f deploy/compose.local-vector.yaml --project-name $project --profile local-vector up --wait vector-reconcile vector-migrate core"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Validate the installation manuals without reading an operator environment or production remote.
|
||||
# Verify canonical local/server installation manuals and their base+override Compose paths.
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
profile="${1:-}"
|
||||
mode="${1:-}"
|
||||
|
||||
trim() {
|
||||
local value="$1"
|
||||
@@ -41,266 +41,13 @@ verify_path_variable_values() {
|
||||
fi
|
||||
fi
|
||||
done <"$source"
|
||||
return 0
|
||||
}
|
||||
|
||||
verify_server_public_contract() {
|
||||
local server_example="$root/docs/install/examples/server-compose.workspace-registry.yaml"
|
||||
for expected in \
|
||||
'THT_SESSION_STORAGE: postgres' \
|
||||
'THT_SESSION_RUNTIME_PASSWORD_FILE: /run/secrets/session_runtime_password' \
|
||||
'THT_SESSION_DB_SSLMODE: ${THT_SESSION_DB_SSLMODE:-verify-full}' \
|
||||
'THT_SESSION_DB_SSLROOTCERT: /run/secrets/session_ca.pem' \
|
||||
'session_runtime_password:' \
|
||||
'session_ca:'; do
|
||||
grep -Fq "$expected" "$server_example" || {
|
||||
echo "server Compose example lacks required public PostgreSQL/TLS contract: $expected" >&2
|
||||
return 1
|
||||
}
|
||||
done
|
||||
}
|
||||
|
||||
verify_manual_supported_path() {
|
||||
local profile="$1" manual="$2"
|
||||
local source_root_export='export THT_SOURCE_ROOT=/absolute/path/to/ThothII'
|
||||
local bindings_export='export THT_WORKSPACE_BINDINGS_ENV_FILE="$(pwd -P)/workspace-bindings.env"'
|
||||
local generator='"$THT_SOURCE_ROOT/scripts/generate-connector-secrets-override.sh" --bindings-env "$THT_WORKSPACE_BINDINGS_ENV_FILE" --operator-env .env --output connector-secrets.local.yaml'
|
||||
local wrapper='"$THT_SOURCE_ROOT/scripts/compose-with-preflight.sh" --env-file .env'
|
||||
|
||||
grep -Fq "$source_root_export" "$manual" || {
|
||||
echo "$profile manual does not export THT_SOURCE_ROOT for its shell commands" >&2
|
||||
return 1
|
||||
}
|
||||
grep -Fq "$bindings_export" "$manual" || {
|
||||
echo "$profile manual does not export THT_WORKSPACE_BINDINGS_ENV_FILE for its shell commands" >&2
|
||||
return 1
|
||||
}
|
||||
grep -Fq "$generator" "$manual" || {
|
||||
echo "$profile manual does not document the connector override generator" >&2
|
||||
return 1
|
||||
}
|
||||
grep -Fq "$wrapper" "$manual" || {
|
||||
echo "$profile manual does not document the Compose preflight wrapper" >&2
|
||||
return 1
|
||||
}
|
||||
if grep -Eq 'connector-secrets\.workspace-registry|docker compose' "$manual"; then
|
||||
echo "$profile manual documents a bypassed Compose or copied connector override path" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "$profile manual requires generated connector override and Compose preflight passed"
|
||||
}
|
||||
|
||||
compose_fixture() {
|
||||
local name="$1" directory="$2"; shift 2
|
||||
(
|
||||
cd "$directory"
|
||||
"$root/scripts/compose-with-preflight.sh" --env-file .env "$@" config --quiet
|
||||
)
|
||||
echo "$name passed"
|
||||
}
|
||||
|
||||
prepare_binding_fixture() {
|
||||
local directory="$1"
|
||||
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' \
|
||||
>"$directory/workspace-bindings.env"
|
||||
printf 'THT_WORKSPACE_BINDINGS_ENV_FILE=%s\n' "$directory/workspace-bindings.env" >>"$directory/.env"
|
||||
}
|
||||
|
||||
verify_connector_fixture() {
|
||||
local directory="$1" rendered project connector_override
|
||||
project="thoth-install-connector-fixture-$$"
|
||||
connector_override="$directory/connector-secrets.local.yaml"
|
||||
"$root/scripts/generate-connector-secrets-override.sh" \
|
||||
--bindings-env "$directory/workspace-bindings.env" --operator-env "$directory/.env" \
|
||||
--output "$connector_override" >/dev/null
|
||||
rendered="$(
|
||||
cd "$directory"
|
||||
"$root/scripts/compose-with-preflight.sh" --env-file .env \
|
||||
-f compose.workspace-registry.yaml -f connector-secrets.local.yaml config
|
||||
)"
|
||||
for expected in \
|
||||
'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_API_KEY_FILE: /run/secrets/north-star-research-vector-api-key' \
|
||||
'target: north-star-research-dwh-password' \
|
||||
'target: north-star-research-vector-api-key'; do
|
||||
grep -Fq "$expected" <<<"$rendered" || {
|
||||
echo "connector fixture does not give core required binding or secret target: $expected" >&2
|
||||
return 1
|
||||
}
|
||||
done
|
||||
echo "copied connector binding/secret fixture passed"
|
||||
if ! (
|
||||
cd "$directory"
|
||||
"$root/scripts/compose-with-preflight.sh" --project-name "$project" --env-file .env \
|
||||
-f compose.workspace-registry.yaml -f connector-secrets.local.yaml run --rm --no-deps --build --entrypoint sh core -c '
|
||||
test "$THT_WS_NORTH_STAR_RESEARCH_DWH_TRANSPORT" = postgres_direct
|
||||
test "$THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_FILE" = /run/secrets/north-star-research-dwh-password
|
||||
test "$THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_FILE" = /run/secrets/north-star-research-vector-api-key
|
||||
test -f "$THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_FILE"
|
||||
test -f "$THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_FILE"
|
||||
'
|
||||
); then
|
||||
(
|
||||
cd "$directory"
|
||||
"$root/scripts/compose-with-preflight.sh" --project-name "$project" --env-file .env \
|
||||
-f compose.workspace-registry.yaml -f connector-secrets.local.yaml down --volumes --remove-orphans
|
||||
) || true
|
||||
return 1
|
||||
fi
|
||||
(
|
||||
cd "$directory"
|
||||
"$root/scripts/compose-with-preflight.sh" --project-name "$project" --env-file .env \
|
||||
-f compose.workspace-registry.yaml -f connector-secrets.local.yaml down --volumes --remove-orphans
|
||||
)
|
||||
echo "core process sees connector bindings and secret files passed"
|
||||
}
|
||||
|
||||
verify_documented_operator_path() {
|
||||
local profile="$1" directory="$2" documented_source_root="$3" connector_override
|
||||
connector_override="$directory/connector-secrets.local.yaml"
|
||||
(
|
||||
cd "$directory"
|
||||
unset THT_SOURCE_ROOT THT_WORKSPACE_BINDINGS_ENV_FILE
|
||||
export THT_SOURCE_ROOT="$documented_source_root"
|
||||
export THT_WORKSPACE_BINDINGS_ENV_FILE="$(pwd -P)/workspace-bindings.env"
|
||||
"$THT_SOURCE_ROOT/scripts/generate-connector-secrets-override.sh" \
|
||||
--bindings-env "$THT_WORKSPACE_BINDINGS_ENV_FILE" --operator-env .env \
|
||||
--output connector-secrets.local.yaml >/dev/null
|
||||
"$THT_SOURCE_ROOT/scripts/compose-with-preflight.sh" --env-file .env \
|
||||
-f compose.workspace-registry.yaml -f git-ssh.workspace-registry.yaml \
|
||||
-f connector-secrets.local.yaml config --quiet
|
||||
)
|
||||
echo "$profile documented shell environment fixture passed"
|
||||
}
|
||||
|
||||
verify_copied_operator_fixtures() {
|
||||
local fixture_root local_dir server_dir https_dir ssh_dir connector_dir
|
||||
fixture_root="$(mktemp -d "${TMPDIR%/}/thoth-install-fixtures.XXXXXX")"
|
||||
trap 'rm -rf "$fixture_root"' RETURN
|
||||
local_dir="$fixture_root/local"; server_dir="$fixture_root/server"
|
||||
https_dir="$fixture_root/https"; ssh_dir="$fixture_root/ssh"; connector_dir="$fixture_root/connector"
|
||||
mkdir -p "$local_dir" "$server_dir" "$https_dir" "$ssh_dir" "$connector_dir"
|
||||
|
||||
cp "$root/docs/install/examples/local-compose.workspace-registry.yaml" "$local_dir/compose.workspace-registry.yaml"
|
||||
printf 'THT_SOURCE_ROOT=%s\n' "$root" >"$local_dir/.env"
|
||||
prepare_binding_fixture "$local_dir"
|
||||
compose_fixture "copied local base fixture" "$local_dir" -f compose.workspace-registry.yaml
|
||||
|
||||
cp "$root/docs/install/examples/server-compose.workspace-registry.yaml" "$server_dir/compose.workspace-registry.yaml"
|
||||
cp "$root/deploy/workspaces/server-sessions.yaml.example" "$server_dir/server-sessions.yaml"
|
||||
: >"$server_dir/session-runtime-password"; : >"$server_dir/session-ca.pem"
|
||||
printf '%s\n' \
|
||||
"THT_SOURCE_ROOT=$root" \
|
||||
"THT_SERVER_WORKSPACE_CONFIG=$server_dir/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_RUNTIME_PASSWORD_SOURCE=$server_dir/session-runtime-password" \
|
||||
"THT_SESSION_CA_SOURCE=$server_dir/session-ca.pem" >"$server_dir/.env"
|
||||
prepare_binding_fixture "$server_dir"
|
||||
compose_fixture "copied server PostgreSQL/TLS fixture" "$server_dir" -f compose.workspace-registry.yaml
|
||||
|
||||
cp "$root/docs/install/examples/local-compose.workspace-registry.yaml" "$https_dir/compose.workspace-registry.yaml"
|
||||
cp "$root/docs/install/examples/git-https.workspace-registry.yaml" "$https_dir/git-https.yaml"
|
||||
: >"$https_dir/git-credentials"; : >"$https_dir/git-ca.pem"
|
||||
printf '%s\n' \
|
||||
"THT_SOURCE_ROOT=$root" \
|
||||
"THT_WORKSPACE_GIT_CREDENTIALS_FILE=$https_dir/git-credentials" \
|
||||
"THT_WORKSPACE_GIT_CA_FILE=$https_dir/git-ca.pem" >"$https_dir/.env"
|
||||
prepare_binding_fixture "$https_dir"
|
||||
compose_fixture "copied HTTPS Git override fixture" "$https_dir" -f compose.workspace-registry.yaml -f git-https.yaml
|
||||
|
||||
cp "$root/docs/install/examples/local-compose.workspace-registry.yaml" "$ssh_dir/compose.workspace-registry.yaml"
|
||||
cp "$root/docs/install/examples/git-ssh.workspace-registry.yaml" "$ssh_dir/git-ssh.yaml"
|
||||
: >"$ssh_dir/git-ssh-key"; : >"$ssh_dir/git-known-hosts"
|
||||
printf '%s\n' \
|
||||
"THT_SOURCE_ROOT=$root" \
|
||||
"THT_WORKSPACE_GIT_SSH_KEY_FILE=$ssh_dir/git-ssh-key" \
|
||||
"THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=$ssh_dir/git-known-hosts" >"$ssh_dir/.env"
|
||||
prepare_binding_fixture "$ssh_dir"
|
||||
compose_fixture "copied SSH Git override fixture" "$ssh_dir" -f compose.workspace-registry.yaml -f git-ssh.yaml
|
||||
|
||||
: >"$server_dir/git-ssh-key"; : >"$server_dir/git-known-hosts"
|
||||
: >"$server_dir/dwh-password"; : >"$server_dir/vector-api-key"
|
||||
printf '%s\n' \
|
||||
"THT_WORKSPACE_GIT_SSH_KEY_FILE=$server_dir/git-ssh-key" \
|
||||
"THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=$server_dir/git-known-hosts" \
|
||||
"THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE=$server_dir/dwh-password" \
|
||||
"THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_SOURCE=$server_dir/vector-api-key" >>"$server_dir/.env"
|
||||
cp "$root/docs/install/examples/git-ssh.workspace-registry.yaml" "$server_dir/git-ssh.workspace-registry.yaml"
|
||||
: >"$ssh_dir/dwh-password"; : >"$ssh_dir/vector-api-key"
|
||||
printf '%s\n' \
|
||||
"THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE=$ssh_dir/dwh-password" \
|
||||
"THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_SOURCE=$ssh_dir/vector-api-key" >>"$ssh_dir/.env"
|
||||
cp "$root/docs/install/examples/git-ssh.workspace-registry.yaml" "$ssh_dir/git-ssh.workspace-registry.yaml"
|
||||
verify_documented_operator_path local "$ssh_dir" "$root"
|
||||
verify_documented_operator_path server "$server_dir" "$root"
|
||||
|
||||
cp "$root/docs/install/examples/local-compose.workspace-registry.yaml" "$connector_dir/compose.workspace-registry.yaml"
|
||||
: >"$connector_dir/dwh-password"; : >"$connector_dir/vector-password"
|
||||
printf '%s\n' \
|
||||
"THT_SOURCE_ROOT=$root" \
|
||||
"THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE=$connector_dir/dwh-password" \
|
||||
"THT_WS_NORTH_STAR_RESEARCH_VECTOR_API_KEY_SOURCE=$connector_dir/vector-password" >"$connector_dir/.env"
|
||||
prepare_binding_fixture "$connector_dir"
|
||||
verify_connector_fixture "$connector_dir"
|
||||
|
||||
printf 'THT_WS_EXAMPLE_DWH_PASSWORD_FILE=not-a-path\n' >"$fixture_root/non-path-secret.env"
|
||||
if verify_path_variable_values "$fixture_root/non-path-secret.env" >/dev/null 2>&1; then
|
||||
echo "non-path secret-file fixture was accepted" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "non-path secret-file fixture rejected passed"
|
||||
|
||||
printf 'THT_WS_EXAMPLE_DWH_PASSWORD_SOURCE=literal-value\n' >"$fixture_root/literal-source.env"
|
||||
if verify_path_variable_values "$fixture_root/literal-source.env" >/dev/null 2>&1; then
|
||||
echo "literal secret-source fixture was accepted" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "literal secret-source fixture rejected passed"
|
||||
|
||||
printf 'THT_WS_EXAMPLE_DWH_PASSWORD_SOURCE=installation-secrets/password\n' >"$fixture_root/relative-source.env"
|
||||
if verify_path_variable_values "$fixture_root/relative-source.env" >/dev/null 2>&1; then
|
||||
echo "relative secret-source fixture was accepted" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "relative secret-source fixture rejected passed"
|
||||
|
||||
printf 'THT_WS_EXAMPLE_DWH_PASSWORD_SOURCE=/srv/thothii/secrets/../password\n' >"$fixture_root/non-normalized-source.env"
|
||||
if verify_path_variable_values "$fixture_root/non-normalized-source.env" >/dev/null 2>&1; then
|
||||
echo "non-normalized secret-source fixture was accepted" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "non-normalized secret-source fixture rejected passed"
|
||||
}
|
||||
|
||||
case "$profile" in
|
||||
--fixtures-only)
|
||||
[[ $# -eq 1 ]] || { echo "usage: $0 --fixtures-only" >&2; exit 2; }
|
||||
verify_manual_supported_path local "$root/docs/install/local-workspace-registry.md"
|
||||
verify_manual_supported_path server "$root/docs/install/server-workspace-registry.md"
|
||||
verify_copied_operator_fixtures
|
||||
exit 0
|
||||
;;
|
||||
--profile)
|
||||
profile="${2:-}"
|
||||
[[ $# -eq 2 ]] || { echo "usage: $0 --profile {local|server}" >&2; exit 2; }
|
||||
;;
|
||||
*)
|
||||
echo "usage: $0 --profile {local|server}" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$profile" in
|
||||
local)
|
||||
manual="$root/docs/install/local-workspace-registry.md"
|
||||
example="$root/docs/install/examples/local-compose.workspace-registry.yaml"
|
||||
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"
|
||||
@@ -310,10 +57,7 @@ case "$profile" in
|
||||
"Publish, update, backup, outage recovery, and rollback"
|
||||
"Troubleshooting"
|
||||
)
|
||||
;;
|
||||
server)
|
||||
manual="$root/docs/install/server-workspace-registry.md"
|
||||
example="$root/docs/install/examples/server-compose.workspace-registry.yaml"
|
||||
else
|
||||
headings=(
|
||||
"Service account, storage, and firewall"
|
||||
"Gitea and remote Git setup"
|
||||
@@ -324,50 +68,186 @@ case "$profile" in
|
||||
"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 "unknown documentation profile: $profile" >&2
|
||||
echo "usage: $0 --fixtures-only | --profile {local|server}" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
[[ -f "$manual" ]] || { echo "missing $profile installation manual: $manual" >&2; exit 1; }
|
||||
[[ -f "$example" ]] || { echo "missing $profile Compose example: $example" >&2; exit 1; }
|
||||
|
||||
for heading in "${headings[@]}"; do
|
||||
grep -Fqx "## $heading" "$manual" >/dev/null || {
|
||||
echo "missing required heading in $profile manual: $heading" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
grep -Fq "$(basename "$example")" "$manual" || {
|
||||
echo "the $profile manual does not reference its Compose example" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Values for secret-bearing variables must be paths. These patterns catch common accidental
|
||||
# credentials while allowing declarative *_FILE bindings and explicitly empty assignments.
|
||||
if grep -Ein '(^|[[:space:]])(password|api[_-]?key|token|secret)[[:space:]]*[:=][[:space:]]*[^[:space:]#]' \
|
||||
"$manual" "$example" >/dev/null; then
|
||||
echo "installation documentation contains a secret literal" >&2
|
||||
exit 1
|
||||
fi
|
||||
verify_path_variable_values "$manual"
|
||||
verify_path_variable_values "$example"
|
||||
verify_path_variable_values "$root/docs/install/examples/git-https.workspace-registry.yaml"
|
||||
verify_path_variable_values "$root/docs/install/examples/git-ssh.workspace-registry.yaml"
|
||||
verify_path_variable_values "$root/docs/install/examples/workspace-bindings.env.example"
|
||||
verify_server_public_contract
|
||||
verify_manual_supported_path "$profile" "$manual"
|
||||
|
||||
echo "== Validate copied operator fixtures and documented optional Git transports =="
|
||||
verify_copied_operator_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"
|
||||
|
||||
Reference in New Issue
Block a user