docs: add autonomous local installation guide
This commit is contained in:
@@ -43,6 +43,132 @@ verify_path_variable_values() {
|
||||
done <"$source"
|
||||
}
|
||||
|
||||
require_headings() {
|
||||
local source="$1" label="$2"
|
||||
shift 2
|
||||
local heading
|
||||
for heading in "$@"; do
|
||||
grep -Fqx "## $heading" "$source" || {
|
||||
echo "missing required heading in $label: $heading" >&2
|
||||
return 1
|
||||
}
|
||||
done
|
||||
}
|
||||
|
||||
require_text() {
|
||||
local source="$1" label="$2"
|
||||
shift 2
|
||||
local expected
|
||||
for expected in "$@"; do
|
||||
grep -Fq -- "$expected" "$source" || {
|
||||
echo "$label lacks required instruction: $expected" >&2
|
||||
return 1
|
||||
}
|
||||
done
|
||||
}
|
||||
|
||||
verify_local_guide() {
|
||||
local guide="$root/docs/install/local.md"
|
||||
[[ -f "$guide" ]] || {
|
||||
echo "missing local installation guide: docs/install/local.md" >&2
|
||||
return 1
|
||||
}
|
||||
require_headings "$guide" "local installation guide" \
|
||||
"Choose your platform" \
|
||||
"Prerequisites" \
|
||||
"Clone and verify LF" \
|
||||
"Create the local operator files" \
|
||||
"Address external services" \
|
||||
"Build ThothII and thothctl" \
|
||||
"Start and verify" \
|
||||
"Update an installation" \
|
||||
"Back up and restore" \
|
||||
"Data-preserving uninstall" \
|
||||
"Next: workspaces and Pi"
|
||||
require_text "$guide" "local installation guide" \
|
||||
"git clone" \
|
||||
"bash scripts/verify-line-endings.sh" \
|
||||
"deploy/env/local.env" \
|
||||
"host.docker.internal" \
|
||||
"host-gateway" \
|
||||
"container 127.0.0.1" \
|
||||
"bash scripts/build-local.sh" \
|
||||
"scripts/build-local.ps1" \
|
||||
"bash scripts/build-thothctl.sh" \
|
||||
"thothctl --installation" \
|
||||
"curl --fail http://127.0.0.1:8080/health" \
|
||||
"http://127.0.0.1:8080" \
|
||||
"git pull --ff-only" \
|
||||
"docker compose down --volumes"
|
||||
echo "local installation guide contract passed"
|
||||
}
|
||||
|
||||
verify_windows_line_endings_guide() {
|
||||
local guide="$root/docs/install/windows-line-endings.md"
|
||||
[[ -f "$guide" ]] || {
|
||||
echo "missing Windows line-ending guide: docs/install/windows-line-endings.md" >&2
|
||||
return 1
|
||||
}
|
||||
require_headings "$guide" "Windows line-ending guide" \
|
||||
"Recommended WSL2 clone" \
|
||||
"Repository-local LF policy" \
|
||||
"Verify after clone or pull" \
|
||||
"Recover an existing CRLF clone"
|
||||
require_text "$guide" "Windows line-ending guide" \
|
||||
"git config --local core.autocrlf false" \
|
||||
"bash scripts/verify-line-endings.sh" \
|
||||
"git add --renormalize ." \
|
||||
"git diff --cached --check" \
|
||||
"reclone"
|
||||
if grep -Fq 'git reset --hard' "$guide"; then
|
||||
node - "$guide" <<'NODE'
|
||||
const fs = require("fs");
|
||||
const lines = fs.readFileSync(process.argv[2], "utf8").split(/\n/);
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
if (!lines[index].includes("git reset --hard")) continue;
|
||||
const warning = lines.slice(Math.max(0, index - 4), index).join(" ").toLowerCase();
|
||||
if (!warning.includes("warning") || !warning.includes("destructive") ||
|
||||
!warning.includes("backup") || !warning.includes("commit")) {
|
||||
throw new Error("git reset --hard lacks an immediate destructive warning requiring backup/commit");
|
||||
}
|
||||
}
|
||||
NODE
|
||||
fi
|
||||
echo "Windows line-ending recovery guide contract passed"
|
||||
}
|
||||
|
||||
verify_pi_management_guide() {
|
||||
local guide="$root/docs/install/pi-management.md"
|
||||
[[ -f "$guide" ]] || {
|
||||
echo "missing Pi management guide: docs/install/pi-management.md" >&2
|
||||
return 1
|
||||
}
|
||||
require_headings "$guide" "Pi management guide" \
|
||||
"Who can use Pi Management" \
|
||||
"Use the Pi Management page" \
|
||||
"Use thothctl" \
|
||||
"Handle credentials and secrets" \
|
||||
"Update and roll back Pi" \
|
||||
"Recover a failed update" \
|
||||
"Direct support access"
|
||||
require_text "$guide" "Pi management guide" \
|
||||
"pi status" \
|
||||
"pi doctor" \
|
||||
"pi test" \
|
||||
"pi check" \
|
||||
"pi configure" \
|
||||
"pi update" \
|
||||
"pi rollback --yes" \
|
||||
"pi maintenance status" \
|
||||
"pi maintenance recover --yes" \
|
||||
"pi logs" \
|
||||
"/run/secrets" \
|
||||
"docker compose exec core pi" \
|
||||
"no browser shell" \
|
||||
"does not mount the Docker socket"
|
||||
echo "Pi management guide contract passed"
|
||||
}
|
||||
|
||||
verify_manual() {
|
||||
local profile="$1" manual
|
||||
manual="$root/docs/install/$profile-workspace-registry.md"
|
||||
@@ -93,6 +219,101 @@ verify_manual() {
|
||||
echo "$profile manual canonical base+override references passed"
|
||||
}
|
||||
|
||||
verify_local_installation_example() {
|
||||
local example="$root/docs/install/examples/thothii-installation.local.yaml"
|
||||
[[ -f "$example" ]] || {
|
||||
echo "missing local installation example: docs/install/examples/thothii-installation.local.yaml" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
local fixture source_copy operator_dir copied_example connector_override env_file
|
||||
fixture="$(mktemp -d "${TMPDIR%/}/thoth local install.XXXXXX")"
|
||||
trap 'rm -rf "$fixture"' RETURN
|
||||
[[ "$fixture" == *" "* ]] || {
|
||||
echo "local installation fixture path does not contain spaces" >&2
|
||||
return 1
|
||||
}
|
||||
source_copy="$fixture/ThothII source"
|
||||
operator_dir="$fixture/operator files"
|
||||
mkdir -p "$source_copy/deploy/pi" "$operator_dir"
|
||||
cp "$root/compose.yaml" "$source_copy/compose.yaml"
|
||||
cp "$root/deploy/compose.local.yaml" "$source_copy/deploy/compose.local.yaml"
|
||||
cp "$root/deploy/compose.git-ssh.yaml" "$source_copy/deploy/compose.git-ssh.yaml"
|
||||
cp "$root/deploy/pi/models.json" "$source_copy/deploy/pi/models.json"
|
||||
cp "$root/deploy/pi/settings.json" "$source_copy/deploy/pi/settings.json"
|
||||
|
||||
write_private "$operator_dir/pi-auth.json" '{"zai":{"type":"api_key","key":"fixture-local-pi-key"}}'
|
||||
write_private "$operator_dir/thothii.secrets" 'THT_MODEL_API_KEY=fixture-local-model-key'
|
||||
write_private "$operator_dir/git-ssh-key" 'fixture-local-ssh-key'
|
||||
write_private "$operator_dir/git-known-hosts" 'fixture-local-known-hosts'
|
||||
write_private "$operator_dir/dwh-password" 'fixture-local-dwh-password'
|
||||
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' \
|
||||
>"$operator_dir/workspace-bindings.env"
|
||||
env_file="$source_copy/deploy/env/local.env"
|
||||
mkdir -p "$source_copy/deploy/env"
|
||||
printf '%s\n' \
|
||||
'THT_WORKSPACE_GIT_REMOTE=ssh://git@git.example.invalid/platform/thoth-workspaces.git' \
|
||||
"PI_AUTH_FILE=$operator_dir/pi-auth.json" \
|
||||
"THT_SECRETS_FILE=$operator_dir/thothii.secrets" \
|
||||
"THT_WORKSPACE_BINDINGS_ENV_FILE=$operator_dir/workspace-bindings.env" \
|
||||
"THT_WORKSPACE_GIT_SSH_KEY_FILE=$operator_dir/git-ssh-key" \
|
||||
"THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=$operator_dir/git-known-hosts" \
|
||||
"THT_WS_NORTH_STAR_RESEARCH_DWH_PASSWORD_SOURCE=$operator_dir/dwh-password" \
|
||||
>"$env_file"
|
||||
connector_override="$operator_dir/connector-secrets.local.yaml"
|
||||
"$root/scripts/generate-connector-secrets-override.sh" \
|
||||
--bindings-env "$operator_dir/workspace-bindings.env" \
|
||||
--operator-env "$env_file" \
|
||||
--output "$connector_override" >/dev/null
|
||||
|
||||
copied_example="$fixture/thothii-installation.yaml"
|
||||
local contents
|
||||
contents="$(<"$example")"
|
||||
contents="${contents//\/absolute\/path\/to\/ThothII/$source_copy}"
|
||||
contents="${contents//\/absolute\/path\/to\/thothii-operator/$operator_dir}"
|
||||
printf '%s\n' "$contents" >"$copied_example"
|
||||
|
||||
local profile project_directory descriptor_env value
|
||||
local -a overrides files
|
||||
profile="$(sed -n 's/^profile: \([^[:space:]]*\)$/\1/p' "$copied_example")"
|
||||
project_directory="$(sed -n 's/^projectDirectory: "\(.*\)"$/\1/p' "$copied_example")"
|
||||
descriptor_env="$(sed -n 's/^envFile: "\(.*\)"$/\1/p' "$copied_example")"
|
||||
while IFS= read -r value; do overrides+=("$value"); done < <(sed -n 's/^ - "\(.*\)"$/\1/p' "$copied_example")
|
||||
[[ "$profile" == local && "$project_directory" == "$source_copy" && "$descriptor_env" == "$env_file" ]] || {
|
||||
echo "local installation example does not resolve its required fields" >&2
|
||||
return 1
|
||||
}
|
||||
[[ "${#overrides[@]}" -eq 2 && "${overrides[1]}" == "$connector_override" ]] || {
|
||||
echo "local installation example does not select the expected optional overrides" >&2
|
||||
return 1
|
||||
}
|
||||
files=(-f "$project_directory/compose.yaml" -f "$project_directory/deploy/compose.$profile.yaml")
|
||||
for value in "${overrides[@]}"; do files+=(-f "$value"); done
|
||||
local rendered="$fixture/local-installation.json"
|
||||
"$root/scripts/compose-with-preflight.sh" --env-file "$descriptor_env" \
|
||||
"${files[@]}" config --format json >"$rendered"
|
||||
node - "$rendered" <<'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("local installation example must render exactly core,frontend");
|
||||
}
|
||||
const output = JSON.stringify(config);
|
||||
for (const secret of [
|
||||
"fixture-local-pi-key",
|
||||
"fixture-local-model-key",
|
||||
"fixture-local-ssh-key",
|
||||
"fixture-local-known-hosts",
|
||||
"fixture-local-dwh-password",
|
||||
]) {
|
||||
if (output.includes(secret)) throw new Error("local installation rendering exposed a fixture secret");
|
||||
}
|
||||
NODE
|
||||
echo "local installation example rendered from path with spaces passed"
|
||||
}
|
||||
|
||||
write_private() {
|
||||
local path="$1" value="$2"
|
||||
printf '%s\n' "$value" >"$path"
|
||||
@@ -229,6 +450,10 @@ NODE
|
||||
case "$mode" in
|
||||
--fixtures-only)
|
||||
[[ $# -eq 1 ]] || { echo "usage: $0 --fixtures-only" >&2; exit 2; }
|
||||
verify_local_guide
|
||||
verify_windows_line_endings_guide
|
||||
verify_pi_management_guide
|
||||
verify_local_installation_example
|
||||
verify_manual local
|
||||
verify_manual server
|
||||
verify_compose_fixtures
|
||||
@@ -237,6 +462,12 @@ case "$mode" in
|
||||
profile="${2:-}"
|
||||
[[ $# -eq 2 && "$profile" =~ ^(local|server)$ ]] \
|
||||
|| { echo "usage: $0 --profile {local|server}" >&2; exit 2; }
|
||||
if [[ "$profile" == local ]]; then
|
||||
verify_local_guide
|
||||
verify_windows_line_endings_guide
|
||||
verify_pi_management_guide
|
||||
verify_local_installation_example
|
||||
fi
|
||||
verify_manual "$profile"
|
||||
verify_compose_fixtures
|
||||
echo "== Run isolated workspace-registry bootstrap and recovery smoke =="
|
||||
|
||||
Reference in New Issue
Block a user