Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/obol/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
8 changes: 7 additions & 1 deletion cmd/obol/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
},
},
{
Expand Down
16 changes: 4 additions & 12 deletions internal/dns/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion internal/openclaw/OPENCLAW_VERSION
Original file line number Diff line number Diff line change
@@ -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
25 changes: 17 additions & 8 deletions internal/stack/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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()
Expand Down
99 changes: 74 additions & 25 deletions obolup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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...)"
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
Loading