696 lines
22 KiB
Bash
696 lines
22 KiB
Bash
#!/usr/bin/env bash
|
|
# Automated developer environment setup for Stella Ops (Linux/macOS).
|
|
#
|
|
# Usage:
|
|
# ./scripts/setup.sh [--skip-build] [--infra-only] [--images-only] [--skip-images]
|
|
set -euo pipefail
|
|
|
|
# ─── Parse flags ────────────────────────────────────────────────────────────
|
|
|
|
SKIP_BUILD=false
|
|
INFRA_ONLY=false
|
|
IMAGES_ONLY=false
|
|
SKIP_IMAGES=false
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--skip-build) SKIP_BUILD=true ;;
|
|
--infra-only) INFRA_ONLY=true ;;
|
|
--images-only) IMAGES_ONLY=true ;;
|
|
--skip-images) SKIP_IMAGES=true ;;
|
|
-h|--help)
|
|
echo "Usage: $0 [--skip-build] [--infra-only] [--images-only] [--skip-images]"
|
|
exit 0
|
|
;;
|
|
*) echo "Unknown flag: $arg" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
|
if [[ -z "$ROOT" ]]; then
|
|
echo "ERROR: Not inside a git repository." >&2
|
|
exit 1
|
|
fi
|
|
|
|
COMPOSE_DIR="${ROOT}/devops/compose"
|
|
|
|
# ─── Helpers ────────────────────────────────────────────────────────────────
|
|
|
|
step() { printf '\n\033[1;36m>> %s\033[0m\n' "$1"; }
|
|
ok() { printf ' \033[0;32m[OK]\033[0m %s\n' "$1"; }
|
|
warn() { printf ' \033[0;33m[WARN]\033[0m %s\n' "$1"; }
|
|
fail() { printf ' \033[0;31m[FAIL]\033[0m %s\n' "$1"; }
|
|
|
|
has_cmd() { command -v "$1" &>/dev/null; }
|
|
|
|
get_running_container_by_service() {
|
|
local service="$1"
|
|
docker ps --filter "label=com.docker.compose.service=${service}" --format "{{.Names}}" 2>/dev/null | head -n1
|
|
}
|
|
|
|
service_http_probe_url() {
|
|
local service="$1"
|
|
local container_port="$2"
|
|
local path="${3:-/}"
|
|
local container mapping host host_port
|
|
|
|
container=$(get_running_container_by_service "$service")
|
|
[[ -z "$container" ]] && return 1
|
|
|
|
mapping=$(docker port "$container" "${container_port}/tcp" 2>/dev/null | head -n1)
|
|
[[ -z "$mapping" ]] && return 1
|
|
|
|
host="${mapping%:*}"
|
|
host_port="${mapping##*:}"
|
|
if [[ "$host" == "0.0.0.0" || "$host" == "::" ]]; then
|
|
host="127.0.0.1"
|
|
fi
|
|
|
|
[[ "$path" != /* ]] && path="/$path"
|
|
printf 'http://%s:%s%s' "$host" "$host_port" "$path"
|
|
}
|
|
|
|
get_compose_service_records() {
|
|
local seen_names=""
|
|
local compose_file compose_path expected_services services_json line service name state health
|
|
|
|
for compose_file in "$@"; do
|
|
if [[ "${compose_file}" = /* ]]; then
|
|
compose_path="${compose_file}"
|
|
else
|
|
compose_path="${COMPOSE_DIR}/${compose_file}"
|
|
fi
|
|
|
|
[[ -f "${compose_path}" ]] || continue
|
|
|
|
expected_services="$(docker compose -f "${compose_path}" config --services 2>/dev/null || true)"
|
|
services_json="$(docker compose -f "${compose_path}" ps --format json 2>/dev/null || true)"
|
|
[[ -n "${services_json}" ]] || continue
|
|
|
|
while IFS= read -r line; do
|
|
[[ -z "${line}" ]] && continue
|
|
service=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('Service',''))" 2>/dev/null || true)
|
|
name=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('Name',''))" 2>/dev/null || true)
|
|
state=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('State',''))" 2>/dev/null || true)
|
|
health=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('Health',''))" 2>/dev/null || true)
|
|
|
|
if [[ -n "${expected_services}" ]] && ! printf '%s\n' "${expected_services}" | grep -Fxq "${service}"; then
|
|
continue
|
|
fi
|
|
|
|
[[ -n "${name}" ]] || continue
|
|
if printf '%s\n' "${seen_names}" | grep -Fxq "${name}"; then
|
|
continue
|
|
fi
|
|
|
|
seen_names="${seen_names}
|
|
${name}"
|
|
printf '%s|%s|%s|%s|%s\n' "${compose_path}" "${service}" "${name}" "${state,,}" "${health,,}"
|
|
done <<< "${services_json}"
|
|
done
|
|
}
|
|
|
|
wait_for_compose_convergence() {
|
|
local success_message="$1"
|
|
local restart_stalled="${2:-false}"
|
|
local max_wait="${3:-180}"
|
|
local restart_after="${4:-45}"
|
|
shift 4
|
|
local compose_files=("$@")
|
|
|
|
local elapsed=0
|
|
local poll_seconds=5
|
|
local restarted_services=""
|
|
|
|
while (( elapsed < max_wait )); do
|
|
local records
|
|
records="$(get_compose_service_records "${compose_files[@]}")"
|
|
if [[ -n "${records}" ]]; then
|
|
local pending=""
|
|
local blocking=""
|
|
local record compose_file service name state health
|
|
|
|
while IFS= read -r record; do
|
|
[[ -z "${record}" ]] && continue
|
|
IFS='|' read -r compose_file service name state health <<< "${record}"
|
|
|
|
if [[ "${state}" != "running" ]]; then
|
|
blocking="${blocking}
|
|
${compose_file}|${service}|${name}|state=${state}"
|
|
continue
|
|
fi
|
|
|
|
if [[ -z "${health}" || "${health}" == "healthy" ]]; then
|
|
continue
|
|
fi
|
|
|
|
if [[ "${health}" == "starting" ]]; then
|
|
pending="${pending}
|
|
${compose_file}|${service}|${name}|health=starting"
|
|
continue
|
|
fi
|
|
|
|
blocking="${blocking}
|
|
${compose_file}|${service}|${name}|health=${health}"
|
|
done <<< "${records}"
|
|
|
|
if [[ -z "${blocking//$'\n'/}" && -z "${pending//$'\n'/}" && ${elapsed} -gt ${poll_seconds} ]]; then
|
|
ok "${success_message}"
|
|
return 0
|
|
fi
|
|
|
|
if [[ "${restart_stalled}" == "true" && ${elapsed} -ge ${restart_after} && -n "${blocking//$'\n'/}" ]]; then
|
|
local restart_targets=""
|
|
while IFS= read -r record; do
|
|
[[ -z "${record}" ]] && continue
|
|
IFS='|' read -r compose_file service _ <<< "${record}"
|
|
local restart_key="${compose_file}|${service}"
|
|
if printf '%s\n' "${restarted_services}" | grep -Fxq "${restart_key}"; then
|
|
continue
|
|
fi
|
|
|
|
restarted_services="${restarted_services}
|
|
${restart_key}"
|
|
restart_targets="${restart_targets}
|
|
${compose_file}|${service}"
|
|
done <<< "${blocking}"
|
|
|
|
local compose_to_restart unique_compose_files service_to_restart
|
|
local -a services_for_compose
|
|
unique_compose_files=$(printf '%s\n' "${restart_targets}" | awk -F'|' 'NF { print $1 }' | sort -u)
|
|
|
|
while IFS= read -r compose_to_restart; do
|
|
[[ -z "${compose_to_restart}" ]] && continue
|
|
services_for_compose=()
|
|
while IFS= read -r service_to_restart; do
|
|
[[ -z "${service_to_restart}" ]] && continue
|
|
services_for_compose+=("${service_to_restart}")
|
|
done < <(printf '%s\n' "${restart_targets}" | awk -F'|' -v cf="${compose_to_restart}" 'NF && $1 == cf { print $2 }' | sort -u)
|
|
|
|
if (( ${#services_for_compose[@]} == 0 )); then
|
|
continue
|
|
fi
|
|
|
|
warn "Restarting stalled services from ${compose_to_restart}: ${services_for_compose[*]}"
|
|
(
|
|
cd "${COMPOSE_DIR}" &&
|
|
docker compose -f "${compose_to_restart}" restart "${services_for_compose[@]}" >/dev/null
|
|
) && ok "Restarted stalled services: ${services_for_compose[*]}" || \
|
|
warn "Failed to restart stalled services: ${services_for_compose[*]}"
|
|
done <<< "${unique_compose_files}"
|
|
fi
|
|
fi
|
|
|
|
sleep "${poll_seconds}"
|
|
elapsed=$((elapsed + poll_seconds))
|
|
done
|
|
|
|
local final_records
|
|
final_records="$(get_compose_service_records "${compose_files[@]}")"
|
|
local final_blocking=""
|
|
local final_pending=""
|
|
local record compose_file service name state health
|
|
|
|
while IFS= read -r record; do
|
|
[[ -z "${record}" ]] && continue
|
|
IFS='|' read -r compose_file service name state health <<< "${record}"
|
|
|
|
if [[ "${state}" != "running" ]]; then
|
|
final_blocking="${final_blocking}
|
|
${name} (state=${state})"
|
|
continue
|
|
fi
|
|
|
|
if [[ -n "${health}" && "${health}" != "healthy" ]]; then
|
|
if [[ "${health}" == "starting" ]]; then
|
|
final_pending="${final_pending}
|
|
${name} (health=starting)"
|
|
else
|
|
final_blocking="${final_blocking}
|
|
${name} (health=${health})"
|
|
fi
|
|
fi
|
|
done <<< "${final_records}"
|
|
|
|
if [[ -n "${final_blocking//$'\n'/}" ]]; then
|
|
warn "Timed out waiting for compose convergence after ${max_wait}s. Blocking services: $(printf '%s\n' "${final_blocking}" | awk 'NF { print }' | paste -sd ', ' -)"
|
|
elif [[ -n "${final_pending//$'\n'/}" ]]; then
|
|
warn "Timed out waiting for compose convergence after ${max_wait}s. Still starting: $(printf '%s\n' "${final_pending}" | awk 'NF { print }' | paste -sd ', ' -)"
|
|
else
|
|
warn "Timed out waiting for compose convergence after ${max_wait}s."
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# ─── 1. Check prerequisites ────────────────────────────────────────────────
|
|
|
|
check_prerequisites() {
|
|
step 'Checking prerequisites'
|
|
local all_good=true
|
|
|
|
# dotnet
|
|
if has_cmd dotnet; then
|
|
local v; v=$(dotnet --version 2>/dev/null)
|
|
if [[ "$v" =~ ^10\. ]]; then
|
|
ok "dotnet $v"
|
|
else
|
|
fail "dotnet $v found, but 10.x is required"
|
|
all_good=false
|
|
fi
|
|
else
|
|
fail 'dotnet SDK not found. Install .NET 10 SDK.'
|
|
all_good=false
|
|
fi
|
|
|
|
# node
|
|
if has_cmd node; then
|
|
local v; v=$(node --version 2>/dev/null | sed 's/^v//')
|
|
local major; major=$(echo "$v" | cut -d. -f1)
|
|
if (( major >= 20 )); then
|
|
ok "node $v"
|
|
else
|
|
fail "node $v found, but 20+ is required"
|
|
all_good=false
|
|
fi
|
|
else
|
|
fail 'node not found. Install Node.js 20+.'
|
|
all_good=false
|
|
fi
|
|
|
|
# npm
|
|
if has_cmd npm; then
|
|
local v; v=$(npm --version 2>/dev/null)
|
|
local major; major=$(echo "$v" | cut -d. -f1)
|
|
if (( major >= 10 )); then
|
|
ok "npm $v"
|
|
else
|
|
fail "npm $v found, but 10+ is required"
|
|
all_good=false
|
|
fi
|
|
else
|
|
fail 'npm not found.'
|
|
all_good=false
|
|
fi
|
|
|
|
# docker
|
|
if has_cmd docker; then
|
|
ok "docker: $(docker --version 2>/dev/null)"
|
|
else
|
|
fail 'docker not found. Install Docker.'
|
|
all_good=false
|
|
fi
|
|
|
|
# docker compose
|
|
if docker compose version &>/dev/null; then
|
|
ok 'docker compose available'
|
|
else
|
|
fail 'docker compose not available. Install Compose V2.'
|
|
all_good=false
|
|
fi
|
|
|
|
# git
|
|
if has_cmd git; then
|
|
ok "$(git --version 2>/dev/null)"
|
|
else
|
|
fail 'git not found.'
|
|
all_good=false
|
|
fi
|
|
|
|
if [[ "$all_good" != "true" ]]; then
|
|
echo 'ERROR: Prerequisites not met. Install missing tools and re-run.' >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# ─── 2. Check and install hosts file ─────────────────────────────────────
|
|
|
|
check_hosts() {
|
|
step 'Checking hosts file for stella-ops.local entries'
|
|
local hosts_source="${ROOT}/devops/compose/hosts.stellaops.local"
|
|
|
|
if grep -q 'stella-ops\.local' /etc/hosts 2>/dev/null; then
|
|
ok 'stella-ops.local entries found in /etc/hosts'
|
|
return
|
|
fi
|
|
|
|
warn 'stella-ops.local entries NOT found in /etc/hosts.'
|
|
|
|
if [[ ! -f "$hosts_source" ]]; then
|
|
warn "Hosts source file not found at $hosts_source"
|
|
echo ' Add the hosts block from docs/dev/DEV_ENVIRONMENT_SETUP.md section 2'
|
|
echo ' to /etc/hosts (use sudo).'
|
|
return
|
|
fi
|
|
|
|
echo ''
|
|
echo ' Stella Ops needs ~50 hosts file entries for local development.'
|
|
echo " Source: devops/compose/hosts.stellaops.local"
|
|
echo ''
|
|
printf ' Add entries to /etc/hosts now? (Y/n) '
|
|
read -r answer
|
|
|
|
if [[ -z "$answer" || "$answer" =~ ^[Yy] ]]; then
|
|
if [[ "$(id -u)" -eq 0 ]]; then
|
|
printf '\n' >> /etc/hosts
|
|
cat "$hosts_source" >> /etc/hosts
|
|
ok 'Hosts entries added successfully'
|
|
else
|
|
echo ''
|
|
echo ' Adding hosts entries requires sudo...'
|
|
if sudo sh -c "printf '\n' >> /etc/hosts && cat '$hosts_source' >> /etc/hosts"; then
|
|
ok 'Hosts entries added successfully'
|
|
else
|
|
warn 'Failed to add hosts entries. Add them manually:'
|
|
echo " sudo sh -c 'cat $hosts_source >> /etc/hosts'"
|
|
fi
|
|
fi
|
|
else
|
|
warn 'Skipped. Add them manually before accessing the platform:'
|
|
echo " sudo sh -c 'cat $hosts_source >> /etc/hosts'"
|
|
fi
|
|
}
|
|
|
|
# ─── 3. Ensure .env ────────────────────────────────────────────────────────
|
|
|
|
ensure_env() {
|
|
step 'Ensuring .env file exists'
|
|
local env_file="${COMPOSE_DIR}/.env"
|
|
local env_example="${COMPOSE_DIR}/env/stellaops.env.example"
|
|
|
|
if [[ -f "$env_file" ]]; then
|
|
ok ".env already exists at $env_file"
|
|
elif [[ -f "$env_example" ]]; then
|
|
cp "$env_example" "$env_file"
|
|
ok "Copied $env_example -> $env_file"
|
|
warn 'For production, change POSTGRES_PASSWORD in .env.'
|
|
else
|
|
fail "Neither .env nor env/stellaops.env.example found in $COMPOSE_DIR"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
get_compose_env_value() {
|
|
local key="$1"
|
|
local env_file="${COMPOSE_DIR}/.env"
|
|
[[ -f "$env_file" ]] || return 1
|
|
|
|
awk -F= -v key="$key" '$1 == key { print substr($0, index($0, "=") + 1); exit }' "$env_file"
|
|
}
|
|
|
|
get_frontdoor_network_name() {
|
|
if [[ -n "${FRONTDOOR_NETWORK:-}" ]]; then
|
|
printf '%s\n' "$FRONTDOOR_NETWORK"
|
|
return
|
|
fi
|
|
|
|
local configured
|
|
configured="$(get_compose_env_value FRONTDOOR_NETWORK || true)"
|
|
if [[ -n "$configured" ]]; then
|
|
printf '%s\n' "$configured"
|
|
return
|
|
fi
|
|
|
|
printf '%s\n' 'stellaops_frontdoor'
|
|
}
|
|
|
|
ensure_frontdoor_network() {
|
|
local network_name
|
|
network_name="$(get_frontdoor_network_name)"
|
|
|
|
if docker network inspect "$network_name" >/dev/null 2>&1; then
|
|
ok "Frontdoor network available ($network_name)"
|
|
return
|
|
fi
|
|
|
|
warn "Frontdoor network missing ($network_name); creating it now."
|
|
docker network create "$network_name" >/dev/null
|
|
ok "Created frontdoor network ($network_name)"
|
|
}
|
|
|
|
# ─── 4. Start infrastructure ───────────────────────────────────────────────
|
|
|
|
start_infra() {
|
|
step 'Starting infrastructure containers (docker-compose.dev.yml)'
|
|
cd "$COMPOSE_DIR"
|
|
|
|
docker compose -f docker-compose.dev.yml up -d
|
|
|
|
echo ' Waiting for containers to become healthy...'
|
|
wait_for_compose_convergence 'All infrastructure containers healthy' false 120 45 docker-compose.dev.yml || true
|
|
cd "$ROOT"
|
|
}
|
|
|
|
# ─── 5. Build .NET solutions ───────────────────────────────────────────────
|
|
|
|
build_solutions() {
|
|
step 'Building all .NET solutions'
|
|
local script="${ROOT}/scripts/build-all-solutions.sh"
|
|
if [[ -x "$script" ]]; then
|
|
"$script" --stop-repo-host-processes
|
|
ok '.NET solutions built successfully'
|
|
elif [[ -f "$script" ]]; then
|
|
bash "$script" --stop-repo-host-processes
|
|
ok '.NET solutions built successfully'
|
|
else
|
|
warn "Build script not found at $script. Skipping .NET build."
|
|
fi
|
|
}
|
|
|
|
# ─── 6. Build Docker images ────────────────────────────────────────────────
|
|
|
|
build_images() {
|
|
local publish_no_restore="${1:-false}"
|
|
step 'Building Docker images'
|
|
local script="${ROOT}/devops/docker/build-all.sh"
|
|
if [[ -x "$script" ]]; then
|
|
PUBLISH_NO_RESTORE="$publish_no_restore" "$script"
|
|
ok 'Docker images built successfully'
|
|
elif [[ -f "$script" ]]; then
|
|
PUBLISH_NO_RESTORE="$publish_no_restore" bash "$script"
|
|
ok 'Docker images built successfully'
|
|
else
|
|
warn "Build script not found at $script. Skipping image build."
|
|
fi
|
|
}
|
|
|
|
# ─── 7. Start full platform ────────────────────────────────────────────────
|
|
|
|
start_platform() {
|
|
step 'Starting full Stella Ops platform'
|
|
ensure_frontdoor_network
|
|
cd "$COMPOSE_DIR"
|
|
docker compose -f docker-compose.stella-ops.yml up -d
|
|
ok 'Platform services started'
|
|
cd "$ROOT"
|
|
wait_for_compose_convergence 'Platform services converged from zero-state startup' true 180 45 docker-compose.stella-ops.yml || true
|
|
}
|
|
|
|
http_status() {
|
|
local url="$1"
|
|
local attempts="${2:-6}"
|
|
local delay_seconds="${3:-2}"
|
|
local status=""
|
|
|
|
for (( attempt=1; attempt<=attempts; attempt++ )); do
|
|
status=$(curl -sk -o /dev/null --connect-timeout 5 -w '%{http_code}' "$url" 2>/dev/null || true)
|
|
if [[ -n "$status" && "$status" != "000" ]]; then
|
|
printf '%s' "$status"
|
|
return 0
|
|
fi
|
|
|
|
if (( attempt < attempts )); then
|
|
sleep "$delay_seconds"
|
|
fi
|
|
done
|
|
|
|
return 0
|
|
}
|
|
|
|
frontdoor_bootstrap_ready() {
|
|
step 'Waiting for frontdoor bootstrap readiness'
|
|
|
|
local probes=(
|
|
"Frontdoor readiness|https://stella-ops.local/health/ready|200"
|
|
"Frontdoor welcome page|https://stella-ops.local/welcome|200"
|
|
"Frontdoor environment settings|https://stella-ops.local/envsettings.json|200"
|
|
"Authority discovery|https://stella-ops.local/.well-known/openid-configuration|200"
|
|
"Authority authorize bootstrap|https://stella-ops.local/connect/authorize?client_id=stella-ops-ui&redirect_uri=https%3A%2F%2Fstella-ops.local%2Fauth%2Fcallback&response_type=code&scope=openid%20profile%20email&state=setup-smoke&nonce=setup-smoke&code_challenge=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&code_challenge_method=S256|200,302,303"
|
|
)
|
|
|
|
local entry name url allowed status matched
|
|
for entry in "${probes[@]}"; do
|
|
IFS='|' read -r name url allowed <<<"$entry"
|
|
status="$(http_status "$url" 24 5)"
|
|
matched=false
|
|
IFS=',' read -ra allowed_codes <<<"$allowed"
|
|
for code in "${allowed_codes[@]}"; do
|
|
if [[ "$status" == "$code" ]]; then
|
|
matched=true
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [[ "$matched" == "true" ]]; then
|
|
ok "$name (HTTP $status)"
|
|
continue
|
|
fi
|
|
|
|
fail "$name did not reach an expected status ($allowed)"
|
|
return 1
|
|
done
|
|
|
|
ok 'Frontdoor bootstrap path is ready for first-user sign-in'
|
|
}
|
|
|
|
# ─── 8. Smoke test ─────────────────────────────────────────────────────────
|
|
|
|
smoke_test() {
|
|
step 'Running smoke tests'
|
|
local has_blocking_failures=false
|
|
|
|
# Infrastructure checks
|
|
if docker exec stellaops-dev-postgres pg_isready -U stellaops &>/dev/null; then
|
|
ok 'PostgreSQL'
|
|
else
|
|
warn 'PostgreSQL not responding'
|
|
has_blocking_failures=true
|
|
fi
|
|
|
|
local pong; pong=$(docker exec stellaops-dev-valkey valkey-cli ping 2>/dev/null || true)
|
|
if [[ "$pong" == "PONG" ]]; then
|
|
ok 'Valkey'
|
|
else
|
|
warn 'Valkey not responding'
|
|
has_blocking_failures=true
|
|
fi
|
|
|
|
local rustfs_url rustfs_status
|
|
rustfs_url=$(service_http_probe_url rustfs 8333 / || true)
|
|
rustfs_status=$(http_status "$rustfs_url")
|
|
if [[ "$rustfs_status" == "200" || "$rustfs_status" == "403" ]]; then
|
|
ok "RustFS S3 endpoint (HTTP $rustfs_status)"
|
|
else
|
|
warn 'RustFS S3 endpoint did not respond with an expected status (wanted 200/403)'
|
|
has_blocking_failures=true
|
|
fi
|
|
|
|
local registry_url registry_status
|
|
registry_url=$(service_http_probe_url registry 5000 /v2/ || true)
|
|
registry_status=$(http_status "$registry_url")
|
|
if [[ "$registry_status" == "200" || "$registry_status" == "401" ]]; then
|
|
ok "Zot registry endpoint (HTTP $registry_status)"
|
|
else
|
|
warn 'Zot registry endpoint did not respond with an expected status (wanted 200/401)'
|
|
has_blocking_failures=true
|
|
fi
|
|
|
|
if [[ "$INFRA_ONLY" != "true" ]]; then
|
|
if ! frontdoor_bootstrap_ready; then
|
|
has_blocking_failures=true
|
|
fi
|
|
fi
|
|
|
|
# Platform container health summary
|
|
step 'Container health summary'
|
|
cd "$COMPOSE_DIR"
|
|
|
|
local total=0
|
|
local healthy=0
|
|
local unhealthy_names=""
|
|
|
|
for cf in docker-compose.dev.yml docker-compose.stella-ops.yml; do
|
|
[[ ! -f "$cf" ]] && continue
|
|
while IFS= read -r line; do
|
|
[[ -z "$line" ]] && continue
|
|
local name; name=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('Name',''))" 2>/dev/null || true)
|
|
local h; h=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('Health',''))" 2>/dev/null || true)
|
|
total=$((total + 1))
|
|
if [[ -z "$h" || "$h" == "healthy" ]]; then
|
|
healthy=$((healthy + 1))
|
|
else
|
|
unhealthy_names="${unhealthy_names} Unhealthy: ${name}\n"
|
|
fi
|
|
done < <(docker compose -f "$cf" ps --format json 2>/dev/null)
|
|
done
|
|
|
|
if (( total > 0 )); then
|
|
if (( healthy == total )); then
|
|
ok "$healthy/$total containers healthy"
|
|
else
|
|
warn "$healthy/$total containers healthy"
|
|
[[ -n "$unhealthy_names" ]] && printf " \033[0;33m%b\033[0m" "$unhealthy_names"
|
|
fi
|
|
fi
|
|
|
|
# Platform endpoint check
|
|
if curl -sk --connect-timeout 5 -o /dev/null -w '' https://stella-ops.local 2>/dev/null; then
|
|
ok 'Platform accessible at https://stella-ops.local'
|
|
elif bash -c "echo >/dev/tcp/stella-ops.local/443" 2>/dev/null; then
|
|
ok 'Platform listening on https://stella-ops.local (TLS handshake pending)'
|
|
else
|
|
warn 'Platform not yet accessible at https://stella-ops.local (may still be starting)'
|
|
has_blocking_failures=true
|
|
fi
|
|
|
|
cd "$ROOT"
|
|
|
|
if [[ "$has_blocking_failures" == "true" ]]; then
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# ─── Main ───────────────────────────────────────────────────────────────────
|
|
|
|
echo '============================================='
|
|
echo ' Stella Ops Developer Environment Setup'
|
|
echo '============================================='
|
|
|
|
check_prerequisites
|
|
check_hosts
|
|
|
|
if [[ "$IMAGES_ONLY" == "true" ]]; then
|
|
build_images false
|
|
echo ''
|
|
echo 'Done (images only).'
|
|
exit 0
|
|
fi
|
|
|
|
ensure_env
|
|
start_infra
|
|
|
|
if [[ "$INFRA_ONLY" == "true" ]]; then
|
|
if ! smoke_test; then
|
|
fail 'Infrastructure setup did not pass blocking smoke tests. Review output and docker compose logs.'
|
|
exit 1
|
|
fi
|
|
echo ''
|
|
echo 'Done (infra only). Infrastructure is running.'
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "$SKIP_BUILD" != "true" ]]; then
|
|
build_solutions
|
|
fi
|
|
|
|
if [[ "$SKIP_IMAGES" != "true" ]]; then
|
|
if [[ "$SKIP_BUILD" == "true" ]]; then
|
|
build_images false
|
|
else
|
|
build_images true
|
|
fi
|
|
fi
|
|
|
|
start_platform
|
|
if ! smoke_test; then
|
|
fail 'Setup did not pass blocking smoke tests. Review output and docker compose logs.'
|
|
exit 1
|
|
fi
|
|
|
|
echo ''
|
|
echo '============================================='
|
|
echo ' Setup complete!'
|
|
echo ' Platform: https://stella-ops.local'
|
|
echo ' Docs: docs/dev/DEV_ENVIRONMENT_SETUP.md'
|
|
echo '============================================='
|