diff --git a/cmd/obol/bootstrap.go b/cmd/obol/bootstrap.go index d74a6039..a8cc03ef 100644 --- a/cmd/obol/bootstrap.go +++ b/cmd/obol/bootstrap.go @@ -38,7 +38,7 @@ func bootstrapCommand(cfg *config.Config) *cli.Command { } // Step 2: Start stack - if err := stack.Up(cfg, u); err != nil { + if err := stack.Up(cfg, u, false); err != nil { return fmt.Errorf("bootstrap up failed: %w", err) } diff --git a/cmd/obol/main.go b/cmd/obol/main.go index e8d80385..3a4d9e25 100644 --- a/cmd/obol/main.go +++ b/cmd/obol/main.go @@ -159,8 +159,14 @@ GLOBAL OPTIONS:{{template "visibleFlagTemplate" .}}{{end}} { Name: "up", Usage: "Start the Obol Stack", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "wildcard-dns", + Usage: "Configure wildcard *.obol.stack DNS via NetworkManager/dnsmasq (Linux) or /etc/resolver (macOS)", + }, + }, Action: func(ctx context.Context, cmd *cli.Command) error { - return stack.Up(cfg, getUI(cmd)) + return stack.Up(cfg, getUI(cmd), cmd.Bool("wildcard-dns")) }, }, { diff --git a/internal/dns/resolver.go b/internal/dns/resolver.go index ea69a730..3e3445ab 100644 --- a/internal/dns/resolver.go +++ b/internal/dns/resolver.go @@ -317,19 +317,15 @@ func removeMacOSResolver() { // --- Linux (NetworkManager dnsmasq plugin) --- // configureLinuxResolver sets up NM's dnsmasq plugin for *.obol.stack. +// Returns a non-fatal error when NetworkManager is unavailable (e.g. headless +// servers) — the caller falls back to /etc/hosts entries. func configureLinuxResolver() error { if configureNMDnsmasq() { return nil } - // NM not available — print instructions - fmt.Println("\nWildcard DNS for *.obol.stack requires NetworkManager with dnsmasq.") - fmt.Println("Install with:") - fmt.Println(" sudo apt install network-manager dnsmasq-base # Debian/Ubuntu") - fmt.Println(" sudo dnf install NetworkManager dnsmasq # Fedora/RHEL") - fmt.Println(" sudo pacman -S networkmanager dnsmasq # Arch") - fmt.Println("\nThen re-run: obol stack up") - return fmt.Errorf("NetworkManager required for wildcard DNS on Linux") + // NM not available — return quiet error; caller handles the fallback message. + return fmt.Errorf("NetworkManager not available (headless or server system)") } // hasNMDnsmasqConfig checks if the NM dnsmasq config for obol.stack exists. @@ -359,10 +355,6 @@ func configureNMDnsmasq() bool { // Check if dnsmasq binary is available (NM plugin requires it) if _, err := exec.LookPath("dnsmasq"); err != nil { - fmt.Println("Note: dnsmasq not found. Install it for wildcard DNS support:") - fmt.Println(" sudo apt install dnsmasq-base # Debian/Ubuntu") - fmt.Println(" sudo dnf install dnsmasq # Fedora/RHEL") - fmt.Println(" sudo pacman -S dnsmasq # Arch") return false } diff --git a/internal/openclaw/OPENCLAW_VERSION b/internal/openclaw/OPENCLAW_VERSION index a4a00969..fb85ceb0 100644 --- a/internal/openclaw/OPENCLAW_VERSION +++ b/internal/openclaw/OPENCLAW_VERSION @@ -1,3 +1,3 @@ # renovate: datasource=github-releases depName=openclaw/openclaw # Pins the upstream OpenClaw version to build and publish. -v2026.3.13-1 +v2026.3.24 diff --git a/internal/stack/stack.go b/internal/stack/stack.go index b7e22101..f9a8dc80 100644 --- a/internal/stack/stack.go +++ b/internal/stack/stack.go @@ -254,7 +254,7 @@ func dockerBridgeGatewayIP() (string, error) { } // Up starts the cluster using the configured backend -func Up(cfg *config.Config, u *ui.UI) error { +func Up(cfg *config.Config, u *ui.UI, wildcardDNS bool) error { stackID := getStackID(cfg) if stackID == "" { return fmt.Errorf("stack ID not found, run 'obol stack init' first") @@ -285,13 +285,22 @@ func Up(cfg *config.Config, u *ui.UI) error { return err } - // Ensure DNS resolver is running for wildcard *.obol.stack - if err := dns.EnsureRunning(); err != nil { - u.Warnf("DNS resolver failed to start: %v", err) - } else if err := dns.ConfigureSystemResolver(); err != nil { - u.Warnf("Failed to configure system DNS resolver: %v", err) - } else { - u.Success("DNS resolver configured") + // Ensure obol.stack resolves to localhost via /etc/hosts (works everywhere). + if err := dns.EnsureHostsEntries(nil); err != nil { + u.Warnf("Could not update /etc/hosts for obol.stack: %v", err) + } + + // Wildcard *.obol.stack DNS is opt-in (--wildcard-dns) because it + // modifies system DNS config (NetworkManager/resolv.conf on Linux, + // /etc/resolver on macOS) which can break host DNS resolution. + if wildcardDNS { + if err := dns.EnsureRunning(); err != nil { + u.Warnf("DNS resolver failed to start: %v", err) + } else if err := dns.ConfigureSystemResolver(); err != nil { + u.Warnf("Wildcard DNS configuration failed: %v", err) + } else { + u.Success("Wildcard DNS configured (*.obol.stack)") + } } u.Blank() diff --git a/obolup.sh b/obolup.sh index dc148231..7d3abf13 100755 --- a/obolup.sh +++ b/obolup.sh @@ -62,7 +62,7 @@ readonly K9S_VERSION="0.50.18" readonly HELM_DIFF_VERSION="3.14.1" # Must match internal/openclaw/OPENCLAW_VERSION (without "v" prefix). # Tested by TestOpenClawVersionConsistency. -readonly OPENCLAW_VERSION="2026.3.13-1" +readonly OPENCLAW_VERSION="2026.3.24" # Repository URL for building from source readonly OBOL_REPO_URL="git@github.com:ObolNetwork/obol-stack.git" @@ -96,6 +96,49 @@ command_exists() { command -v "$1" >/dev/null 2>&1 } +# Check host prerequisites that the installer cannot provide itself. +# Binaries we download (helm, kubectl, k3d, etc.) are not checked here — +# only tools the user must install separately. +check_prerequisites() { + local missing=() + + # Node.js 22+ / npm — required for openclaw CLI (unless already installed) + local need_npm=true + if command_exists openclaw; then + local oc_version + oc_version=$(openclaw --version 2>/dev/null | tr -d '[:space:]' || echo "") + if [[ -n "$oc_version" ]] && version_ge "$oc_version" "$OPENCLAW_VERSION"; then + need_npm=false + fi + fi + + if [[ "$need_npm" == "true" ]]; then + if ! command_exists npm; then + missing+=("Node.js 22+ (npm) — required to install openclaw CLI") + else + local node_major + node_major=$(node --version 2>/dev/null | sed 's/v//' | cut -d. -f1) + if [[ -z "$node_major" ]] || [[ "$node_major" -lt 22 ]]; then + missing+=("Node.js 22+ (found: v${node_major:-none}) — required for openclaw CLI") + fi + fi + fi + + if [[ ${#missing[@]} -gt 0 ]]; then + log_error "Missing prerequisites:" + echo "" + for dep in "${missing[@]}"; do + echo " • $dep" + done + echo "" + log_dim " Install the above, then re-run obolup.sh" + echo "" + return 1 + fi + + return 0 +} + # Detect installation mode (install vs upgrade) detect_installation_mode() { if [[ -f "$OBOL_BIN_DIR/obol" ]]; then @@ -123,8 +166,24 @@ check_docker() { return 1 fi - # Check if Docker daemon is running; try to start it automatically + # Check if Docker daemon is running and accessible if ! docker info >/dev/null 2>&1; then + # Distinguish permission errors from daemon-not-running + local docker_err + docker_err=$(docker info 2>&1) + + if echo "$docker_err" | grep -qi "permission denied"; then + log_error "Docker is installed but your user does not have permission to access it" + echo "" + echo " Add your user to the docker group and apply the change:" + echo " sudo usermod -aG docker \$USER" + echo " newgrp docker" + echo "" + log_dim " Then run this installer again." + echo "" + return 1 + fi + if [[ "$(uname -s)" == "Linux" ]]; then log_warn "Docker daemon is not running — attempting to start..." # Try systemd first (apt/yum installs), then snap @@ -183,21 +242,6 @@ check_docker() { log_warn "k3d may not work correctly with older Docker versions" fi - # Check if user can run Docker without sudo (Linux-specific) - if [[ "$(uname -s)" == "Linux" ]]; then - if ! docker ps >/dev/null 2>&1; then - log_warn "Current user cannot run Docker commands" - echo "" - echo "You may need to add your user to the docker group:" - echo " sudo usermod -aG docker \$USER" - echo " newgrp docker" - echo "" - echo "Or run commands with sudo." - echo "" - # Don't fail here - user might be running with sudo - fi - fi - # Check Docker networking (ensure bridge network works) if ! docker network ls >/dev/null 2>&1; then log_error "Docker networking is not functional" @@ -1241,6 +1285,13 @@ install_dependencies() { log_info "Checking and installing dependencies..." echo "" + # Ensure OBOL_BIN_DIR is in PATH for the current process so that + # binaries installed here (helm, helmfile, etc.) are immediately + # available to each other and to later steps like `obol bootstrap`. + if ! echo "$PATH" | grep -q "$OBOL_BIN_DIR"; then + export PATH="$OBOL_BIN_DIR:$PATH" + fi + # Install each dependency install_kubectl || log_warn "kubectl installation failed (continuing...)" install_helm || log_warn "helm installation failed (continuing...)" @@ -1433,20 +1484,13 @@ add_to_profile() { # In interactive mode, asks user whether to auto-modify or show manual instructions. # In non-interactive mode (CI/CD), prints manual instructions only unless OBOL_MODIFY_PATH=yes. configure_path() { - # Check if OBOL_BIN_DIR is already in current PATH - if echo "$PATH" | grep -q "$OBOL_BIN_DIR"; then - log_success "OBOL_BIN_DIR already in PATH" - return 0 - fi - # Detect appropriate profile file local profile profile=$(detect_shell_profile) - # Check if already configured in detected profile + # Check if already persisted in the shell profile if [[ -f "$profile" ]] && grep -qF "$OBOL_BIN_DIR" "$profile" 2>/dev/null; then log_success "OBOL_BIN_DIR already configured in $profile" - log_info "Will be available in new shell sessions" return 0 fi @@ -1690,6 +1734,11 @@ main() { fi echo "" + # Check host prerequisites (npm/Node.js, etc.) before starting installs + if ! check_prerequisites; then + exit 1 + fi + create_directories install_obol_binary copy_bootstrap_script