#!/usr/bin/env bash # Validate the installation manuals without reading an operator environment or production remote. set -euo pipefail root="$(cd "$(dirname "$0")/.." && pwd -P)" profile="${1:-}" trim() { local value="$1" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" printf '%s' "$value" } is_safe_absolute_path() { local value="$1" segment local -a segments [[ "$value" == /* && "$value" != *//* ]] || return 1 IFS=/ read -r -a segments <<<"$value" for segment in "${segments[@]}"; do [[ "$segment" != . && "$segment" != .. ]] || return 1 done } verify_path_variable_values() { local source="$1" line trimmed name value while IFS= read -r line || [[ -n "$line" ]]; do trimmed="$(trim "$line")" if [[ "$trimmed" == *=* || "$trimmed" == *:* ]]; then name="$(trim "${trimmed%%[=:]*}")" value="$(trim "${trimmed#"$name"}")" value="$(trim "${value#[:=]}")" if [[ "$name" =~ ^[A-Za-z_][A-Za-z0-9_]*_(FILE|SOURCE)$ ]]; then value="$(trim "${value%%#*}")" value="${value#\"}"; value="${value%\"}" value="${value#\'}"; value="${value%\'}" if [[ -n "$value" ]] && ! is_safe_absolute_path "$value"; then echo "unsafe path value for $name in $source" >&2 return 1 fi fi fi done <"$source" 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 } compose_fixture() { local name="$1" directory="$2"; shift 2 ( cd "$directory" docker compose --env-file .env "$@" config --quiet ) echo "$name passed" } prepare_binding_fixture() { local directory="$1" cp "$root/docs/install/examples/workspace-bindings.env.example" "$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 project="thoth-install-connector-fixture-$$" rendered="$( cd "$directory" docker compose --env-file .env -f compose.workspace-registry.yaml -f connector-secrets.yaml config )" for expected in \ 'THT_WS_PSD_CLINICAL_DWH_TRANSPORT: postgres_direct' \ 'THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE: /run/secrets/psd-clinical-dwh-password' \ 'THT_WS_PSD_CLINICAL_VECTOR_PASSWORD_FILE: /run/secrets/psd-clinical-vector-password' \ 'target: psd-clinical-dwh-password' \ 'target: psd-clinical-vector-password'; 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" docker compose --project-name "$project" --env-file .env -f compose.workspace-registry.yaml -f connector-secrets.yaml \ run --rm --no-deps --build --entrypoint sh core -c ' test "$THT_WS_PSD_CLINICAL_DWH_TRANSPORT" = postgres_direct test "$THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE" = /run/secrets/psd-clinical-dwh-password test "$THT_WS_PSD_CLINICAL_VECTOR_PASSWORD_FILE" = /run/secrets/psd-clinical-vector-password test -f "$THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE" test -f "$THT_WS_PSD_CLINICAL_VECTOR_PASSWORD_FILE" ' ); then ( cd "$directory" docker compose --project-name "$project" --env-file .env -f compose.workspace-registry.yaml -f connector-secrets.yaml \ down --volumes --remove-orphans ) || true return 1 fi ( cd "$directory" docker compose --project-name "$project" --env-file .env -f compose.workspace-registry.yaml -f connector-secrets.yaml \ down --volumes --remove-orphans ) echo "core process sees connector bindings and secret files passed" } verify_copied_operator_fixtures() { local fixture_root local_dir server_dir https_dir ssh_dir connector_dir fixture_root="$(mktemp -d "${TMPDIR:-/tmp}/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 cp "$root/docs/install/examples/local-compose.workspace-registry.yaml" "$connector_dir/compose.workspace-registry.yaml" cp "$root/docs/install/examples/connector-secrets.workspace-registry.yaml" "$connector_dir/connector-secrets.yaml" : >"$connector_dir/dwh-password"; : >"$connector_dir/vector-password" printf '%s\n' \ "THT_SOURCE_ROOT=$root" \ "THT_WS_PSD_CLINICAL_DWH_PASSWORD_SOURCE=$connector_dir/dwh-password" \ "THT_WS_PSD_CLINICAL_VECTOR_PASSWORD_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_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" headings=( "Prerequisites" "Git remote: SSH and HTTPS" "Shared Git values, local bindings, and secret files" "Direct PostgreSQL, REST, and SSH tunnel bindings" "Bootstrap, first pull, and diagnostics" "Publish, update, backup, outage recovery, and rollback" "Troubleshooting" ) ;; server) manual="$root/docs/install/server-workspace-registry.md" example="$root/docs/install/examples/server-compose.workspace-registry.yaml" headings=( "Service account, storage, and firewall" "Gitea and remote Git setup" "Git credentials, CA, SSH key, and known-hosts mounts" "Shared Git values, local bindings, and secret files" "Direct PostgreSQL, REST, and SSH tunnel bindings" "Same-origin reverse proxy, bootstrap, and health" "Pull, publish, upgrade, backup, and recovery" "Troubleshooting and snapshot rollback" ) ;; *) echo "unknown documentation profile: $profile" >&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_path_variable_values "$root/docs/install/examples/connector-secrets.workspace-registry.yaml" verify_server_public_contract commands="$(mktemp "${TMPDIR:-/tmp}/thoth-install-docs.XXXXXX")" trap 'rm -f "$commands"' EXIT HUP INT TERM # A runnable documentation command is a sh fence immediately following this marker. Commands # outside the marker are explanatory/operator commands and are deliberately never executed here. awk ' /^[[:space:]]*$/ { marked=1; next } marked && /^```(sh|bash|shell)[[:space:]]*$/ { in_fence=1; marked=0; seen=1; next } in_fence && /^```[[:space:]]*$/ { in_fence=0; next } in_fence { print } ' "$manual" >"$commands" [[ -s "$commands" ]] || { echo "no marked runnable commands in $profile manual" >&2; exit 1; } echo "== Validate $profile documented Compose example ==" ( cd "$root" bash "$commands" ) echo "== Run isolated workspace-registry bootstrap and recovery smoke ==" ( cd "$root" env -u WORKSPACE_GIT_REMOTE ./scripts/workspace-registry-smoke.sh ) echo "== Validate copied operator fixtures and optional Git transports ==" verify_copied_operator_fixtures echo "$profile installation documentation verification passed"