feat: add dev-local mode for localhost testing with auto-discovery of bootstrap peers

This commit is contained in:
anonpenguin 2025-08-09 17:50:02 +03:00
parent e76ad5cf16
commit cf36d301d5
5 changed files with 98 additions and 26 deletions

View File

@ -26,21 +26,21 @@ test:
# Run bootstrap node explicitly # Run bootstrap node explicitly
run-node: run-node:
@echo "Starting BOOTSTRAP node (role=bootstrap)..." @echo "Starting BOOTSTRAP node (role=bootstrap)..."
go run ./cmd/node -role bootstrap -data ./data/bootstrap -advertise localhost -p2p-port $${P2P:-4001} go run ./cmd/node -role bootstrap -data ./data/bootstrap -advertise localhost -p2p-port $${P2P:-4001} -dev-local
# Run second node (regular) - requires BOOTSTRAP multiaddr # Run second node (regular) - requires BOOTSTRAP multiaddr
# Usage: make run-node2 BOOTSTRAP=/ip4/127.0.0.1/tcp/4001/p2p/<ID> HTTP=5002 RAFT=7002 P2P=4002 # Usage: make run-node2 BOOTSTRAP=/ip4/127.0.0.1/tcp/4001/p2p/<ID> HTTP=5002 RAFT=7002 P2P=4002
run-node2: run-node2:
@echo "Starting REGULAR node2 (role=node)..." @echo "Starting REGULAR node2 (role=node)..."
@if [ -z "$(BOOTSTRAP)" ]; then echo "ERROR: Provide BOOTSTRAP multiaddr: make run-node2 BOOTSTRAP=/ip4/127.0.0.1/tcp/4001/p2p/<ID> [HTTP=5002 RAFT=7002 P2P=4002]"; exit 1; fi @if [ -z "$(BOOTSTRAP)" ]; then echo "ERROR: Provide BOOTSTRAP multiaddr: make run-node2 BOOTSTRAP=/ip4/127.0.0.1/tcp/4001/p2p/<ID> [HTTP=5002 RAFT=7002 P2P=4002]"; exit 1; fi
go run ./cmd/node -role node -id node2 -data ./data/node2 -bootstrap $(BOOTSTRAP) -rqlite-http-port $${HTTP:-5002} -rqlite-raft-port $${RAFT:-7002} -p2p-port $${P2P:-4002} -advertise $${ADVERTISE:-localhost} go run ./cmd/node -role node -id node2 -data ./data/node2 -bootstrap $(BOOTSTRAP) -rqlite-http-port $${HTTP:-5002} -rqlite-raft-port $${RAFT:-7002} -p2p-port $${P2P:-4002} -advertise $${ADVERTISE:-localhost} -dev-local
# Run third node (regular) - requires BOOTSTRAP multiaddr # Run third node (regular) - requires BOOTSTRAP multiaddr
# Usage: make run-node3 BOOTSTRAP=/ip4/127.0.0.1/tcp/4001/p2p/<ID> HTTP=5003 RAFT=7003 P2P=4003 # Usage: make run-node3 BOOTSTRAP=/ip4/127.0.0.1/tcp/4001/p2p/<ID> HTTP=5003 RAFT=7003 P2P=4003
run-node3: run-node3:
@echo "Starting REGULAR node3 (role=node)..." @echo "Starting REGULAR node3 (role=node)..."
@if [ -z "$(BOOTSTRAP)" ]; then echo "ERROR: Provide BOOTSTRAP multiaddr: make run-node3 BOOTSTRAP=/ip4/127.0.0.1/tcp/4001/p2p/<ID> [HTTP=5003 RAFT=7003 P2P=4003]"; exit 1; fi @if [ -z "$(BOOTSTRAP)" ]; then echo "ERROR: Provide BOOTSTRAP multiaddr: make run-node3 BOOTSTRAP=/ip4/127.0.0.1/tcp/4001/p2p/<ID> [HTTP=5003 RAFT=7003 P2P=4003]"; exit 1; fi
go run ./cmd/node -role node -id node3 -data ./data/node3 -bootstrap $(BOOTSTRAP) -rqlite-http-port $${HTTP:-5003} -rqlite-raft-port $${RAFT:-7003} -p2p-port $${P2P:-4003} -advertise $${ADVERTISE:-localhost} go run ./cmd/node -role node -id node3 -data ./data/node3 -bootstrap $(BOOTSTRAP) -rqlite-http-port $${HTTP:-5003} -rqlite-raft-port $${RAFT:-7003} -p2p-port $${P2P:-4003} -advertise $${ADVERTISE:-localhost} -dev-local
# Run basic usage example # Run basic usage example
run-example: run-example:

