Files
state-hub/scripts/bootstrap-env.sh

370 lines
9.0 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
STATE_HUB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
GITEA_CONF="${GITEA_CONF:-$HOME/.railiance_gitea.conf}"
GITEA_URL="${GITEA_URL:-http://92.205.130.254:32166}"
GITEA_USER="${GITEA_USER:-}"
GITEA_TOKEN="${GITEA_TOKEN:-}"
GIT_HELPER="${GIT_HELPER:-auto}"
INSTALL_MISSING=0
NON_INTERACTIVE=0
DRY_RUN=0
AUTHORIZE_SSH=0
ALLOW_PLAINTEXT_STORE=0
SKIP_GITEA=0
SKIP_MCP=0
SSH_KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519}"
SSH_TARGETS=(
"tegwick@92.205.62.239"
"tegwick@92.205.130.254"
)
usage() {
cat <<'USAGE'
Usage: scripts/bootstrap-env.sh [options]
Idempotently prepares a State Hub operator or collaborator environment.
Options:
--install-missing Install missing apt packages when possible.
--non-interactive Do not prompt; warn instead of asking for secrets.
--dry-run Show intended actions without changing local config.
--git-helper MODE auto, libsecret, cache, or store. Default: auto.
--allow-plaintext-store Allow git credential.helper=store in auto mode.
--authorize-ssh Run ssh-copy-id for configured SSH targets.
--ssh-target USER@HOST Add an SSH authorization target. May repeat.
--gitea-url URL Gitea base URL for ~/.railiance_gitea.conf.
--gitea-user USER Gitea user for ~/.railiance_gitea.conf.
--gitea-token TOKEN Gitea token; otherwise prompted when interactive.
--skip-gitea Do not create or update ~/.railiance_gitea.conf.
--skip-mcp Do not run make register-mcp.
-h, --help Show this help.
USAGE
}
ok() { printf '[OK] %s\n' "$*"; }
warn() { printf '[WARN] %s\n' "$*"; }
err() { printf '[ERR] %s\n' "$*" >&2; }
step() { printf '\n==> %s\n' "$*"; }
run() {
if [ "$DRY_RUN" -eq 1 ]; then
printf 'DRY-RUN: %s\n' "$*"
else
"$@"
fi
}
need_arg() {
if [ -z "${2:-}" ]; then
err "$1 requires a value"
exit 2
fi
}
while [ "$#" -gt 0 ]; do
case "$1" in
--install-missing)
INSTALL_MISSING=1
shift
;;
--non-interactive)
NON_INTERACTIVE=1
shift
;;
--dry-run)
DRY_RUN=1
shift
;;
--git-helper)
need_arg "$1" "${2:-}"
GIT_HELPER="$2"
shift 2
;;
--allow-plaintext-store)
ALLOW_PLAINTEXT_STORE=1
shift
;;
--authorize-ssh)
AUTHORIZE_SSH=1
shift
;;
--ssh-target)
need_arg "$1" "${2:-}"
SSH_TARGETS+=("$2")
shift 2
;;
--gitea-url)
need_arg "$1" "${2:-}"
GITEA_URL="$2"
shift 2
;;
--gitea-user)
need_arg "$1" "${2:-}"
GITEA_USER="$2"
shift 2
;;
--gitea-token)
need_arg "$1" "${2:-}"
GITEA_TOKEN="$2"
shift 2
;;
--skip-gitea)
SKIP_GITEA=1
shift
;;
--skip-mcp)
SKIP_MCP=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
err "unknown argument: $1"
usage >&2
exit 2
;;
esac
done
case "$GIT_HELPER" in
auto|libsecret|cache|store) ;;
*)
err "--git-helper must be auto, libsecret, cache, or store"
exit 2
;;
esac
apt_install() {
local packages=("$@")
if [ "$INSTALL_MISSING" -ne 1 ]; then
warn "Missing packages: ${packages[*]}"
warn "Rerun with --install-missing or install them manually."
return
fi
if ! command -v sudo >/dev/null 2>&1; then
warn "sudo is not available; cannot install: ${packages[*]}"
return
fi
run sudo apt-get update
run sudo apt-get install -y "${packages[@]}"
}
check_commands() {
step "Checking prerequisites"
local missing=()
local commands=(git curl ssh-keygen ssh-copy-id python3 make)
local optional=(sops age helm kubectl uv claude)
for cmd in "${commands[@]}"; do
if command -v "$cmd" >/dev/null 2>&1; then
ok "$cmd found"
else
missing+=("$cmd")
warn "$cmd missing"
fi
done
for cmd in "${optional[@]}"; do
if command -v "$cmd" >/dev/null 2>&1; then
ok "$cmd found"
else
warn "$cmd missing"
fi
done
if [ "${#missing[@]}" -gt 0 ]; then
apt_install "${missing[@]}"
fi
}
libsecret_helper_path() {
local candidates=(
"/usr/share/doc/git/contrib/credential/libsecret/git-credential-libsecret"
"/usr/lib/git-core/git-credential-libsecret"
"/usr/libexec/git-core/git-credential-libsecret"
)
local candidate
for candidate in "${candidates[@]}"; do
if [ -x "$candidate" ]; then
printf '%s\n' "$candidate"
return 0
fi
done
return 1
}
build_libsecret_helper() {
local source_dir="/usr/share/doc/git/contrib/credential/libsecret"
if [ ! -d "$source_dir" ]; then
apt_install libsecret-1-0 libsecret-1-dev make gcc
fi
if [ -d "$source_dir" ]; then
run sudo make -C "$source_dir"
fi
}
configure_git_helper() {
step "Configuring Git credential helper"
local current
current="$(git config --global --get credential.helper || true)"
if [ -n "$current" ]; then
ok "credential.helper already set: $current"
return
fi
local helper="$GIT_HELPER"
if [ "$helper" = "auto" ]; then
if libsecret_helper_path >/dev/null 2>&1; then
helper="libsecret"
elif [ "$ALLOW_PLAINTEXT_STORE" -eq 1 ]; then
helper="store"
else
helper="cache"
fi
fi
case "$helper" in
libsecret)
local path
path="$(libsecret_helper_path || true)"
if [ -z "$path" ]; then
build_libsecret_helper
path="$(libsecret_helper_path || true)"
fi
if [ -z "$path" ]; then
warn "libsecret helper is not available; using cache helper for this machine."
run git config --global credential.helper "cache --timeout=3600"
else
run git config --global credential.helper "$path"
fi
;;
cache)
run git config --global credential.helper "cache --timeout=3600"
;;
store)
if [ "$ALLOW_PLAINTEXT_STORE" -ne 1 ]; then
err "credential.helper=store writes plaintext credentials."
err "Rerun with --allow-plaintext-store if that is intended for this host."
exit 1
fi
run git config --global credential.helper store
;;
esac
ok "credential.helper configured"
}
setup_ssh_key() {
step "Checking SSH key"
mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
if [ -f "$SSH_KEY" ]; then
ok "SSH key exists: $SSH_KEY"
else
run ssh-keygen -t ed25519 -f "$SSH_KEY" -N "" -C "$USER@$(hostname)-state-hub"
ok "SSH key generated: $SSH_KEY"
fi
if [ -f "${SSH_KEY}.pub" ]; then
printf '\nPublic key to authorize on managed hosts:\n\n'
sed 's/^/ /' "${SSH_KEY}.pub"
printf '\n'
fi
if [ "$AUTHORIZE_SSH" -eq 1 ]; then
local target
for target in "${SSH_TARGETS[@]}"; do
run ssh-copy-id -i "${SSH_KEY}.pub" "$target"
done
else
warn "SSH authorization not attempted. Use --authorize-ssh after confirming host access."
fi
}
write_gitea_conf() {
step "Checking Gitea config"
if [ "$SKIP_GITEA" -eq 1 ]; then
warn "Skipping Gitea config by request."
return
fi
if [ -f "$GITEA_CONF" ]; then
chmod 600 "$GITEA_CONF"
ok "$GITEA_CONF already exists"
return
fi
if [ -z "$GITEA_USER" ] && [ "$NON_INTERACTIVE" -eq 0 ]; then
read -r -p "Gitea username: " GITEA_USER
fi
if [ -z "$GITEA_TOKEN" ] && [ "$NON_INTERACTIVE" -eq 0 ]; then
read -r -s -p "Gitea token (requires read:user and repository write scopes): " GITEA_TOKEN
printf '\n'
fi
if [ -z "$GITEA_USER" ] || [ -z "$GITEA_TOKEN" ]; then
warn "Gitea config not written. Set GITEA_USER/GITEA_TOKEN or rerun interactively."
return
fi
if [ "$DRY_RUN" -eq 1 ]; then
printf 'DRY-RUN: would write %s with GITEA_URL and GITEA_USER; token hidden\n' "$GITEA_CONF"
return
fi
umask 077
{
printf 'GITEA_URL="%s"\n' "$GITEA_URL"
printf 'GITEA_USER="%s"\n' "$GITEA_USER"
printf 'GITEA_TOKEN="%s"\n' "$GITEA_TOKEN"
} >"$GITEA_CONF"
chmod 600 "$GITEA_CONF"
ok "Wrote $GITEA_CONF"
}
register_mcp() {
step "Registering State Hub MCP"
if [ "$SKIP_MCP" -eq 1 ]; then
warn "Skipping MCP registration by request."
return
fi
if [ "$DRY_RUN" -eq 1 ]; then
run make -C "$STATE_HUB_DIR" register-mcp DRY_RUN=1
else
make -C "$STATE_HUB_DIR" register-mcp
fi
}
health_check() {
step "Checking State Hub reachability"
if curl -fsS --max-time 2 "http://127.0.0.1:8000/state/health" >/dev/null 2>&1; then
ok "State Hub API reachable at http://127.0.0.1:8000"
elif curl -fsS --max-time 2 "http://127.0.0.1:18000/state/health" >/dev/null 2>&1; then
ok "State Hub API reachable through tunnel at http://127.0.0.1:18000"
else
warn "State Hub API is not reachable locally or through the default tunnel."
warn "Start it with 'make api' or run 'make bridges' if this machine uses ops-bridge."
fi
}
main() {
step "State Hub environment bootstrap"
printf 'Repository: %s\n' "$STATE_HUB_DIR"
check_commands
configure_git_helper
setup_ssh_key
write_gitea_conf
register_mcp
health_check
ok "Bootstrap checks complete."
}
main "$@"