generated from coulomb/repo-seed
370 lines
9.0 KiB
Bash
Executable File
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 "$@"
|