From 5b5a1140aa1dfc990e0ce11687d67999b55483e0 Mon Sep 17 00:00:00 2001 From: mrbobbytables Date: Sun, 13 Sep 2026 15:36:07 +0000 Subject: [PATCH] feat(tap): provide single-command kc-agent launcher formula and wrapper Closes #110 Provide a bulletproof single-command setup experience for users accessing the Bluefin Server KubeStellar Console: - Provide files/bin/bluefin-kubestellar launcher wrapper script: - Optimizes client environment by setting KAGENTI_CONTROLLER_URL="none" to prevent in-cluster Kagenti DNS lookup stalls - Configures console allowed-origins (default: http://localhost:8080,http://127.0.0.1:8080) - Auto-taps kubestellar/tap and installs kc-agent via brew if not present in PATH - Launches kc-agent in background with health polling verification, or in foreground - Provides status, stop, and logs lifecycle subcommands - Provide Homebrew formulas Formula/kc-agent.rb and Formula/bluefin-kubestellar.rb with service definition and environment configuration - Wire setup-kubestellar recipe in Justfile for single-command developer setup - Add comprehensive BATS and pytest unit tests covering launcher script and Justfile recipe contracts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: mrbobbytables --- Formula/bluefin-kubestellar.rb | 31 ++++ Formula/kc-agent.rb | 67 +++++++ Justfile | 7 + files/bin/bluefin-kubestellar | 223 +++++++++++++++++++++++ tests/unit/bluefin-kubestellar_test.bats | 202 ++++++++++++++++++++ tests/unit/test_setup_kubestellar.py | 54 ++++++ 6 files changed, 584 insertions(+) create mode 100644 Formula/bluefin-kubestellar.rb create mode 100644 Formula/kc-agent.rb create mode 100755 files/bin/bluefin-kubestellar create mode 100644 tests/unit/bluefin-kubestellar_test.bats create mode 100644 tests/unit/test_setup_kubestellar.py diff --git a/Formula/bluefin-kubestellar.rb b/Formula/bluefin-kubestellar.rb new file mode 100644 index 0000000..5bd09d5 --- /dev/null +++ b/Formula/bluefin-kubestellar.rb @@ -0,0 +1,31 @@ +# typed: false +# frozen_string_literal: true + +class BluefinKubestellar < Formula + desc "Single-command launcher and service wrapper for KubeStellar Console kc-agent" + homepage "https://github.com/projectbluefin/server" + version "26.08.0" + license "Apache-2.0" + + depends_on "ublue-os/tap/kc-agent" + + def install + (bin/"bluefin-kubestellar").write <<~SHELL + #!/usr/bin/env bash + exec "#{Formula["ublue-os/tap/kc-agent"].opt_bin}/kc-agent" "$@" + SHELL + chmod 0755, bin/"bluefin-kubestellar" + end + + service do + run [opt_bin/"bluefin-kubestellar"] + environment_variables KAGENTI_CONTROLLER_URL: "none" + keep_alive true + log_path var/"log/bluefin-kubestellar.log" + error_log_path var/"log/bluefin-kubestellar.log" + end + + test do + system bin/"bluefin-kubestellar", "--version" + end +end diff --git a/Formula/kc-agent.rb b/Formula/kc-agent.rb new file mode 100644 index 0000000..2d71e12 --- /dev/null +++ b/Formula/kc-agent.rb @@ -0,0 +1,67 @@ +# typed: false +# frozen_string_literal: true + +class KcAgent < Formula + desc "Local agent for KubeStellar Console: browser to kubeconfig" + homepage "https://github.com/kubestellar/console" + version "0.3.41" + license "Apache-2.0" + + on_macos do + if Hardware::CPU.intel? + url "https://github.com/kubestellar/console/releases/download/v0.3.41/kc-agent_0.3.41_darwin_amd64.tar.gz" + sha256 "238d15ca0e710f5f709cc776c664989fb16ebc1c6e7ac3d68c378d14f4ed0482" + end + if Hardware::CPU.arm? + url "https://github.com/kubestellar/console/releases/download/v0.3.41/kc-agent_0.3.41_darwin_arm64.tar.gz" + sha256 "8be0327ad033e77b7d32d8620a82fbb7b9423fad550965a25dc8f8f3769a9753" + end + end + + on_linux do + if Hardware::CPU.intel? && Hardware::CPU.is_64_bit? + url "https://github.com/kubestellar/console/releases/download/v0.3.41/kc-agent_0.3.41_linux_amd64.tar.gz" + sha256 "052b80bd0700f1c7e22ed5d6ad7547a45d6441d6a2cd40609fed1e69bf592df2" + end + if Hardware::CPU.arm? && Hardware::CPU.is_64_bit? + url "https://github.com/kubestellar/console/releases/download/v0.3.41/kc-agent_0.3.41_linux_arm64.tar.gz" + sha256 "a1fefe20ebf64d938a92d0a61661f8ad79cdb2b6d9cf1f63673d9f42959f0ccb" + end + end + + def install + libexec.install "kc-agent" => "kc-agent-bin" + + (bin/"kc-agent").write <<~SHELL + #!/usr/bin/env bash + set -euo pipefail + + # Optimize client environment: disable in-cluster kagenti DNS stalls + export KAGENTI_CONTROLLER_URL="${KAGENTI_CONTROLLER_URL:-none}" + + # Configure origin flags for local console if not already provided + DEFAULT_ORIGINS="http://localhost:8080,http://127.0.0.1:8080" + if [[ " $* " != *" -allowed-origins"* ]]; then + export KC_ALLOWED_ORIGINS="${KC_ALLOWED_ORIGINS:-$DEFAULT_ORIGINS}" + exec "#{libexec}/kc-agent-bin" -allowed-origins "${KC_ALLOWED_ORIGINS}" "$@" + else + exec "#{libexec}/kc-agent-bin" "$@" + fi + SHELL + + chmod 0755, bin/"kc-agent" + bin.install_symlink bin/"kc-agent" => "bluefin-kubestellar" + end + + service do + run [opt_bin/"kc-agent"] + environment_variables KAGENTI_CONTROLLER_URL: "none" + keep_alive true + log_path var/"log/kc-agent.log" + error_log_path var/"log/kc-agent.log" + end + + test do + system bin/"kc-agent", "--version" + end +end diff --git a/Justfile b/Justfile index 68d6423..b0fc63e 100644 --- a/Justfile +++ b/Justfile @@ -528,5 +528,12 @@ install-vm: echo "==> KubeStellar Console is ready!" echo "==> Access URL (LAN): http://${HOST_IP:-localhost}:8080/" echo "==> Access URL (Local): http://localhost:8080/" + echo "==> Connect client agent: just setup-kubestellar" xdg-open "http://${HOST_IP:-localhost}:8080/" || xdg-open http://localhost:8080/ || true wait "$QEMU_PID" + +# Setup, configure, and launch kc-agent for KubeStellar Console client connectivity. +[group('dev')] +setup-kubestellar ORIGIN="http://localhost:8080": + ./files/bin/bluefin-kubestellar --origin "{{ORIGIN}}" + diff --git a/files/bin/bluefin-kubestellar b/files/bin/bluefin-kubestellar new file mode 100755 index 0000000..4b86738 --- /dev/null +++ b/files/bin/bluefin-kubestellar @@ -0,0 +1,223 @@ +#!/usr/bin/env bash +# bluefin-kubestellar: Single-command launcher and manager for kc-agent +# +# Provides bulletproof setup and launch for connecting client workstations to the +# Bluefin Server KubeStellar Console kiosk: +# 1. Installs kc-agent via Homebrew (kubestellar/tap) if missing. +# 2. Configures origin flags for the local console (default: http://localhost:8080,http://127.0.0.1:8080). +# 3. Optimizes client environment by disabling in-cluster Kagenti DNS queries (KAGENTI_CONTROLLER_URL="none"). +# 4. Launches kc-agent backgrounded with health verification, or in foreground. +# 5. Provides lifecycle management (status, stop, logs). + +set -euo pipefail + +STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/bluefin-server" +PID_FILE="${STATE_DIR}/kc-agent.pid" +LOG_FILE="${STATE_DIR}/kc-agent.log" + +CMD="start" +ORIGIN="" +PORT="8585" +FOREGROUND=0 +AUTO_INSTALL=1 + +usage() { + cat < Allowed origin URL(s) (comma-separated) + Default: http://localhost:8080,http://127.0.0.1:8080 + -p, --port kc-agent local health/listen port (default: 8585) + -f, --foreground Run kc-agent in foreground instead of backgrounding + --no-install Do not attempt Homebrew installation if kc-agent is missing + -h, --help Show this help message +EOF +} + +while [ $# -gt 0 ]; do + case "$1" in + start|stop|status|logs|help) + CMD="$1" + shift + ;; + -o|--origin) + [ $# -gt 1 ] || { echo "ERROR: --origin requires an argument" >&2; exit 1; } + ORIGIN="$2" + shift 2 + ;; + --origin=*) + ORIGIN="${1#*=}" + shift + ;; + -p|--port) + [ $# -gt 1 ] || { echo "ERROR: --port requires an argument" >&2; exit 1; } + PORT="$2" + shift 2 + ;; + --port=*) + PORT="${1#*=}" + shift + ;; + -f|--foreground) + FOREGROUND=1 + shift + ;; + --no-install) + AUTO_INSTALL=0 + shift + ;; + -h|--help) + CMD="help" + shift + ;; + *) + if [[ "$1" =~ ^https?:// ]]; then + ORIGIN="$1" + shift + else + echo "ERROR: Unknown option or command: $1" >&2 + usage >&2 + exit 1 + fi + ;; + esac +done + +case "$CMD" in + help) + usage + exit 0 + ;; + stop) + if [ -f "$PID_FILE" ]; then + PID=$(<"$PID_FILE") + if kill -0 "$PID" 2>/dev/null; then + kill "$PID" 2>/dev/null || true + echo "==> Stopped kc-agent (PID: $PID)" + else + echo "==> kc-agent process ($PID) was not running" + fi + rm -f "$PID_FILE" + else + echo "==> No active kc-agent PID file found at $PID_FILE" + fi + exit 0 + ;; + status) + HEALTH_URL="http://127.0.0.1:${PORT}/health" + if curl --silent --fail --max-time 2 "$HEALTH_URL" >/dev/null 2>&1; then + echo "==> kc-agent is running and healthy at $HEALTH_URL" + if [ -f "$PID_FILE" ]; then + echo "==> Process PID: $(<"$PID_FILE")" + fi + exit 0 + fi + if [ -f "$PID_FILE" ]; then + PID=$(<"$PID_FILE") + if kill -0 "$PID" 2>/dev/null; then + echo "==> kc-agent process $PID is running but $HEALTH_URL is not responding" >&2 + exit 1 + fi + fi + echo "==> kc-agent is not running" + exit 3 + ;; + logs) + if [ -f "$LOG_FILE" ]; then + tail -n 50 "$LOG_FILE" + else + echo "==> No log file found at $LOG_FILE" + fi + exit 0 + ;; + start) + ORIGIN="${ORIGIN:-http://localhost:8080,http://127.0.0.1:8080}" + mkdir -p "$STATE_DIR" + + # Optimize client environment: disable in-cluster Kagenti DNS queries + export KAGENTI_CONTROLLER_URL="${KAGENTI_CONTROLLER_URL:-none}" + export KC_ALLOWED_ORIGINS="${ORIGIN}" + + # Check if already running and healthy (background mode only) + if [ "$FOREGROUND" -eq 0 ]; then + if curl --silent --fail --max-time 1 "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then + echo "==> kc-agent is already running and healthy at http://127.0.0.1:${PORT}/health" + echo "==> Allowed origin(s): ${ORIGIN}" + exit 0 + fi + fi + + # Ensure kc-agent is installed + if ! command -v kc-agent >/dev/null 2>&1; then + if [ "$AUTO_INSTALL" -eq 1 ] && command -v brew >/dev/null 2>&1; then + echo "==> kc-agent not found in PATH; installing from kubestellar/tap via Homebrew..." + brew tap kubestellar/tap + brew install kc-agent + else + echo "ERROR: kc-agent is not installed and cannot be auto-installed." >&2 + echo "Please install Homebrew (https://brew.sh) or install kc-agent manually:" >&2 + echo " brew tap kubestellar/tap && brew install kc-agent" >&2 + exit 1 + fi + fi + + # Foreground execution + if [ "$FOREGROUND" -eq 1 ]; then + echo "==> Starting kc-agent in foreground..." + echo "==> Allowed origins: ${ORIGIN}" + echo "==> KAGENTI_CONTROLLER_URL: ${KAGENTI_CONTROLLER_URL}" + exec kc-agent -allowed-origins "${ORIGIN}" + fi + + # Clean up stale PID file if process died + if [ -f "$PID_FILE" ]; then + OLD_PID=$(<"$PID_FILE") + if kill -0 "$OLD_PID" 2>/dev/null; then + kill "$OLD_PID" 2>/dev/null || true + sleep 0.5 + fi + rm -f "$PID_FILE" + fi + + # Background execution + echo "==> Starting kc-agent in background..." + echo "==> Allowed origins: ${ORIGIN}" + echo "==> KAGENTI_CONTROLLER_URL: ${KAGENTI_CONTROLLER_URL}" + nohup kc-agent -allowed-origins "${ORIGIN}" > "$LOG_FILE" 2>&1 & + AGENT_PID=$! + echo "$AGENT_PID" > "$PID_FILE" + + echo "==> Waiting for kc-agent health probe at http://127.0.0.1:${PORT}/health..." + DEADLINE=15 + START_TIME=$(date +%s) + while true; do + if ! kill -0 "$AGENT_PID" 2>/dev/null; then + echo "ERROR: kc-agent process ($AGENT_PID) exited unexpectedly!" >&2 + [ -f "$LOG_FILE" ] && tail -n 20 "$LOG_FILE" >&2 + rm -f "$PID_FILE" + exit 1 + fi + if curl --silent --fail --max-time 1 "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then + echo "==> kc-agent is healthy and connected! (PID: $AGENT_PID)" + echo "==> Console client connectivity active for origin(s): ${ORIGIN}" + echo "==> Log file: ${LOG_FILE}" + break + fi + NOW=$(date +%s) + if [ $((NOW - START_TIME)) -ge "$DEADLINE" ]; then + echo "ERROR: Timed out waiting for kc-agent to become healthy on port ${PORT}." >&2 + [ -f "$LOG_FILE" ] && tail -n 20 "$LOG_FILE" >&2 + exit 1 + fi + sleep 0.5 + done + ;; +esac diff --git a/tests/unit/bluefin-kubestellar_test.bats b/tests/unit/bluefin-kubestellar_test.bats new file mode 100644 index 0000000..c4fc205 --- /dev/null +++ b/tests/unit/bluefin-kubestellar_test.bats @@ -0,0 +1,202 @@ +#!/usr/bin/env bats +# +# Unit tests for files/bin/bluefin-kubestellar launcher and manager. +# +# Tests run in a temporary sandbox where kc-agent, brew, curl, and kill +# are replaced by stubs on PATH to verify orchestration, arguments, +# and environment isolation. + +setup() { + REPO_ROOT="$(cd "${BATS_TEST_DIRNAME}/../.." && pwd)" + SCRIPT="${REPO_ROOT}/files/bin/bluefin-kubestellar" + SANDBOX="${BATS_TEST_TMPDIR}/sandbox" + STUB_DIR="${BATS_TEST_TMPDIR}/bin" + LOG="${BATS_TEST_TMPDIR}/calls.log" + STATE_DIR="${SANDBOX}/state" + + mkdir -p "$STUB_DIR" "$STATE_DIR" + : > "$LOG" + + # Default stubs + make_curl_stub 1 # default health fails unless overridden + make_kc_agent_stub 0 + make_brew_stub 0 +} + +make_kc_agent_stub() { + cat > "${STUB_DIR}/kc-agent" <> "${LOG}" +echo "ENV_KAGENTI=\${KAGENTI_CONTROLLER_URL:-unset}" >> "${LOG}" +echo "ENV_ORIGINS=\${KC_ALLOWED_ORIGINS:-unset}" >> "${LOG}" +# If running in background test, background stub can sleep briefly +if [ "\${TEST_KC_BACKGROUND:-0}" = "1" ]; then + sleep 30 & + echo \$! +else + exit $1 +fi +EOF + chmod +x "${STUB_DIR}/kc-agent" +} + +make_brew_stub() { + cat > "${STUB_DIR}/brew" <> "${LOG}" +if [ "\${1:-}" = "install" ] && [ "\${2:-}" = "kc-agent" ]; then + cat > "${STUB_DIR}/kc-agent" << 'INNER' +#!/usr/bin/env bash +echo "kc-agent \$*" >> "${LOG}" +exit 0 +INNER + chmod +x "${STUB_DIR}/kc-agent" +fi +exit $1 +EOF + chmod +x "${STUB_DIR}/brew" +} + +make_curl_stub() { + cat > "${STUB_DIR}/curl" <> "${LOG}" +if [ "\${TEST_CURL_HEALTHY:-0}" = "1" ]; then + exit 0 +else + exit $1 +fi +EOF + chmod +x "${STUB_DIR}/curl" +} + +run_launcher() { + run env PATH="${STUB_DIR}:${PATH}" \ + XDG_STATE_HOME="${SANDBOX}" \ + bash "$SCRIPT" "$@" +} + +assert_log() { + if ! grep -qF -- "$1" "$LOG"; then + echo "expected call log to contain: $1" >&2 + cat "$LOG" >&2 + return 1 + fi +} + +refute_log() { + if grep -qF -- "$1" "$LOG"; then + echo "expected call log NOT to contain: $1" >&2 + cat "$LOG" >&2 + return 1 + fi +} + +@test "script exists and is executable" { + [ -f "$SCRIPT" ] + [ -x "$SCRIPT" ] +} + +@test "script enforces strict bash mode" { + run head -n 15 "$SCRIPT" + [ "$status" -eq 0 ] + [[ "$output" == *"set -euo pipefail"* ]] +} + +@test "help option displays usage" { + run_launcher --help + [ "$status" -eq 0 ] + [[ "$output" == *"Usage:"* ]] + [[ "$output" == *"--origin"* ]] + [[ "$output" == *"start"* ]] + [[ "$output" == *"stop"* ]] + [[ "$output" == *"status"* ]] +} + +@test "unknown argument fails with error" { + run_launcher --invalid-flag + [ "$status" -ne 0 ] + [[ "$output" == *"ERROR: Unknown option or command"* ]] +} + +@test "missing kc-agent triggers brew tap and install" { + rm -f "${STUB_DIR}/kc-agent" + TEST_CURL_HEALTHY=1 run_launcher --foreground + assert_log "brew tap kubestellar/tap" + assert_log "brew install kc-agent" +} + +@test "missing kc-agent with --no-install aborts without brew" { + rm -f "${STUB_DIR}/kc-agent" + run_launcher --no-install --foreground + [ "$status" -ne 0 ] + [[ "$output" == *"ERROR: kc-agent is not installed"* ]] + refute_log "brew " +} + +@test "missing both kc-agent and brew reports helpful error" { + rm -f "${STUB_DIR}/kc-agent" + rm -f "${STUB_DIR}/brew" + run_launcher --foreground + [ "$status" -ne 0 ] + [[ "$output" == *"ERROR: kc-agent is not installed and cannot be auto-installed"* ]] +} + +@test "foreground launch optimizes client environment and configures origin" { + run_launcher --foreground --origin "http://test-origin:8080" + [ "$status" -eq 0 ] + assert_log "ENV_KAGENTI=none" + assert_log "ENV_ORIGINS=http://test-origin:8080" + assert_log "-allowed-origins http://test-origin:8080" +} + +@test "bare URL argument is parsed as origin" { + run_launcher --foreground "http://custom-node:8080" + [ "$status" -eq 0 ] + assert_log "-allowed-origins http://custom-node:8080" +} + +@test "start skips relaunch if already healthy on health port" { + TEST_CURL_HEALTHY=1 run_launcher + [ "$status" -eq 0 ] + [[ "$output" == *"already running and healthy"* ]] + refute_log "kc-agent " +} + +@test "status reports healthy when endpoint responds" { + TEST_CURL_HEALTHY=1 run_launcher status + [ "$status" -eq 0 ] + [[ "$output" == *"running and healthy"* ]] +} + +@test "status reports not running when endpoint is unreachable" { + TEST_CURL_HEALTHY=0 run_launcher status + [ "$status" -eq 3 ] + [[ "$output" == *"not running"* ]] +} + +@test "stop terminates recorded PID and cleans pidfile" { + PID_FILE="${SANDBOX}/bluefin-server/kc-agent.pid" + mkdir -p "${SANDBOX}/bluefin-server" + sleep 30 & + BG_PID=$! + echo "$BG_PID" > "$PID_FILE" + + run_launcher stop + [ "$status" -eq 0 ] + [[ "$output" == *"Stopped kc-agent"* ]] + [ ! -f "$PID_FILE" ] + + # Verify background process was killed + ! kill -0 "$BG_PID" 2>/dev/null +} + +@test "logs displays existing log file contents" { + LOG_FILE="${SANDBOX}/bluefin-server/kc-agent.log" + mkdir -p "${SANDBOX}/bluefin-server" + echo "test kc-agent log line" > "$LOG_FILE" + + run_launcher logs + [ "$status" -eq 0 ] + [[ "$output" == *"test kc-agent log line"* ]] +} diff --git a/tests/unit/test_setup_kubestellar.py b/tests/unit/test_setup_kubestellar.py new file mode 100644 index 0000000..dcd9045 --- /dev/null +++ b/tests/unit/test_setup_kubestellar.py @@ -0,0 +1,54 @@ +"""Contracts for the kc-agent launcher and setup-kubestellar recipe.""" + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +JUSTFILE = ROOT / "Justfile" +LAUNCHER = ROOT / "files" / "bin" / "bluefin-kubestellar" +KC_AGENT_FORMULA = ROOT / "Formula" / "kc-agent.rb" +BLUEFIN_FORMULA = ROOT / "Formula" / "bluefin-kubestellar.rb" + + +def test_setup_kubestellar_recipe_contract() -> None: + justfile = JUSTFILE.read_text(encoding="utf-8") + + assert "setup-kubestellar ORIGIN=" in justfile + start = justfile.index("setup-kubestellar ORIGIN=") + recipe = justfile[start:] + + assert "./files/bin/bluefin-kubestellar --origin \"{{ORIGIN}}\"" in recipe + before_lines = [line.strip() for line in justfile[:start].splitlines() if line.strip()] + assert "[group('dev')]" in before_lines[-1] or "[group('dev')]" in before_lines[-2] + + + +def test_bluefin_kubestellar_launcher_script_contract() -> None: + assert LAUNCHER.is_file() + script = LAUNCHER.read_text(encoding="utf-8") + + assert script.startswith("#!/usr/bin/env bash") + assert "set -euo pipefail" in script + assert "KAGENTI_CONTROLLER_URL" in script + assert "KC_ALLOWED_ORIGINS" in script + assert "brew tap kubestellar/tap" in script + assert "brew install kc-agent" in script + assert "/health" in script + + +def test_homebrew_formulas_contract() -> None: + assert KC_AGENT_FORMULA.is_file() + assert BLUEFIN_FORMULA.is_file() + + kc_agent = KC_AGENT_FORMULA.read_text(encoding="utf-8") + bluefin = BLUEFIN_FORMULA.read_text(encoding="utf-8") + + # kc-agent formula verifies client environment optimization & background service + assert "class KcAgent < Formula" in kc_agent + assert "KAGENTI_CONTROLLER_URL" in kc_agent + assert "service do" in kc_agent + assert "bluefin-kubestellar" in kc_agent + + # bluefin-kubestellar formula provides service & launcher + assert "class BluefinKubestellar < Formula" in bluefin + assert "service do" in bluefin + assert "KAGENTI_CONTROLLER_URL" in bluefin