203 lines
8.4 KiB
Bash
Executable File
203 lines
8.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Generate Task 13 fixtures and validate rendered bindings with the production workspace resolver.
|
|
set -euo pipefail
|
|
|
|
profile="${1:-}"
|
|
[[ "$profile" == local || "$profile" == server ]] || {
|
|
echo "usage: $0 local|server" >&2
|
|
exit 2
|
|
}
|
|
root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
tmp_parent="${TMPDIR:-/tmp}"
|
|
tmp_parent="${tmp_parent%/}"
|
|
fixture="$(mktemp -d "$tmp_parent/thoth-task13-runtime-$profile.XXXXXX")"
|
|
trap 'rm -rf "$fixture"' EXIT HUP INT TERM
|
|
|
|
# shellcheck source=./unified-deployment-smoke.sh
|
|
source "$root/scripts/unified-deployment-smoke.sh"
|
|
TASK13_ROOT="$root"
|
|
TASK13_TMP="$fixture"
|
|
TASK13_RUN_ID="fixture-$profile"
|
|
TASK13_PROJECT="thothii-task13-$profile"
|
|
TASK13_PROFILE="$profile"
|
|
TASK13_CORE_IMAGE="task13-core-$profile:fixture"
|
|
TASK13_FRONTEND_IMAGE="task13-frontend-$profile:fixture"
|
|
TASK13_SECRET_VALUE="task13-runtime-secret-$profile"
|
|
TASK13_BRANCH=main
|
|
TASK13_ENV_FILE="$fixture/operator.env"
|
|
TASK13_OVERRIDE="$fixture/compose.task13.yaml"
|
|
TASK13_LOG="$fixture/task13.log"
|
|
: >"$TASK13_LOG"
|
|
TASK13_INSTALLATION="$fixture/thothii-installation.yaml"
|
|
TASK13_PI_AUTH="$fixture/pi-auth.json"
|
|
TASK13_SECRETS="$fixture/thothii.secrets"
|
|
TASK13_AUTH_ROOT="$fixture/auth"
|
|
TASK13_AUTH_RUNTIME_ROOT="$fixture/auth-runtime"
|
|
TASK13_AUTH_PASSWORD_FILE="$fixture/local-auth-password"
|
|
TASK13_AUTH_ADMIN=task13-admin
|
|
TASK13_AUTH_PASSWORD="fixture-auth-password-$profile"
|
|
TASK13_OIDC_CLIENT_SECRET="fixture-oidc-client-$profile"
|
|
TASK13_AUTHENTIK_API_TOKEN="fixture-authentik-token-$profile"
|
|
TASK13_FRONTEND_PORT=18080
|
|
TASK13_SESSION_RUNTIME_PASSWORD="$fixture/runtime-password"
|
|
TASK13_SESSION_CA="$fixture/session-ca.pem"
|
|
TASK13_LLM_SERVER="$fixture/fake-llm.mjs"
|
|
TASK13_LLM_CONTAINER="$TASK13_PROJECT-llm"
|
|
TASK13_REMOTE="$fixture/remote.git"
|
|
TASK13_CURRENT_IMAGE_OVERRIDE="$fixture/current-image.yaml"
|
|
mkdir -p "$TASK13_REMOTE"
|
|
|
|
workspace="$fixture/task13-smoke.yaml"
|
|
cp "$root/scripts/fixtures/workspace-registry-task13.yaml" "$workspace"
|
|
|
|
if [[ "$profile" == local ]]; then
|
|
task13_write_fixture_files
|
|
task13_write_environment /fixtures/remote.git
|
|
compose_files=(-f "$root/compose.yaml" -f "$root/deploy/compose.local.yaml" -f "$TASK13_OVERRIDE" -f "$fixture/generated/compose.models.yaml")
|
|
else
|
|
TASK13_SERVER_DATA="$fixture/Server Data"
|
|
TASK13_SERVER_PI_STATE="$fixture/Server Pi State"
|
|
TASK13_SERVER_REGISTRY="$fixture/Server Registry"
|
|
TASK13_SERVER_WORKSPACE_CONFIG="$fixture/server-sessions.yaml"
|
|
TASK13_SESSION_RUNTIME_PASSWORD="$fixture/session-runtime-password"
|
|
TASK13_SESSION_MIGRATOR_PASSWORD_FILE="$fixture/session-migrator-password"
|
|
TASK13_SESSION_CA="$fixture/session-ca.pem"
|
|
TASK13_SESSION_PASSWORD="fixture-private-token-$profile"
|
|
TASK13_SESSION_MIGRATOR_PASSWORD="fixture-migrator-token-$profile"
|
|
task13_write_server_fixture_files
|
|
original_task13_run_logged="$(declare -f task13_run_logged)"
|
|
ownership_calls="$fixture/server-auth-ownership.calls"
|
|
task13_run_logged() {
|
|
printf '%s\n' "$*" >>"$ownership_calls"
|
|
}
|
|
task13_prepare_server_auth_roots
|
|
grep -Fxq -- \
|
|
"prepare server authentication canonical root sudo -n -- install -d -o 0 -g 0 -m 0700 -- $TASK13_AUTH_ROOT" \
|
|
"$ownership_calls" || {
|
|
echo "server auth fixture does not prepare a root-owned private canonical directory" >&2
|
|
exit 1
|
|
}
|
|
grep -Fxq -- \
|
|
"prepare server authentication runtime root sudo -n -- install -d -o 10001 -g 10001 -m 0700 -- $TASK13_AUTH_RUNTIME_ROOT" \
|
|
"$ownership_calls" || {
|
|
echo "server auth fixture does not prepare the private runtime projection directory" >&2
|
|
exit 1
|
|
}
|
|
unset -f task13_run_logged
|
|
eval "$original_task13_run_logged"
|
|
compose_files=(
|
|
-f "$root/compose.yaml"
|
|
-f "$root/deploy/compose.server.yaml"
|
|
-f "$root/deploy/compose.session-server.yaml.example"
|
|
-f "$TASK13_OVERRIDE"
|
|
-f "$fixture/generated/compose.models.yaml"
|
|
-f "$root/deploy/compose.auth-runtime-projection.yaml"
|
|
)
|
|
fi
|
|
|
|
rendered="$fixture/rendered.json"
|
|
docker compose --project-name "$TASK13_PROJECT" --project-directory "$root" \
|
|
--env-file "$TASK13_ENV_FILE" "${compose_files[@]}" config --format json >"$rendered"
|
|
if [[ "$profile" == server ]]; then
|
|
original_task13_compose_invoke="$(declare -f task13_compose_invoke)"
|
|
task13_compose_invoke() {
|
|
"$@"
|
|
}
|
|
fi
|
|
task13_assert_rendered_contract
|
|
if [[ "$profile" == server ]]; then
|
|
unset -f task13_compose_invoke
|
|
eval "$original_task13_compose_invoke"
|
|
fi
|
|
maintenance_rendered="$fixture/maintenance-rendered.json"
|
|
docker compose --project-name "$TASK13_PROJECT" --project-directory "$root" \
|
|
--env-file "$TASK13_ENV_FILE" "${compose_files[@]}" --profile workspace-maintenance \
|
|
config --format json >"$maintenance_rendered"
|
|
node - "$maintenance_rendered" <<'NODE'
|
|
const config = JSON.parse(require("fs").readFileSync(process.argv[2], "utf8"));
|
|
const core = config.services?.core;
|
|
const maintenance = config.services?.["workspace-maintenance"];
|
|
if (!core || !maintenance || maintenance.image !== core.image) {
|
|
throw new Error("workspace-maintenance must use the isolated core image");
|
|
}
|
|
NODE
|
|
tsx_loader="$root/backend/node_modules/tsx/dist/loader.mjs"
|
|
checker=(node --import "$tsx_loader" "$root/scripts/task13-runtime-fixture-check.ts")
|
|
[[ -f "$tsx_loader" ]] || {
|
|
echo "backend dependencies are required for the Task 13 runtime fixture contract" >&2
|
|
exit 2
|
|
}
|
|
"${checker[@]}" "$rendered" "$workspace" "$profile" "$TASK13_SECRETS" "$TASK13_SESSION_RUNTIME_PASSWORD"
|
|
|
|
for mutation in \
|
|
wrong-service \
|
|
wrong-value \
|
|
wrong-secret-mount \
|
|
wrong-qdrant-service \
|
|
wrong-embedding-service \
|
|
external-semantic-urls; do
|
|
mutated="$fixture/$mutation.json"
|
|
node - "$rendered" "$mutated" "$mutation" "$profile" <<'NODE'
|
|
const fs = require("fs");
|
|
const [source, destination, mutation, profile] = process.argv.slice(2);
|
|
const config = JSON.parse(fs.readFileSync(source, "utf8"));
|
|
if (mutation === "wrong-service") {
|
|
const name = "THT_WS_TASK13_SMOKE_DWH_HOST";
|
|
config.services.frontend.environment ||= {};
|
|
config.services.frontend.environment[name] = config.services.core.environment[name];
|
|
delete config.services.core.environment[name];
|
|
} else if (mutation === "wrong-value") {
|
|
config.services.core.environment.THT_WS_TASK13_SMOKE_DWH_HOST = "wrong.task13.invalid";
|
|
} else if (mutation === "wrong-secret-mount") {
|
|
if (profile === "local") {
|
|
const mount = config.services.core.volumes.find((item) => item.target === "/run/secrets");
|
|
mount.source = "wrong-application-secrets";
|
|
} else {
|
|
config.services.core.secrets[0].target = "wrong.secrets";
|
|
}
|
|
} else if (mutation === "wrong-qdrant-service") {
|
|
config.services.core.environment.THT_INTERNAL_QDRANT_URL = "http://vector:6333";
|
|
} else if (mutation === "wrong-embedding-service") {
|
|
config.services.core.environment.THT_INTERNAL_EMBEDDING_URL = "http://ollama:11434";
|
|
} else if (mutation === "external-semantic-urls") {
|
|
config.services.core.environment.THT_INTERNAL_QDRANT_URL = "https://qdrant.example.test";
|
|
config.services.core.environment.THT_INTERNAL_EMBEDDING_URL = "https://embedding.example.test";
|
|
}
|
|
fs.writeFileSync(destination, JSON.stringify(config));
|
|
NODE
|
|
if "${checker[@]}" "$mutated" "$workspace" "$profile" \
|
|
"$TASK13_SECRETS" "$TASK13_SESSION_RUNTIME_PASSWORD" \
|
|
>"$fixture/$mutation.out" 2>"$fixture/$mutation.err"; then
|
|
echo "runtime fixture checker accepted mutation: $mutation" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
for mutation in workspace-semantic-index workspace-llm-policy; do
|
|
mutated="$fixture/$mutation.yaml"
|
|
node - "$root/backend/package.json" "$workspace" "$mutated" "$mutation" <<'NODE'
|
|
const fs = require("fs");
|
|
const { createRequire } = require("module");
|
|
const requireFromBackend = createRequire(process.argv[2]);
|
|
const yaml = requireFromBackend("yaml");
|
|
const [source, destination, mutation] = process.argv.slice(3);
|
|
const workspace = yaml.parse(fs.readFileSync(source, "utf8"));
|
|
if (mutation === "workspace-semantic-index") {
|
|
workspace.semantic_index = {
|
|
vector_store: { engine: "qdrant", collection: "shared-semantic", dimensions: 1536 },
|
|
};
|
|
} else if (mutation === "workspace-llm-policy") {
|
|
workspace.llm_policy = { provider: "openai", model: "gpt-test" };
|
|
}
|
|
fs.writeFileSync(destination, yaml.stringify(workspace));
|
|
NODE
|
|
if "${checker[@]}" "$rendered" "$mutated" "$profile" \
|
|
"$TASK13_SECRETS" "$TASK13_SESSION_RUNTIME_PASSWORD" \
|
|
>"$fixture/$mutation.out" 2>"$fixture/$mutation.err"; then
|
|
echo "runtime fixture checker accepted workspace mutation: $mutation" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
echo "Task 13 $profile rendered runtime fixture contract passed."
|