View File

@ -187,7 +187,7 @@ make build
```bash ```bash
# Start an explicit bootstrap node (LibP2P 4001, RQLite 5001/7001) # Start an explicit bootstrap node (LibP2P 4001, RQLite 5001/7001)
go run ./cmd/node -role bootstrap -data ./data/bootstrap go run ./cmd/node -role bootstrap -data ./data/bootstrap -dev-local
``` ```
**Terminal 2 - Regular Node:** **Terminal 2 - Regular Node:**
@ -200,7 +200,8 @@ go run ./cmd/node \
-data ./data/node2 \ -data ./data/node2 \
-bootstrap /ip4/127.0.0.1/tcp/4001/p2p/<BOOTSTRAP_PEER_ID> \ -bootstrap /ip4/127.0.0.1/tcp/4001/p2p/<BOOTSTRAP_PEER_ID> \
-rqlite-http-port 5002 \ -rqlite-http-port 5002 \
-rqlite-raft-port 7002 -rqlite-raft-port 7002 \
-dev-local
``` ```
**Terminal 3 - Another Node (optional):** **Terminal 3 - Another Node (optional):**
@ -421,6 +422,7 @@ For more advanced configuration options and development setup, see the sections
- **Bootstrap node**: `-role bootstrap` - **Bootstrap node**: `-role bootstrap`
- **Regular node**: `-role node -bootstrap <multiaddr>` - **Regular node**: `-role node -bootstrap <multiaddr>`
- **Development localhost defaults**: `-dev-local` (sets `NETWORK_DEV_LOCAL=1` in-process); use this for local-only testing so the library returns localhost DB endpoints and bootstrap peers.
- **RQLite ports**: `-rqlite-http-port` (default 5001), `-rqlite-raft-port` (default 7001) - **RQLite ports**: `-rqlite-http-port` (default 5001), `-rqlite-raft-port` (default 7001)
Examples are shown in Quick Start above for local multi-node on a single machine. Examples are shown in Quick Start above for local multi-node on a single machine.

View File

