Add a fedwiki-render compose service and render.sh to resolve real Keycloak user UUIDs and render .tpl templates into testdata on compose up. Convert hardcoded FedWiki testdata into templates, add seed-stack.sh helper, and update compose/env and .gitignore to run seeding before starting fedwiki.
255 lines
7.8 KiB
Bash
Executable File
255 lines
7.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Bootstraps an isolated test stack for the current git worktree.
|
|
#
|
|
# - Derives a slug from the worktree directory (or $1 if given).
|
|
# - Allocates a deterministic port slot via FNV-32(slug) % 200, advancing on
|
|
# collisions until a free range is found.
|
|
# - Copies test/secrets/ from the main worktree if missing locally.
|
|
# - Writes test/.env with COMPOSE_PROJECT_NAME, host port assignments, and
|
|
# MC_* env overrides for the application's Viper config.
|
|
# - Idempotent: if test/.env exists, prints the summary and exits 0.
|
|
#
|
|
# Usage:
|
|
# ./test/bootstrap-stack.sh # slug = basename of worktree
|
|
# ./test/bootstrap-stack.sh my-slug # explicit slug
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
TEST_DIR="${REPO_ROOT}/test"
|
|
ENV_FILE="${TEST_DIR}/.env"
|
|
SECRETS_DIR="${TEST_DIR}/secrets"
|
|
|
|
SLOT_COUNT=200
|
|
PORT_STEP=50
|
|
|
|
# ---------- helpers ----------
|
|
|
|
print_summary() {
|
|
# Prints the URL/port table from a sourced .env. Caller must have sourced it.
|
|
local slot="${SLOT:-?}"
|
|
cat <<EOF
|
|
|
|
=== Test stack ready ===
|
|
Stack: ${COMPOSE_PROJECT_NAME}
|
|
Slot: ${slot}
|
|
|
|
Member Console: ${MC_BASE_URL}
|
|
Keycloak: http://${KC_HOSTNAME}:${KEYCLOAK_PORT}
|
|
Temporal UI: ${TEMPORAL_UI_URL}
|
|
Temporal gRPC: localhost:${TEMPORAL_PORT}
|
|
FedWiki: http://localhost:${FEDWIKI_PORT}
|
|
FedWiki API: ${MC_FEDWIKI_FARM_API_URL}
|
|
|
|
Postgres: localhost:${POSTGRES_PORT} (member_console / member_console)
|
|
Valkey: localhost:${VALKEY_PORT}
|
|
|
|
Next steps (run from test/):
|
|
docker compose up -d
|
|
set -a; . .env; set +a
|
|
go run .. start --config mc-config.yaml
|
|
|
|
EOF
|
|
}
|
|
|
|
fnv32() {
|
|
# FNV-1a 32-bit hash, pure shell. Output: decimal integer.
|
|
local s="$1"
|
|
local h=2166136261
|
|
local i c
|
|
for ((i = 0; i < ${#s}; i++)); do
|
|
c=$(printf '%d' "'${s:i:1}")
|
|
h=$(( (h ^ c) & 0xFFFFFFFF ))
|
|
h=$(( (h * 16777619) & 0xFFFFFFFF ))
|
|
done
|
|
echo "$h"
|
|
}
|
|
|
|
port_in_use() {
|
|
# Returns 0 if any process is listening on the given TCP port.
|
|
if command -v ss >/dev/null 2>&1; then
|
|
ss -ltn "( sport = :$1 )" 2>/dev/null | grep -q LISTEN
|
|
elif command -v lsof >/dev/null 2>&1; then
|
|
lsof -iTCP:"$1" -sTCP:LISTEN -n -P >/dev/null 2>&1
|
|
else
|
|
# Fallback: bash /dev/tcp probe (returns 0 if connect succeeds)
|
|
(echo > "/dev/tcp/127.0.0.1/$1") >/dev/null 2>&1
|
|
fi
|
|
}
|
|
|
|
slot_ports_free() {
|
|
local slot="$1"
|
|
local p
|
|
for p in \
|
|
$((5432 + slot * PORT_STEP)) \
|
|
$((6379 + slot * PORT_STEP)) \
|
|
$((8080 + slot * PORT_STEP)) \
|
|
$((7233 + slot * PORT_STEP)) \
|
|
$((8233 + slot * PORT_STEP)) \
|
|
$((8081 + slot * PORT_STEP)); do
|
|
if port_in_use "$p"; then
|
|
return 1
|
|
fi
|
|
done
|
|
return 0
|
|
}
|
|
|
|
find_main_worktree() {
|
|
# First entry of `git worktree list --porcelain` is the main worktree.
|
|
git worktree list --porcelain | awk '/^worktree / { print $2; exit }'
|
|
}
|
|
|
|
copy_secrets_if_missing() {
|
|
if [[ -d "$SECRETS_DIR" ]] && [[ -n "$(ls -A "$SECRETS_DIR" 2>/dev/null)" ]]; then
|
|
return 0
|
|
fi
|
|
local main
|
|
main="$(find_main_worktree)"
|
|
if [[ -z "$main" || "$main" == "$REPO_ROOT" ]]; then
|
|
cat >&2 <<EOF
|
|
ERROR: test/secrets/ is missing in this worktree and could not be copied
|
|
from a main worktree (this appears to be the main worktree itself,
|
|
or no main worktree was found).
|
|
|
|
Expected files:
|
|
test/secrets/stripe-api-key
|
|
test/secrets/stripe-webhook-secret
|
|
|
|
Obtain Stripe test-mode credentials from the team and place them in
|
|
test/secrets/ before running bootstrap again.
|
|
EOF
|
|
return 1
|
|
fi
|
|
local src="${main}/test/secrets"
|
|
if [[ ! -d "$src" ]] || [[ -z "$(ls -A "$src" 2>/dev/null)" ]]; then
|
|
cat >&2 <<EOF
|
|
ERROR: Source main worktree at ${main} has no test/secrets/ directory
|
|
(or it is empty). Cannot propagate credentials.
|
|
|
|
Populate ${src}/ first, then re-run bootstrap.
|
|
EOF
|
|
return 1
|
|
fi
|
|
echo "Copying test/secrets/ from ${src}..."
|
|
mkdir -p "$SECRETS_DIR"
|
|
cp -r "${src}/." "${SECRETS_DIR}/"
|
|
}
|
|
|
|
# ---------- idempotent re-run ----------
|
|
|
|
if [[ -f "$ENV_FILE" ]]; then
|
|
echo "test/.env already exists — using existing port assignments."
|
|
# shellcheck disable=SC1090
|
|
set -a; . "$ENV_FILE"; set +a
|
|
print_summary
|
|
exit 0
|
|
fi
|
|
|
|
# ---------- slug + slot ----------
|
|
|
|
derive_slug() {
|
|
# If basename of the worktree is the generic "member-console" (the case for
|
|
# the main worktree and for `git worktree add` invocations that don't pass a
|
|
# custom path), prefer the parent directory name when it looks like a
|
|
# worktree-specific label. Falls back to the branch name, then basename.
|
|
local base parent branch
|
|
base="$(basename "$REPO_ROOT")"
|
|
parent="$(basename "$(dirname "$REPO_ROOT")")"
|
|
if [[ "$base" != "member-console" ]]; then
|
|
echo "$base"; return
|
|
fi
|
|
if [[ "$parent" != "member-console" && "$parent" != "Repos" && "$parent" != "worktrees" && -n "$parent" ]]; then
|
|
echo "mc-${parent}"; return
|
|
fi
|
|
branch="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")"
|
|
if [[ -n "$branch" && "$branch" != "HEAD" && "$branch" != "main" && "$branch" != "master" ]]; then
|
|
echo "mc-${branch//\//-}"; return
|
|
fi
|
|
echo "$base"
|
|
}
|
|
|
|
SLUG="${1:-$(derive_slug)}"
|
|
|
|
H="$(fnv32 "$SLUG")"
|
|
START_SLOT=$(( H % SLOT_COUNT ))
|
|
SLOT="$START_SLOT"
|
|
|
|
attempts=0
|
|
while ! slot_ports_free "$SLOT"; do
|
|
SLOT=$(( (SLOT + 1) % SLOT_COUNT ))
|
|
attempts=$(( attempts + 1 ))
|
|
if (( attempts >= SLOT_COUNT )); then
|
|
echo "ERROR: no free port slot found after scanning all ${SLOT_COUNT} slots" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
if (( SLOT != START_SLOT )); then
|
|
echo "Slot ${START_SLOT} ports were in use; advanced to slot ${SLOT}."
|
|
fi
|
|
|
|
# ---------- compute ports ----------
|
|
|
|
POSTGRES_PORT=$(( 5432 + SLOT * PORT_STEP ))
|
|
VALKEY_PORT=$(( 6379 + SLOT * PORT_STEP ))
|
|
KEYCLOAK_PORT=$(( 8080 + SLOT * PORT_STEP ))
|
|
TEMPORAL_PORT=$(( 7233 + SLOT * PORT_STEP ))
|
|
TEMPORAL_UI_PORT=$(( 8233 + SLOT * PORT_STEP ))
|
|
MC_PORT=$(( 8081 + SLOT * PORT_STEP ))
|
|
# FedWiki defaults to host port 80; offset onto the high range.
|
|
FEDWIKI_PORT=$(( 8090 + SLOT * PORT_STEP ))
|
|
|
|
# Per-stack hostnames — required for cookie isolation across stacks.
|
|
# Cookies are scoped per domain (not per port; RFC 6265), so two stacks on
|
|
# different localhost ports would share one cookie jar and clobber each
|
|
# other's sessions. *.localhost resolves to loopback per RFC 6761.
|
|
KC_HOSTNAME="keycloak-${SLUG}.localhost"
|
|
MC_HOSTNAME="${SLUG}.localhost"
|
|
TEMPORAL_UI_HOSTNAME="temporal-ui-${SLUG}.localhost"
|
|
MC_BASE_URL="http://${MC_HOSTNAME}:${MC_PORT}"
|
|
TEMPORAL_UI_URL="http://${TEMPORAL_UI_HOSTNAME}:${TEMPORAL_UI_PORT}"
|
|
|
|
# ---------- secrets ----------
|
|
|
|
copy_secrets_if_missing
|
|
|
|
# ---------- write .env ----------
|
|
|
|
cat > "$ENV_FILE" <<EOF
|
|
# Generated by test/bootstrap-stack.sh on $(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
# Slug: ${SLUG} Slot: ${SLOT}
|
|
# Re-run bootstrap to refresh; teardown removes this file.
|
|
|
|
COMPOSE_PROJECT_NAME=${SLUG}
|
|
SLOT=${SLOT}
|
|
|
|
# Host port bindings (consumed by test/compose.yaml)
|
|
POSTGRES_PORT=${POSTGRES_PORT}
|
|
VALKEY_PORT=${VALKEY_PORT}
|
|
KEYCLOAK_PORT=${KEYCLOAK_PORT}
|
|
TEMPORAL_PORT=${TEMPORAL_PORT}
|
|
TEMPORAL_UI_PORT=${TEMPORAL_UI_PORT}
|
|
FEDWIKI_PORT=${FEDWIKI_PORT}
|
|
|
|
# Per-stack URLs (consumed by compose + Keycloak seed)
|
|
KC_HOSTNAME=${KC_HOSTNAME}
|
|
MC_BASE_URL=${MC_BASE_URL}
|
|
TEMPORAL_UI_URL=${TEMPORAL_UI_URL}
|
|
|
|
# Viper overrides for mc-config.yaml (MC_ prefix, dash → underscore)
|
|
MC_PORT=${MC_PORT}
|
|
MC_BASE_URL=${MC_BASE_URL}
|
|
MC_DB_DSN=postgres://member_console:member_console@localhost:${POSTGRES_PORT}/member_console?sslmode=disable
|
|
MC_VALKEY_ADDR=localhost:${VALKEY_PORT}
|
|
MC_OIDC_IDP_ISSUER_URL=http://${KC_HOSTNAME}:${KEYCLOAK_PORT}/realms/master
|
|
MC_TEMPORAL_HOST=localhost:${TEMPORAL_PORT}
|
|
MC_TEMPORAL_OAUTH_TOKEN_URL=http://${KC_HOSTNAME}:${KEYCLOAK_PORT}/realms/master/protocol/openid-connect/token
|
|
MC_FEDWIKI_FARM_API_URL=http://admin.localtest.me:${FEDWIKI_PORT}
|
|
EOF
|
|
|
|
echo "Wrote ${ENV_FILE}."
|
|
|
|
# shellcheck disable=SC1090
|
|
set -a; . "$ENV_FILE"; set +a
|
|
print_summary
|