#!/bin/sh # ============================================================================= # obserae demo installer — one-liner bootstrap # ----------------------------------------------------------------------------- # Deploy the full obserae demo lab (Docker Compose) in one command: # # curl -fsSL https://demo.obserae.com | sh # # This script is the ONLY file hosted on demo.obserae.com. It fetches the rest # of the demo (compose file, Dockerfiles, configs, scripts) from the public # GitHub repo, then builds and starts the stack. It also manages the demo's # lifecycle (status / logs / update / uninstall). # # POSIX sh on purpose (runs on the host via `| sh`). Pass a command/flags to a # piped invocation with `-s --`, e.g.: # curl -fsSL https://demo.obserae.com | sh -s -- --with-ldap # curl -fsSL https://demo.obserae.com | sh -s -- uninstall --yes # ============================================================================= set -eu # --- Configuration (override via environment) -------------------------------- REPO_OWNER="${REPO_OWNER:-spartan-conseil}" REPO_NAME="${REPO_NAME:-obserae}" REPO_REF="${REPO_REF:-main}" # branch or tag; also --ref DEMO_SUBDIR="${DEMO_SUBDIR:-obserae-demo}" # path of the demo inside the repo DEMO_DIR="${OBSERAE_DEMO_DIR:-./obserae-demo}" UI_URL="${UI_URL:-http://127.0.0.1:8081}" INSTALL_URL="${INSTALL_URL:-https://demo.obserae.com}" # --- Runtime state ----------------------------------------------------------- CMD="" EXTRA_ARGS="" WITH_LDAP="${WITH_LDAP:-0}" ASSUME_YES="${ASSUME_YES:-0}" NO_START=0 DOCKER_SUDO="" DOWNLOADER="" _tmpfiles="" # --- Logging ----------------------------------------------------------------- if [ -t 2 ] && [ -z "${NO_COLOR:-}" ]; then C_RESET="$(printf '\033[0m')"; C_INFO="$(printf '\033[1;34m')" C_WARN="$(printf '\033[1;33m')"; C_ERR="$(printf '\033[1;31m')" C_OK="$(printf '\033[1;32m')"; C_DIM="$(printf '\033[2m')" else C_RESET=''; C_INFO=''; C_WARN=''; C_ERR=''; C_OK=''; C_DIM='' fi info() { printf '%s==>%s %s\n' "$C_INFO" "$C_RESET" "$*" >&2; } ok() { printf '%s==>%s %s\n' "$C_OK" "$C_RESET" "$*" >&2; } warn() { printf '%swarning:%s %s\n' "$C_WARN" "$C_RESET" "$*" >&2; } err() { printf '%serror:%s %s\n' "$C_ERR" "$C_RESET" "$*" >&2; } die() { err "$*"; exit 1; } cleanup() { [ -n "$_tmpfiles" ] && rm -f $_tmpfiles 2>/dev/null || true; } trap cleanup EXIT INT TERM have() { command -v "$1" >/dev/null 2>&1; } new_tmp() { REPLY="$(mktemp)"; _tmpfiles="$_tmpfiles $REPLY"; } # -> $REPLY usage() { cat >&2 < -> stdout (fails on HTTP error) fetch() { if [ "$DOWNLOADER" = "curl" ]; then curl -fsSL "$1" else wget -qO- "$1"; fi } # ui_up: connection-level health check (any HTTP response = up), soft ui_up() { if [ "$DOWNLOADER" = "curl" ]; then curl -sS -o /dev/null --max-time 3 "$UI_URL" 2>/dev/null elif [ "$DOWNLOADER" = "wget" ]; then wget -q -O /dev/null --timeout=3 "$UI_URL" 2>/dev/null else return 1; fi } # --- Docker preflight -------------------------------------------------------- detect_compose() { have docker || die "Docker is not installed — see https://docs.docker.com/engine/install/" if docker compose version >/dev/null 2>&1; then DOCKER_SUDO="" elif have sudo && sudo docker compose version >/dev/null 2>&1; then DOCKER_SUDO="sudo" warn "You can't reach Docker directly; using sudo. (Add yourself to the 'docker' group to avoid this.)" else die "Docker Compose v2 is required (the 'docker compose' subcommand), reachable directly or via sudo." fi } require_daemon() { $DOCKER_SUDO docker info >/dev/null 2>&1 || die "Cannot reach the Docker daemon — is it running?" } # compose ... : run docker compose inside the demo dir (picks up its .env) compose() { ( cd "$DEMO_DIR" && exec $DOCKER_SUDO docker compose "$@" ); } check_host() { [ "$(uname -s)" = "Linux" ] || warn "Non-Linux host ($(uname -s)): the softflowd sensor uses network_mode:host and won't capture the bridges the same way on Docker Desktop (README §8)." if have ss; then if ss -ltnH 2>/dev/null | grep -qE '(127\.0\.0\.1|0\.0\.0\.0|\*|\[::\]|\[::1\]):8081([^0-9]|$)'; then warn "TCP port 8081 on localhost looks busy — the obserae UI may fail to bind ($UI_URL)." fi fi } require_demo_dir() { [ -f "$DEMO_DIR/docker-compose.yml" ] || die "no demo found at '$DEMO_DIR' (run 'install' first, or pass --dir)." } # --- .env management (persist chosen options for later compose commands) ------ set_env_kv() { # file key value — upsert KEY=VALUE _f="$1"; _k="$2"; _v="$3" new_tmp [ -f "$_f" ] && grep -v "^${_k}=" "$_f" > "$REPLY" 2>/dev/null || true printf '%s=%s\n' "$_k" "$_v" >> "$REPLY" mv "$REPLY" "$_f" } del_env_kv() { # file key — remove KEY= line if present _f="$1"; _k="$2" [ -f "$_f" ] || return 0 new_tmp grep -v "^${_k}=" "$_f" > "$REPLY" 2>/dev/null || true mv "$REPLY" "$_f" } write_env() { _env="$DEMO_DIR/.env" # COMPOSE_PROFILES=ldap makes `docker compose up` include the FreeIPA service # (which is behind the "ldap" profile). Written here so every later compose # command in this dir is consistent without needing the flag again. if [ "$WITH_LDAP" = "1" ]; then set_env_kv "$_env" COMPOSE_PROFILES ldap else del_env_kv "$_env" COMPOSE_PROFILES; fi [ -n "${OBSERAE_IMAGE:-}" ] && set_env_kv "$_env" OBSERAE_IMAGE "$OBSERAE_IMAGE" || true [ -n "${ACTIVITY_MIN_SLEEP:-}" ] && set_env_kv "$_env" ACTIVITY_MIN_SLEEP "$ACTIVITY_MIN_SLEEP" || true [ -n "${ACTIVITY_MAX_SLEEP:-}" ] && set_env_kv "$_env" ACTIVITY_MAX_SLEEP "$ACTIVITY_MAX_SLEEP" || true } # --- Fetch demo files from GitHub via the manifest --------------------------- fetch_files() { [ -n "$DOWNLOADER" ] || die "need curl or wget to download the demo files" have mktemp || die "mktemp is required" info "Fetching demo files from $RAW_BASE" mkdir -p "$DEMO_DIR" new_tmp; _manifest="$REPLY" fetch "$RAW_BASE/manifest.txt" > "$_manifest" \ || die "cannot download manifest.txt — check that $REPO_OWNER/$REPO_NAME@$REPO_REF is public and $DEMO_SUBDIR exists" [ -s "$_manifest" ] || die "manifest.txt is empty" _count=0 while IFS= read -r rel || [ -n "$rel" ]; do rel="$(printf '%s' "$rel" | tr -d '\r')" case "$rel" in ""|\#*) continue ;; # blank / comment /*|*..*) die "unsafe manifest entry: $rel" ;; esac dest="$DEMO_DIR/$rel" mkdir -p "$(dirname "$dest")" printf '%s %s%s\n' "$C_DIM" "$rel" "$C_RESET" >&2 fetch "$RAW_BASE/$rel" > "$dest" || die "failed to download $rel" case "$rel" in *.sh) chmod +x "$dest" ;; esac _count=$((_count + 1)) done < "$_manifest" [ "$_count" -gt 0 ] || die "manifest listed no files" ok "Fetched $_count files into $DEMO_DIR" } # --- Confirmations (works under `curl | sh` via /dev/tty) -------------------- confirm() { [ "$ASSUME_YES" = "1" ] && return 0 if [ -r /dev/tty ]; then printf '%s [y/N] ' "$1" > /dev/tty read -r _ans < /dev/tty || _ans="" case "$_ans" in [yY]|[yY][eE][sS]) return 0 ;; *) return 1 ;; esac fi warn "no TTY for confirmation — pass --yes to proceed non-interactively" return 1 } wait_ui() { info "Waiting for the obserae UI at $UI_URL ..." _i=0 while [ "$_i" -lt 30 ]; do if ui_up; then ok "obserae UI is up: $UI_URL"; return 0; fi _i=$((_i + 1)); sleep 2 done warn "obserae UI not reachable yet — it may still be starting. Follow it with: curl -fsSL $INSTALL_URL | sh -s -- logs obserae" } print_next_steps() { _sudo=""; [ -n "$DOCKER_SUDO" ] && _sudo="sudo " cat </dev/null || true } cmd_logs() { require_demo_dir detect_compose require_daemon # EXTRA_ARGS may name a single service (e.g. sensor); unquoted on purpose. # shellcheck disable=SC2086 compose logs -f --tail 100 $EXTRA_ARGS } cmd_uninstall() { require_demo_dir detect_compose require_daemon case "$DEMO_DIR" in ""|/|.|./|"$HOME"|"$HOME/") die "refusing to remove unsafe path: '$DEMO_DIR'" ;; esac confirm "This STOPS the demo and DELETES its data volumes and the directory '$DEMO_DIR'. Continue?" || { info "Aborted."; exit 0; } info "Stopping the stack and removing volumes ..." compose down -v --remove-orphans || warn "compose down reported an error; continuing with directory removal." rm -rf "$DEMO_DIR" ok "Demo removed ('$DEMO_DIR' deleted, volumes dropped)." } # --- Main -------------------------------------------------------------------- main() { parse_args "$@" case "$CMD" in install) cmd_install ;; update) cmd_update ;; status) cmd_status ;; logs) cmd_logs ;; uninstall) cmd_uninstall ;; help) usage ;; *) usage; exit 1 ;; esac } main "$@"