@ -2,9 +2,11 @@ package main
import ( import (
"fmt" "fmt"
"os"
"strings" "strings"
"git.debros.io/DeBros/network/pkg/config" "git.debros.io/DeBros/network/pkg/config"
"git.debros.io/DeBros/network/pkg/client"
"git.debros.io/DeBros/network/pkg/constants" "git.debros.io/DeBros/network/pkg/constants"
"git.debros.io/DeBros/network/pkg/logging" "git.debros.io/DeBros/network/pkg/logging"
) )
@ -21,6 +23,17 @@ type NodeFlagValues struct {
Advertise string Advertise string
} }
// isTruthyEnv returns true if the environment variable is set to a common truthy value
func isTruthyEnv(key string) bool {
v := strings.ToLower(strings.TrimSpace(os.Getenv(key)))
switch v {
case "1", "true", "yes", "on":
return true
default:
return false
}
}
// MapFlagsAndEnvToConfig applies environment overrides and CLI flags to cfg. // MapFlagsAndEnvToConfig applies environment overrides and CLI flags to cfg.
// Precedence: flags > env > defaults. Behavior mirrors previous inline logic in main.go. // Precedence: flags > env > defaults. Behavior mirrors previous inline logic in main.go.
// Returns the derived RQLite Raft join address for non-bootstrap nodes (empty for bootstrap nodes). // Returns the derived RQLite Raft join address for non-bootstrap nodes (empty for bootstrap nodes).
@ -28,6 +41,9 @@ func MapFlagsAndEnvToConfig(cfg *config.Config, fv NodeFlagValues, isBootstrap b
// Apply environment variable overrides first so that flags can override them after // Apply environment variable overrides first so that flags can override them after
config.ApplyEnvOverrides(cfg) config.ApplyEnvOverrides(cfg)
// Detect dev-local mode (set via -dev-local -> NETWORK_DEV_LOCAL=1)
devLocal := isTruthyEnv("NETWORK_DEV_LOCAL")
// Determine data directory if not provided // Determine data directory if not provided
if fv.DataDir == "" { if fv.DataDir == "" {
if isBootstrap { if isBootstrap {
@ -56,6 +72,14 @@ func MapFlagsAndEnvToConfig(cfg *config.Config, fv NodeFlagValues, isBootstrap b
// Bootstrap-specific vs regular-node logic // Bootstrap-specific vs regular-node logic
if isBootstrap { if isBootstrap {
if devLocal {
// In dev-local, run a primary bootstrap locally
cfg.Database.RQLiteJoinAddress = ""
// Also prefer localhost bootstrap peers for any consumers reading cfg
cfg.Discovery.BootstrapPeers = client.DefaultBootstrapPeers()
logger.Printf("Dev-local: Primary bootstrap node - localhost defaults enabled")
return ""
}
bootstrapPeers := constants.GetBootstrapPeers() bootstrapPeers := constants.GetBootstrapPeers()
isSecondaryBootstrap := false isSecondaryBootstrap := false
if len(bootstrapPeers) > 1 { if len(bootstrapPeers) > 1 {
@ -103,6 +127,11 @@ func MapFlagsAndEnvToConfig(cfg *config.Config, fv NodeFlagValues, isBootstrap b
logger.Printf("Using command line bootstrap peer: %s", fv.Bootstrap) logger.Printf("Using command line bootstrap peer: %s", fv.Bootstrap)
} else { } else {
bootstrapPeers := cfg.Discovery.BootstrapPeers bootstrapPeers := cfg.Discovery.BootstrapPeers
if devLocal {
// Force localhost bootstrap for development
bootstrapPeers = client.DefaultBootstrapPeers()
logger.Printf("Dev-local: overriding bootstrap peers to %v", bootstrapPeers)
}
if len(bootstrapPeers) == 0 { if len(bootstrapPeers) == 0 {
bootstrapPeers = constants.GetBootstrapPeers() bootstrapPeers = constants.GetBootstrapPeers()
} }

View File

@ -29,6 +29,7 @@ func main() {
rqlHTTP = flag.Int("rqlite-http-port", 5001, "RQLite HTTP API port") rqlHTTP = flag.Int("rqlite-http-port", 5001, "RQLite HTTP API port")
rqlRaft = flag.Int("rqlite-raft-port", 7001, "RQLite Raft port") rqlRaft = flag.Int("rqlite-raft-port", 7001, "RQLite Raft port")
advertise = flag.String("advertise", "auto", "Advertise mode: auto|localhost|ip") advertise = flag.String("advertise", "auto", "Advertise mode: auto|localhost|ip")
devLocal = flag.Bool("dev-local", false, "Enable development localhost defaults for the client library (sets NETWORK_DEV_LOCAL=1)")
help = flag.Bool("help", false, "Show help") help = flag.Bool("help", false, "Show help")
) )
flag.Parse() flag.Parse()
@ -38,6 +39,12 @@ func main() {
return return
} }
// Enable development localhost defaults for the client library if requested
if *devLocal {
os.Setenv("NETWORK_DEV_LOCAL", "1")
log.Printf("Development local mode enabled (NETWORK_DEV_LOCAL=1)")
}
// Determine node role // Determine node role
var isBootstrap bool var isBootstrap bool
switch strings.ToLower(*role) { switch strings.ToLower(*role) {

View File

@ -2,6 +2,7 @@ package client
import ( import (
"os" "os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
@ -16,6 +17,10 @@ func DefaultBootstrapPeers() []string {
if ma := os.Getenv("LOCAL_BOOTSTRAP_MULTIADDR"); ma != "" { if ma := os.Getenv("LOCAL_BOOTSTRAP_MULTIADDR"); ma != "" {
return []string{ma} return []string{ma}
} }
// Try to auto-resolve local bootstrap peer multiaddr from peer.info
if ma, ok := readLocalPeerInfoMultiaddr(); ok {
return []string{ma}
}
// Fallback to localhost transport without peer ID (connect will warn and skip) // Fallback to localhost transport without peer ID (connect will warn and skip)
return []string{"/ip4/127.0.0.1/tcp/4001"} return []string{"/ip4/127.0.0.1/tcp/4001"}
} }
@ -84,27 +89,27 @@ func MapAddrsToDBEndpoints(addrs []multiaddr.Multiaddr, dbPort int) []string {
} }
func endpointFromMultiaddr(ma multiaddr.Multiaddr, port int) string { func endpointFromMultiaddr(ma multiaddr.Multiaddr, port int) string {
var host string var host string
// Prefer DNS if present, then IP // Prefer DNS if present, then IP
if v, err := ma.ValueForProtocol(multiaddr.P_DNS); err == nil && v != "" { if v, err := ma.ValueForProtocol(multiaddr.P_DNS); err == nil && v != "" {
host = v host = v
} }
if host == "" { if host == "" {
if v, err := ma.ValueForProtocol(multiaddr.P_DNS4); err == nil && v != "" { host = v } if v, err := ma.ValueForProtocol(multiaddr.P_DNS4); err == nil && v != "" { host = v }
} }
if host == "" { if host == "" {
if v, err := ma.ValueForProtocol(multiaddr.P_DNS6); err == nil && v != "" { host = v } if v, err := ma.ValueForProtocol(multiaddr.P_DNS6); err == nil && v != "" { host = v }
} }
if host == "" { if host == "" {
if v, err := ma.ValueForProtocol(multiaddr.P_IP4); err == nil && v != "" { host = v } if v, err := ma.ValueForProtocol(multiaddr.P_IP4); err == nil && v != "" { host = v }
} }
if host == "" { if host == "" {
if v, err := ma.ValueForProtocol(multiaddr.P_IP6); err == nil && v != "" { host = v } if v, err := ma.ValueForProtocol(multiaddr.P_IP6); err == nil && v != "" { host = v }
} }
if host == "" { if host == "" {
host = "localhost" host = "localhost"
} }
return "http://" + host + ":" + strconv.Itoa(port) return "http://" + host + ":" + strconv.Itoa(port)
} }
func dedupeStrings(in []string) []string { func dedupeStrings(in []string) []string {
@ -123,3 +128,32 @@ func dedupeStrings(in []string) []string {
} }
return out return out
} }
// readLocalPeerInfoMultiaddr attempts to read the local bootstrap peer multiaddr from common dev paths.
// It checks LOCAL_BOOTSTRAP_INFO (path), then ./data/bootstrap/peer.info, then ./data/peer.info.
func readLocalPeerInfoMultiaddr() (string, bool) {
candidates := make([]string, 0, 3)
if p := strings.TrimSpace(os.Getenv("LOCAL_BOOTSTRAP_INFO")); p != "" {
candidates = append(candidates, p)
}
candidates = append(candidates,
filepath.Join(".", "data", "bootstrap", "peer.info"),
filepath.Join(".", "data", "peer.info"),
)
for _, p := range candidates {
b, err := os.ReadFile(p)
if err != nil {
continue
}
s := strings.TrimSpace(string(b))
if s == "" {
continue
}
// expect a full multiaddr with /p2p/<peerID>
if strings.Contains(s, "/p2p/") {
return s, true
}
}
return "", false
}