Files
ThothII/scripts/verify-workspace-install-docs.sh
T

361 lines
16 KiB
Bash
Executable File

#!/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
}
verify_manual_supported_path() {
local profile="$1" manual="$2"
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 "$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" connector_override
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
(
cd "$directory"
"$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 generator and preflight 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"
verify_documented_operator_path server "$server_dir"
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"
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_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"