mirror of
https://github.com/DeBrosOfficial/network.git
synced 2025-12-13 01:18:49 +00:00
commit
42131c0e75
89
.githooks/pre-commit
Normal file
89
.githooks/pre-commit
Normal file
@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Colors for output
|
||||
CYAN='\033[0;36m'
|
||||
YELLOW='\033[1;33m'
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
BLUE='\033[0;34m'
|
||||
NOCOLOR='\033[0m'
|
||||
|
||||
# Get the directory where this hook is located
|
||||
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# Go up from .git/hooks/ to repo root
|
||||
REPO_ROOT="$(cd "$HOOK_DIR/../.." && pwd)"
|
||||
CHANGELOG_SCRIPT="$REPO_ROOT/scripts/update_changelog.sh"
|
||||
PREVIEW_FILE="$REPO_ROOT/.changelog_preview.tmp"
|
||||
VERSION_FILE="$REPO_ROOT/.changelog_version.tmp"
|
||||
|
||||
# Only run changelog update if there are actual code changes (not just changelog files)
|
||||
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
||||
if [ -z "$STAGED_FILES" ]; then
|
||||
# No staged files, exit
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if only CHANGELOG.md and/or Makefile are being committed
|
||||
OTHER_FILES=$(echo "$STAGED_FILES" | grep -v "^CHANGELOG.md$" | grep -v "^Makefile$")
|
||||
if [ -z "$OTHER_FILES" ]; then
|
||||
# Only changelog files are being committed, skip update
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Update changelog before commit
|
||||
if [ -f "$CHANGELOG_SCRIPT" ]; then
|
||||
echo -e "\n${CYAN}Updating changelog...${NOCOLOR}"
|
||||
|
||||
# Set environment variable to indicate we're running from pre-commit
|
||||
export CHANGELOG_CONTEXT=pre-commit
|
||||
|
||||
bash "$CHANGELOG_SCRIPT"
|
||||
changelog_status=$?
|
||||
if [ $changelog_status -ne 0 ]; then
|
||||
echo -e "${RED}Commit aborted: changelog update failed.${NOCOLOR}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Show preview if changelog was updated
|
||||
if [ -f "$PREVIEW_FILE" ] && [ -f "$VERSION_FILE" ]; then
|
||||
NEW_VERSION=$(cat "$VERSION_FILE")
|
||||
PREVIEW_CONTENT=$(cat "$PREVIEW_FILE")
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}========================================================================${NOCOLOR}"
|
||||
echo -e "${CYAN} CHANGELOG PREVIEW${NOCOLOR}"
|
||||
echo -e "${BLUE}========================================================================${NOCOLOR}"
|
||||
echo ""
|
||||
echo -e "${GREEN}New Version: ${YELLOW}$NEW_VERSION${NOCOLOR}"
|
||||
echo ""
|
||||
echo -e "${CYAN}Changelog Entry:${NOCOLOR}"
|
||||
echo -e "${BLUE}────────────────────────────────────────────────────────────────────────${NOCOLOR}"
|
||||
echo -e "$PREVIEW_CONTENT"
|
||||
echo -e "${BLUE}────────────────────────────────────────────────────────────────────────${NOCOLOR}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Do you want to proceed with the commit? (yes/no):${NOCOLOR} "
|
||||
# Read from /dev/tty to ensure we can read from terminal even in git hook context
|
||||
read -r confirmation < /dev/tty
|
||||
|
||||
if [ "$confirmation" != "yes" ]; then
|
||||
echo -e "${RED}Commit aborted by user.${NOCOLOR}"
|
||||
echo -e "${YELLOW}To revert changes, run:${NOCOLOR}"
|
||||
echo -e " git checkout CHANGELOG.md Makefile"
|
||||
# Clean up temp files
|
||||
rm -f "$PREVIEW_FILE" "$VERSION_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Proceeding with commit...${NOCOLOR}"
|
||||
|
||||
# Add the updated CHANGELOG.md and Makefile to the current commit
|
||||
echo -e "${CYAN}Staging CHANGELOG.md and Makefile...${NOCOLOR}"
|
||||
git add CHANGELOG.md Makefile
|
||||
|
||||
# Clean up temp files
|
||||
rm -f "$PREVIEW_FILE" "$VERSION_FILE"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}Warning: changelog update script not found at $CHANGELOG_SCRIPT${NOCOLOR}"
|
||||
fi
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo -e "\nRunning tests:"
|
||||
# Colors for output
|
||||
CYAN='\033[0;36m'
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
NOCOLOR='\033[0m'
|
||||
|
||||
# Run tests before push
|
||||
echo -e "\n${CYAN}Running tests...${NOCOLOR}"
|
||||
go test ./... # Runs all tests in your repo
|
||||
status=$?
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "Push aborted: some tests failed."
|
||||
echo -e "${RED}Push aborted: some tests failed.${NOCOLOR}"
|
||||
exit 1
|
||||
else
|
||||
echo "All tests passed. Proceeding with push."
|
||||
echo -e "${GREEN}All tests passed. Proceeding with push.${NOCOLOR}"
|
||||
fi
|
||||
|
||||
138
CHANGELOG.md
138
CHANGELOG.md
@ -13,6 +13,144 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant
|
||||
### Deprecated
|
||||
|
||||
### Fixed
|
||||
## [0.53.18] - 2025-11-03
|
||||
|
||||
### Added
|
||||
\n
|
||||
### Changed
|
||||
- Increased the connection timeout during peer discovery from 15 seconds to 20 seconds to improve connection reliability.
|
||||
- Removed unnecessary debug logging related to filtering out ephemeral port addresses during peer exchange.
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
\n
|
||||
## [0.53.17] - 2025-11-03
|
||||
|
||||
### Added
|
||||
- Added a new Git `pre-commit` hook to automatically update the changelog and version before committing, ensuring version consistency.
|
||||
|
||||
### Changed
|
||||
- Refactored the `update_changelog.sh` script to support different execution contexts (pre-commit vs. pre-push), allowing it to analyze only staged changes during commit.
|
||||
- The Git `pre-push` hook was simplified by removing the changelog update logic, which is now handled by the `pre-commit` hook.
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
\n
|
||||
## [0.53.16] - 2025-11-03
|
||||
|
||||
### Added
|
||||
\n
|
||||
### Changed
|
||||
- Improved the changelog generation script to prevent infinite loops when the only unpushed commit is a previous changelog update.
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
\n
|
||||
## [0.53.15] - 2025-11-03
|
||||
|
||||
### Added
|
||||
\n
|
||||
### Changed
|
||||
- Improved the pre-push git hook to automatically commit updated changelog and Makefile after generation.
|
||||
- Updated the changelog generation script to load the OpenRouter API key from the .env file or environment variables for better security.
|
||||
- Modified the pre-push hook to read user confirmation from /dev/tty for better compatibility.
|
||||
- Updated the bootstrap peer logic to prioritize the DEBROS_BOOTSTRAP_PEERS environment variable for easier configuration.
|
||||
- Improved the gateway's private host check to correctly handle IPv6 addresses with or without brackets and ports.
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
\n
|
||||
## [0.53.15] - 2025-11-03
|
||||
|
||||
### Added
|
||||
\n
|
||||
### Changed
|
||||
- Improved the pre-push git hook to automatically commit updated changelog and Makefile after generation.
|
||||
- Updated the changelog generation script to load the OpenRouter API key from the .env file or environment variables for better security.
|
||||
- Modified the pre-push hook to read user confirmation from /dev/tty for better compatibility.
|
||||
- Updated the bootstrap peer logic to prioritize the DEBROS_BOOTSTRAP_PEERS environment variable for easier configuration.
|
||||
- Improved the gateway's private host check to correctly handle IPv6 addresses with or without brackets and ports.
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
\n
|
||||
## [0.53.14] - 2025-11-03
|
||||
|
||||
### Added
|
||||
- Added a new `install-hooks` target to the Makefile to easily set up git hooks.
|
||||
- Added a script (`scripts/install-hooks.sh`) to copy git hooks from `.githooks` to `.git/hooks`.
|
||||
|
||||
### Changed
|
||||
- Improved the pre-push git hook to automatically commit the updated `CHANGELOG.md` and `Makefile` after generating the changelog.
|
||||
- Updated the changelog generation script (`scripts/update_changelog.sh`) to load the OpenRouter API key from the `.env` file or environment variables, improving security and configuration.
|
||||
- Modified the pre-push hook to read user confirmation from `/dev/tty` for better compatibility in various terminal environments.
|
||||
- Updated the bootstrap peer logic to check the `DEBROS_BOOTSTRAP_PEERS` environment variable first, allowing easier configuration override.
|
||||
- Improved the gateway's private host check to correctly handle IPv6 addresses with or without brackets and ports.
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
\n
|
||||
## [0.53.14] - 2025-11-03
|
||||
|
||||
### Added
|
||||
- Added a new `install-hooks` target to the Makefile to easily set up git hooks.
|
||||
- Added a script (`scripts/install-hooks.sh`) to copy git hooks from `.githooks` to `.git/hooks`.
|
||||
|
||||
### Changed
|
||||
- Improved the pre-push git hook to automatically commit the updated `CHANGELOG.md` and `Makefile` after generating the changelog.
|
||||
- Updated the changelog generation script (`scripts/update_changelog.sh`) to load the OpenRouter API key from the `.env` file or environment variables, improving security and configuration.
|
||||
- Modified the pre-push hook to read user confirmation from `/dev/tty` for better compatibility in various terminal environments.
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
\n
|
||||
|
||||
## [0.53.8] - 2025-10-31
|
||||
|
||||
### Added
|
||||
|
||||
- **HTTPS/ACME Support**: Gateway now supports automatic HTTPS with Let's Encrypt certificates via ACME
|
||||
- Interactive domain configuration during `network-cli setup` command
|
||||
- Automatic port availability checking for ports 80 and 443 before enabling HTTPS
|
||||
- DNS resolution verification to ensure domain points to the server IP
|
||||
- TLS certificate cache directory management (`~/.debros/tls-cache`)
|
||||
- Gateway automatically serves HTTP (port 80) for ACME challenges and HTTPS (port 443) for traffic
|
||||
- New gateway config fields: `enable_https`, `domain_name`, `tls_cache_dir`
|
||||
- **Domain Validation**: Added domain name validation and DNS verification helpers in setup CLI
|
||||
- **Port Checking**: Added port availability checking utilities to detect conflicts before HTTPS setup
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated `generateGatewayConfigDirect` to include HTTPS configuration fields
|
||||
- Enhanced gateway config parsing to support HTTPS settings with validation
|
||||
- Modified gateway startup to handle both HTTP-only and HTTPS+ACME modes
|
||||
- Gateway now automatically manages ACME certificate acquisition and renewal
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved error handling during HTTPS setup with clear messaging when ports are unavailable
|
||||
- Enhanced DNS verification flow with better user feedback during setup
|
||||
|
||||
## [0.53.0] - 2025-10-31
|
||||
|
||||
|
||||
9
Makefile
9
Makefile
@ -19,9 +19,9 @@ test-e2e:
|
||||
# Network - Distributed P2P Database System
|
||||
# Makefile for development and build tasks
|
||||
|
||||
.PHONY: build clean test run-node run-node2 run-node3 run-example deps tidy fmt vet lint clear-ports
|
||||
.PHONY: build clean test run-node run-node2 run-node3 run-example deps tidy fmt vet lint clear-ports install-hooks
|
||||
|
||||
VERSION := 0.53.3
|
||||
VERSION := 0.53.18
|
||||
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)' -X 'main.date=$(DATE)'
|
||||
@ -37,6 +37,11 @@ build: deps
|
||||
go build -ldflags "$(LDFLAGS) -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildVersion=$(VERSION)' -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildCommit=$(COMMIT)' -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildTime=$(DATE)'" -o bin/gateway ./cmd/gateway
|
||||
@echo "Build complete! Run ./bin/network-cli version"
|
||||
|
||||
# Install git hooks
|
||||
install-hooks:
|
||||
@echo "Installing git hooks..."
|
||||
@bash scripts/install-hooks.sh
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
@echo "Cleaning build artifacts..."
|
||||
|
||||
@ -108,6 +108,10 @@ func main() {
|
||||
}
|
||||
cli.HandleConnectCommand(args[0], timeout)
|
||||
|
||||
// RQLite commands
|
||||
case "rqlite":
|
||||
cli.HandleRQLiteCommand(args)
|
||||
|
||||
// Help
|
||||
case "help", "--help", "-h":
|
||||
showHelp()
|
||||
@ -175,6 +179,9 @@ func showHelp() {
|
||||
fmt.Printf("🗄️ Database:\n")
|
||||
fmt.Printf(" query <sql> 🔐 Execute database query\n\n")
|
||||
|
||||
fmt.Printf("🔧 RQLite:\n")
|
||||
fmt.Printf(" rqlite fix 🔧 Fix misconfigured join address and clean raft state\n\n")
|
||||
|
||||
fmt.Printf("📡 PubSub:\n")
|
||||
fmt.Printf(" pubsub publish <topic> <msg> 🔐 Publish message\n")
|
||||
fmt.Printf(" pubsub subscribe <topic> 🔐 Subscribe to topic\n")
|
||||
|
||||
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/config"
|
||||
@ -53,6 +54,9 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
|
||||
ClientNamespace string `yaml:"client_namespace"`
|
||||
RQLiteDSN string `yaml:"rqlite_dsn"`
|
||||
BootstrapPeers []string `yaml:"bootstrap_peers"`
|
||||
EnableHTTPS bool `yaml:"enable_https"`
|
||||
DomainName string `yaml:"domain_name"`
|
||||
TLSCacheDir string `yaml:"tls_cache_dir"`
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
@ -79,6 +83,9 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
|
||||
ClientNamespace: "default",
|
||||
BootstrapPeers: nil,
|
||||
RQLiteDSN: "",
|
||||
EnableHTTPS: false,
|
||||
DomainName: "",
|
||||
TLSCacheDir: "",
|
||||
}
|
||||
|
||||
if v := strings.TrimSpace(y.ListenAddr); v != "" {
|
||||
@ -103,6 +110,21 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPS configuration
|
||||
cfg.EnableHTTPS = y.EnableHTTPS
|
||||
if v := strings.TrimSpace(y.DomainName); v != "" {
|
||||
cfg.DomainName = v
|
||||
}
|
||||
if v := strings.TrimSpace(y.TLSCacheDir); v != "" {
|
||||
cfg.TLSCacheDir = v
|
||||
} else if cfg.EnableHTTPS {
|
||||
// Default TLS cache directory if HTTPS is enabled but not specified
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
cfg.TLSCacheDir = filepath.Join(homeDir, ".debros", "tls-cache")
|
||||
}
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
if errs := cfg.ValidateConfig(); len(errs) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "\nGateway configuration errors (%d):\n", len(errs))
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
"github.com/DeBrosOfficial/network/pkg/gateway"
|
||||
"github.com/DeBrosOfficial/network/pkg/logging"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
func setupLogger() *logging.ColoredLogger {
|
||||
@ -42,6 +43,123 @@ func main() {
|
||||
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "Creating HTTP server and routes...")
|
||||
|
||||
// Check if HTTPS is enabled
|
||||
if cfg.EnableHTTPS && cfg.DomainName != "" {
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "HTTPS enabled with ACME",
|
||||
zap.String("domain", cfg.DomainName),
|
||||
zap.String("tls_cache_dir", cfg.TLSCacheDir),
|
||||
)
|
||||
|
||||
// Set up ACME manager
|
||||
manager := &autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
HostPolicy: autocert.HostWhitelist(cfg.DomainName),
|
||||
}
|
||||
|
||||
// Set cache directory if specified
|
||||
if cfg.TLSCacheDir != "" {
|
||||
manager.Cache = autocert.DirCache(cfg.TLSCacheDir)
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "Using TLS certificate cache",
|
||||
zap.String("cache_dir", cfg.TLSCacheDir),
|
||||
)
|
||||
}
|
||||
|
||||
// Create HTTP server for ACME challenge (port 80)
|
||||
httpServer := &http.Server{
|
||||
Addr: ":80",
|
||||
Handler: manager.HTTPHandler(nil), // Redirects all HTTP traffic to HTTPS except ACME challenge
|
||||
}
|
||||
|
||||
// Create HTTPS server (port 443)
|
||||
httpsServer := &http.Server{
|
||||
Addr: ":443",
|
||||
Handler: gw.Routes(),
|
||||
TLSConfig: manager.TLSConfig(),
|
||||
}
|
||||
|
||||
// Start HTTP server for ACME challenge
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "Starting HTTP server for ACME challenge on port 80...")
|
||||
httpLn, err := net.Listen("tcp", ":80")
|
||||
if err != nil {
|
||||
logger.ComponentError(logging.ComponentGeneral, "failed to bind HTTP listen address (port 80)", zap.Error(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "HTTP listener bound", zap.String("listen_addr", httpLn.Addr().String()))
|
||||
|
||||
// Start HTTPS server
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "Starting HTTPS server on port 443...")
|
||||
httpsLn, err := net.Listen("tcp", ":443")
|
||||
if err != nil {
|
||||
logger.ComponentError(logging.ComponentGeneral, "failed to bind HTTPS listen address (port 443)", zap.Error(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "HTTPS listener bound", zap.String("listen_addr", httpsLn.Addr().String()))
|
||||
|
||||
// Serve HTTP in a goroutine
|
||||
httpServeErrCh := make(chan error, 1)
|
||||
go func() {
|
||||
if err := httpServer.Serve(httpLn); err != nil && err != http.ErrServerClosed {
|
||||
httpServeErrCh <- err
|
||||
return
|
||||
}
|
||||
httpServeErrCh <- nil
|
||||
}()
|
||||
|
||||
// Serve HTTPS in a goroutine
|
||||
httpsServeErrCh := make(chan error, 1)
|
||||
go func() {
|
||||
if err := httpsServer.ServeTLS(httpsLn, "", ""); err != nil && err != http.ErrServerClosed {
|
||||
httpsServeErrCh <- err
|
||||
return
|
||||
}
|
||||
httpsServeErrCh <- nil
|
||||
}()
|
||||
|
||||
// Wait for termination signal or server error
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
select {
|
||||
case sig := <-quit:
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "shutdown signal received", zap.String("signal", sig.String()))
|
||||
case err := <-httpServeErrCh:
|
||||
if err != nil {
|
||||
logger.ComponentError(logging.ComponentGeneral, "HTTP server error", zap.Error(err))
|
||||
} else {
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "HTTP server exited normally")
|
||||
}
|
||||
case err := <-httpsServeErrCh:
|
||||
if err != nil {
|
||||
logger.ComponentError(logging.ComponentGeneral, "HTTPS server error", zap.Error(err))
|
||||
} else {
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "HTTPS server exited normally")
|
||||
}
|
||||
}
|
||||
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "Shutting down gateway servers...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Shutdown HTTPS server
|
||||
if err := httpsServer.Shutdown(ctx); err != nil {
|
||||
logger.ComponentError(logging.ComponentGeneral, "HTTPS server shutdown error", zap.Error(err))
|
||||
} else {
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "HTTPS server shutdown complete")
|
||||
}
|
||||
|
||||
// Shutdown HTTP server
|
||||
if err := httpServer.Shutdown(ctx); err != nil {
|
||||
logger.ComponentError(logging.ComponentGeneral, "HTTP server shutdown error", zap.Error(err))
|
||||
} else {
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "HTTP server shutdown complete")
|
||||
}
|
||||
|
||||
logger.ComponentInfo(logging.ComponentGeneral, "Gateway shutdown complete")
|
||||
return
|
||||
}
|
||||
|
||||
// Standard HTTP server (no HTTPS)
|
||||
server := &http.Server{
|
||||
Addr: cfg.ListenAddr,
|
||||
Handler: gw.Routes(),
|
||||
|
||||
@ -1,151 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/client"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create client configuration
|
||||
config := client.DefaultClientConfig("example_app")
|
||||
config.BootstrapPeers = []string{
|
||||
"/ip4/127.0.0.1/tcp/4001/p2p/QmBootstrap1",
|
||||
}
|
||||
|
||||
// Create network client
|
||||
networkClient, err := client.NewClient(config)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create network client: %v", err)
|
||||
}
|
||||
|
||||
// Connect to network
|
||||
if err := networkClient.Connect(); err != nil {
|
||||
log.Fatalf("Failed to connect to network: %v", err)
|
||||
}
|
||||
defer networkClient.Disconnect()
|
||||
|
||||
log.Printf("Connected to network successfully!")
|
||||
|
||||
// Example: Database operations
|
||||
demonstrateDatabase(networkClient)
|
||||
|
||||
// Example: Pub/Sub messaging
|
||||
demonstratePubSub(networkClient)
|
||||
|
||||
// Example: Network information
|
||||
demonstrateNetworkInfo(networkClient)
|
||||
|
||||
log.Printf("Example completed successfully!")
|
||||
}
|
||||
|
||||
func demonstrateDatabase(client client.NetworkClient) {
|
||||
ctx := context.Background()
|
||||
db := client.Database()
|
||||
|
||||
log.Printf("=== Database Operations ===")
|
||||
|
||||
// Create a table
|
||||
schema := `
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY,
|
||||
content TEXT NOT NULL,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`
|
||||
if err := db.CreateTable(ctx, schema); err != nil {
|
||||
log.Printf("Error creating table: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Table created successfully")
|
||||
|
||||
// Insert some data
|
||||
insertSQL := "INSERT INTO messages (content) VALUES (?)"
|
||||
result, err := db.Query(ctx, insertSQL, "Hello, distributed world!")
|
||||
if err != nil {
|
||||
log.Printf("Error inserting data: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Data inserted, result: %+v", result)
|
||||
|
||||
// Query data
|
||||
selectSQL := "SELECT * FROM messages"
|
||||
result, err = db.Query(ctx, selectSQL)
|
||||
if err != nil {
|
||||
log.Printf("Error querying data: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Query result: %+v", result)
|
||||
}
|
||||
|
||||
func demonstratePubSub(client client.NetworkClient) {
|
||||
ctx := context.Background()
|
||||
pubsub := client.PubSub()
|
||||
|
||||
log.Printf("=== Pub/Sub Operations ===")
|
||||
|
||||
// Subscribe to a topic
|
||||
topic := "notifications"
|
||||
handler := func(topic string, data []byte) error {
|
||||
log.Printf("Received message on topic '%s': %s", topic, string(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := pubsub.Subscribe(ctx, topic, handler); err != nil {
|
||||
log.Printf("Error subscribing: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Subscribed to topic: %s", topic)
|
||||
|
||||
// Publish a message
|
||||
message := []byte("Hello from pub/sub!")
|
||||
if err := pubsub.Publish(ctx, topic, message); err != nil {
|
||||
log.Printf("Error publishing: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Message published")
|
||||
|
||||
// Wait a bit for message delivery
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
|
||||
// List topics
|
||||
topics, err := pubsub.ListTopics(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Error listing topics: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Subscribed topics: %v", topics)
|
||||
}
|
||||
|
||||
func demonstrateNetworkInfo(client client.NetworkClient) {
|
||||
ctx := context.Background()
|
||||
network := client.Network()
|
||||
|
||||
log.Printf("=== Network Information ===")
|
||||
|
||||
// Get network status
|
||||
status, err := network.GetStatus(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Error getting status: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Network status: %+v", status)
|
||||
|
||||
// Get peers
|
||||
peers, err := network.GetPeers(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Error getting peers: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Connected peers: %+v", peers)
|
||||
|
||||
// Get client health
|
||||
health, err := client.Health()
|
||||
if err != nil {
|
||||
log.Printf("Error getting health: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Client health: %+v", health)
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
# DeBros Gateway TypeScript SDK (Minimal Example)
|
||||
|
||||
Minimal, dependency-light wrapper around the HTTP Gateway.
|
||||
|
||||
Usage:
|
||||
|
||||
```bash
|
||||
npm i
|
||||
export GATEWAY_BASE_URL=http://127.0.0.1:6001
|
||||
export GATEWAY_API_KEY=your_api_key
|
||||
```
|
||||
|
||||
```ts
|
||||
import { GatewayClient } from './src/client';
|
||||
|
||||
const c = new GatewayClient(process.env.GATEWAY_BASE_URL!, process.env.GATEWAY_API_KEY!);
|
||||
await c.createTable('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
|
||||
await c.transaction([
|
||||
'INSERT INTO users (id,name) VALUES (1,\'Alice\')'
|
||||
]);
|
||||
const res = await c.query('SELECT name FROM users WHERE id = ?', [1]);
|
||||
console.log(res.rows);
|
||||
```
|
||||
@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "debros-gateway-sdk",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"isomorphic-ws": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
@ -1,154 +0,0 @@
|
||||
import WebSocket from "isomorphic-ws";
|
||||
|
||||
export class GatewayClient {
|
||||
constructor(
|
||||
private baseUrl: string,
|
||||
private apiKey: string,
|
||||
private http = fetch
|
||||
) {}
|
||||
|
||||
private headers(json = true): Record<string, string> {
|
||||
const h: Record<string, string> = { "X-API-Key": this.apiKey };
|
||||
if (json) h["Content-Type"] = "application/json";
|
||||
return h;
|
||||
}
|
||||
|
||||
// Database
|
||||
async createTable(schema: string): Promise<void> {
|
||||
const r = await this.http(`${this.baseUrl}/v1/rqlite/create-table`, {
|
||||
method: "POST",
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ schema }),
|
||||
});
|
||||
if (!r.ok) throw new Error(`createTable failed: ${r.status}`);
|
||||
}
|
||||
|
||||
async dropTable(table: string): Promise<void> {
|
||||
const r = await this.http(`${this.baseUrl}/v1/rqlite/drop-table`, {
|
||||
method: "POST",
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ table }),
|
||||
});
|
||||
if (!r.ok) throw new Error(`dropTable failed: ${r.status}`);
|
||||
}
|
||||
|
||||
async query<T = any>(sql: string, args: any[] = []): Promise<{ rows: T[] }> {
|
||||
const r = await this.http(`${this.baseUrl}/v1/rqlite/query`, {
|
||||
method: "POST",
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ sql, args }),
|
||||
});
|
||||
if (!r.ok) throw new Error(`query failed: ${r.status}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
async transaction(statements: string[]): Promise<void> {
|
||||
const r = await this.http(`${this.baseUrl}/v1/rqlite/transaction`, {
|
||||
method: "POST",
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ statements }),
|
||||
});
|
||||
if (!r.ok) throw new Error(`transaction failed: ${r.status}`);
|
||||
}
|
||||
|
||||
async schema(): Promise<any> {
|
||||
const r = await this.http(`${this.baseUrl}/v1/rqlite/schema`, {
|
||||
headers: this.headers(false),
|
||||
});
|
||||
if (!r.ok) throw new Error(`schema failed: ${r.status}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
// Storage
|
||||
async put(key: string, value: Uint8Array | string): Promise<void> {
|
||||
const body =
|
||||
typeof value === "string" ? new TextEncoder().encode(value) : value;
|
||||
const r = await this.http(
|
||||
`${this.baseUrl}/v1/storage/put?key=${encodeURIComponent(key)}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "X-API-Key": this.apiKey },
|
||||
body,
|
||||
}
|
||||
);
|
||||
if (!r.ok) throw new Error(`put failed: ${r.status}`);
|
||||
}
|
||||
|
||||
async get(key: string): Promise<Uint8Array> {
|
||||
const r = await this.http(
|
||||
`${this.baseUrl}/v1/storage/get?key=${encodeURIComponent(key)}`,
|
||||
{
|
||||
headers: { "X-API-Key": this.apiKey },
|
||||
}
|
||||
);
|
||||
if (!r.ok) throw new Error(`get failed: ${r.status}`);
|
||||
const buf = new Uint8Array(await r.arrayBuffer());
|
||||
return buf;
|
||||
}
|
||||
|
||||
async exists(key: string): Promise<boolean> {
|
||||
const r = await this.http(
|
||||
`${this.baseUrl}/v1/storage/exists?key=${encodeURIComponent(key)}`,
|
||||
{
|
||||
headers: this.headers(false),
|
||||
}
|
||||
);
|
||||
if (!r.ok) throw new Error(`exists failed: ${r.status}`);
|
||||
const j = await r.json();
|
||||
return !!j.exists;
|
||||
}
|
||||
|
||||
async list(prefix = ""): Promise<string[]> {
|
||||
const r = await this.http(
|
||||
`${this.baseUrl}/v1/storage/list?prefix=${encodeURIComponent(prefix)}`,
|
||||
{
|
||||
headers: this.headers(false),
|
||||
}
|
||||
);
|
||||
if (!r.ok) throw new Error(`list failed: ${r.status}`);
|
||||
const j = await r.json();
|
||||
return j.keys || [];
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
const r = await this.http(`${this.baseUrl}/v1/storage/delete`, {
|
||||
method: "POST",
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ key }),
|
||||
});
|
||||
if (!r.ok) throw new Error(`delete failed: ${r.status}`);
|
||||
}
|
||||
|
||||
// PubSub (minimal)
|
||||
subscribe(
|
||||
topic: string,
|
||||
onMessage: (data: Uint8Array) => void
|
||||
): { close: () => void } {
|
||||
const url = new URL(`${this.baseUrl.replace(/^http/, "ws")}/v1/pubsub/ws`);
|
||||
url.searchParams.set("topic", topic);
|
||||
const ws = new WebSocket(url.toString(), {
|
||||
headers: { "X-API-Key": this.apiKey },
|
||||
} as any);
|
||||
ws.binaryType = "arraybuffer";
|
||||
ws.onmessage = (ev: any) => {
|
||||
const data =
|
||||
ev.data instanceof ArrayBuffer
|
||||
? new Uint8Array(ev.data)
|
||||
: new TextEncoder().encode(String(ev.data));
|
||||
onMessage(data);
|
||||
};
|
||||
return { close: () => ws.close() };
|
||||
}
|
||||
|
||||
async publish(topic: string, data: Uint8Array | string): Promise<void> {
|
||||
const bytes =
|
||||
typeof data === "string" ? new TextEncoder().encode(data) : data;
|
||||
const b64 = Buffer.from(bytes).toString("base64");
|
||||
const r = await this.http(`${this.baseUrl}/v1/pubsub/publish`, {
|
||||
method: "POST",
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ topic, data_base64: b64 }),
|
||||
});
|
||||
if (!r.ok) throw new Error(`publish failed: ${r.status}`);
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"moduleResolution": "Node"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
@ -318,7 +318,7 @@ func initFullStack(force bool) {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
node2Content := GenerateNodeConfig(node2Name, "", 4002, 5002, 7002, "localhost:7001", bootstrapMultiaddr)
|
||||
node2Content := GenerateNodeConfig(node2Name, "", 4002, 5002, 7002, "localhost:5001", bootstrapMultiaddr)
|
||||
if err := os.WriteFile(node2Path, []byte(node2Content), 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to write node2 config: %v\n", err)
|
||||
os.Exit(1)
|
||||
@ -334,7 +334,7 @@ func initFullStack(force bool) {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
node3Content := GenerateNodeConfig(node3Name, "", 4003, 5003, 7003, "localhost:7001", bootstrapMultiaddr)
|
||||
node3Content := GenerateNodeConfig(node3Name, "", 4003, 5003, 7003, "localhost:5001", bootstrapMultiaddr)
|
||||
if err := os.WriteFile(node3Path, []byte(node3Content), 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to write node3 config: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
||||
327
pkg/cli/rqlite_commands.go
Normal file
327
pkg/cli/rqlite_commands.go
Normal file
@ -0,0 +1,327 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/config"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// HandleRQLiteCommand handles rqlite-related commands
|
||||
func HandleRQLiteCommand(args []string) {
|
||||
if len(args) == 0 {
|
||||
showRQLiteHelp()
|
||||
return
|
||||
}
|
||||
|
||||
if runtime.GOOS != "linux" {
|
||||
fmt.Fprintf(os.Stderr, "❌ RQLite commands are only supported on Linux\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
subcommand := args[0]
|
||||
subargs := args[1:]
|
||||
|
||||
switch subcommand {
|
||||
case "fix":
|
||||
handleRQLiteFix(subargs)
|
||||
case "help":
|
||||
showRQLiteHelp()
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "Unknown rqlite subcommand: %s\n", subcommand)
|
||||
showRQLiteHelp()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func showRQLiteHelp() {
|
||||
fmt.Printf("🗄️ RQLite Commands\n\n")
|
||||
fmt.Printf("Usage: network-cli rqlite <subcommand> [options]\n\n")
|
||||
fmt.Printf("Subcommands:\n")
|
||||
fmt.Printf(" fix - Fix misconfigured join address and clean stale raft state\n\n")
|
||||
fmt.Printf("Description:\n")
|
||||
fmt.Printf(" The 'fix' command automatically repairs common rqlite cluster issues:\n")
|
||||
fmt.Printf(" - Corrects join address from HTTP port (5001) to Raft port (7001) if misconfigured\n")
|
||||
fmt.Printf(" - Cleans stale raft state that prevents proper cluster formation\n")
|
||||
fmt.Printf(" - Restarts the node service with corrected configuration\n\n")
|
||||
fmt.Printf("Requirements:\n")
|
||||
fmt.Printf(" - Must be run as root (use sudo)\n")
|
||||
fmt.Printf(" - Only works on non-bootstrap nodes (nodes with join_address configured)\n")
|
||||
fmt.Printf(" - Stops and restarts the debros-node service\n\n")
|
||||
fmt.Printf("Examples:\n")
|
||||
fmt.Printf(" sudo network-cli rqlite fix\n")
|
||||
}
|
||||
|
||||
func handleRQLiteFix(args []string) {
|
||||
requireRoot()
|
||||
|
||||
// Parse optional flags
|
||||
dryRun := false
|
||||
for _, arg := range args {
|
||||
if arg == "--dry-run" || arg == "-n" {
|
||||
dryRun = true
|
||||
}
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
fmt.Printf("🔍 Dry-run mode - no changes will be made\n\n")
|
||||
}
|
||||
|
||||
fmt.Printf("🔧 RQLite Cluster Repair\n\n")
|
||||
|
||||
// Load config
|
||||
configPath, err := config.DefaultPath("node.yaml")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to determine config path: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cfg, err := loadConfigForRepair(configPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to load config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Check if this is a bootstrap node
|
||||
if cfg.Node.Type == "bootstrap" || cfg.Database.RQLiteJoinAddress == "" {
|
||||
fmt.Printf("ℹ️ This is a bootstrap node (no join address configured)\n")
|
||||
fmt.Printf(" Bootstrap nodes don't need repair - they are the cluster leader\n")
|
||||
fmt.Printf(" Run this command on follower nodes instead\n")
|
||||
return
|
||||
}
|
||||
|
||||
joinAddr := cfg.Database.RQLiteJoinAddress
|
||||
|
||||
// Check if join address needs fixing
|
||||
needsConfigFix := needsFix(joinAddr, cfg.Database.RQLiteRaftPort, cfg.Database.RQLitePort)
|
||||
var fixedAddr string
|
||||
|
||||
if needsConfigFix {
|
||||
fmt.Printf("⚠️ Detected misconfigured join address: %s\n", joinAddr)
|
||||
fmt.Printf(" Expected Raft port (%d) but found HTTP port (%d)\n", cfg.Database.RQLiteRaftPort, cfg.Database.RQLitePort)
|
||||
|
||||
// Extract host from join address
|
||||
host, _, err := parseJoinAddress(joinAddr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to parse join address: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Fix the join address - rqlite expects Raft port for -join
|
||||
fixedAddr = fmt.Sprintf("%s:%d", host, cfg.Database.RQLiteRaftPort)
|
||||
fmt.Printf(" Corrected address: %s\n\n", fixedAddr)
|
||||
} else {
|
||||
fmt.Printf("✅ Join address looks correct: %s\n", joinAddr)
|
||||
fmt.Printf(" Will clean stale raft state to ensure proper cluster formation\n\n")
|
||||
fixedAddr = joinAddr // No change needed
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
fmt.Printf("🔍 Dry-run: Would clean raft state")
|
||||
if needsConfigFix {
|
||||
fmt.Printf(" and fix config")
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
return
|
||||
}
|
||||
|
||||
// Stop the service
|
||||
fmt.Printf("⏹️ Stopping debros-node service...\n")
|
||||
if err := stopService("debros-node"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to stop service: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf(" ✓ Service stopped\n\n")
|
||||
|
||||
// Update config file if needed
|
||||
if needsConfigFix {
|
||||
fmt.Printf("📝 Updating configuration file...\n")
|
||||
if err := updateConfigJoinAddress(configPath, fixedAddr); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to update config: %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, " Service is stopped - please fix manually and restart\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf(" ✓ Config updated: %s\n\n", configPath)
|
||||
}
|
||||
|
||||
// Clean raft state
|
||||
fmt.Printf("🧹 Cleaning stale raft state...\n")
|
||||
dataDir := expandDataDir(cfg.Node.DataDir)
|
||||
raftDir := filepath.Join(dataDir, "rqlite", "raft")
|
||||
if err := cleanRaftState(raftDir); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to clean raft state: %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, " Continuing anyway - raft state may still exist\n")
|
||||
} else {
|
||||
fmt.Printf(" ✓ Raft state cleaned\n\n")
|
||||
}
|
||||
|
||||
// Restart the service
|
||||
fmt.Printf("🚀 Restarting debros-node service...\n")
|
||||
if err := startService("debros-node"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to start service: %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, " Config has been fixed - please restart manually:\n")
|
||||
fmt.Fprintf(os.Stderr, " sudo systemctl start debros-node\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf(" ✓ Service started\n\n")
|
||||
|
||||
fmt.Printf("✅ Repair complete!\n\n")
|
||||
fmt.Printf("The node should now join the cluster correctly.\n")
|
||||
fmt.Printf("Monitor logs with: sudo network-cli service logs node --follow\n")
|
||||
}
|
||||
|
||||
func loadConfigForRepair(path string) (*config.Config, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open config file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var cfg config.Config
|
||||
if err := config.DecodeStrict(file, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func needsFix(joinAddr string, raftPort int, httpPort int) bool {
|
||||
if joinAddr == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Remove http:// or https:// prefix if present
|
||||
addr := joinAddr
|
||||
if strings.HasPrefix(addr, "http://") {
|
||||
addr = strings.TrimPrefix(addr, "http://")
|
||||
} else if strings.HasPrefix(addr, "https://") {
|
||||
addr = strings.TrimPrefix(addr, "https://")
|
||||
}
|
||||
|
||||
// Parse host:port
|
||||
_, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return false // Can't parse, assume it's fine
|
||||
}
|
||||
|
||||
// Check if port matches HTTP port (incorrect - should be Raft port)
|
||||
if port == fmt.Sprintf("%d", httpPort) {
|
||||
return true
|
||||
}
|
||||
|
||||
// If it matches Raft port, it's correct
|
||||
if port == fmt.Sprintf("%d", raftPort) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Unknown port - assume it's fine
|
||||
return false
|
||||
}
|
||||
|
||||
func parseJoinAddress(joinAddr string) (host, port string, err error) {
|
||||
// Remove http:// or https:// prefix if present
|
||||
addr := joinAddr
|
||||
if strings.HasPrefix(addr, "http://") {
|
||||
addr = strings.TrimPrefix(addr, "http://")
|
||||
} else if strings.HasPrefix(addr, "https://") {
|
||||
addr = strings.TrimPrefix(addr, "https://")
|
||||
}
|
||||
|
||||
host, port, err = net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("invalid join address format: %w", err)
|
||||
}
|
||||
|
||||
return host, port, nil
|
||||
}
|
||||
|
||||
func updateConfigJoinAddress(configPath string, newJoinAddr string) error {
|
||||
// Read the file
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
// Parse YAML into a generic map to preserve structure
|
||||
var yamlData map[string]interface{}
|
||||
if err := yaml.Unmarshal(data, &yamlData); err != nil {
|
||||
return fmt.Errorf("failed to parse YAML: %w", err)
|
||||
}
|
||||
|
||||
// Navigate to database.rqlite_join_address
|
||||
database, ok := yamlData["database"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("database section not found in config")
|
||||
}
|
||||
|
||||
database["rqlite_join_address"] = newJoinAddr
|
||||
|
||||
// Write back to file
|
||||
updatedData, err := yaml.Marshal(yamlData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal YAML: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, updatedData, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write config file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func expandDataDir(dataDir string) string {
|
||||
expanded := os.ExpandEnv(dataDir)
|
||||
if strings.HasPrefix(expanded, "~") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return expanded // Fallback to original
|
||||
}
|
||||
expanded = filepath.Join(home, expanded[1:])
|
||||
}
|
||||
return expanded
|
||||
}
|
||||
|
||||
func cleanRaftState(raftDir string) error {
|
||||
if _, err := os.Stat(raftDir); os.IsNotExist(err) {
|
||||
return nil // Directory doesn't exist, nothing to clean
|
||||
}
|
||||
|
||||
// Remove raft state files
|
||||
filesToRemove := []string{
|
||||
"peers.json",
|
||||
"peers.json.backup",
|
||||
"peers.info",
|
||||
"raft.db",
|
||||
}
|
||||
|
||||
for _, file := range filesToRemove {
|
||||
filePath := filepath.Join(raftDir, file)
|
||||
if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to remove %s: %w", filePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopService(serviceName string) error {
|
||||
cmd := exec.Command("systemctl", "stop", serviceName)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("systemctl stop failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func startService(serviceName string) error {
|
||||
cmd := exec.Command("systemctl", "start", serviceName)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("systemctl start failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
857
pkg/cli/setup.go
857
pkg/cli/setup.go
@ -3,12 +3,16 @@ package cli
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/DeBrosOfficial/network/pkg/config"
|
||||
)
|
||||
|
||||
// HandleSetupCommand handles the interactive 'setup' command for VPS installation
|
||||
@ -108,19 +112,179 @@ func HandleSetupCommand(args []string) {
|
||||
fmt.Printf("✅ Setup Complete!\n")
|
||||
fmt.Printf(strings.Repeat("=", 70) + "\n\n")
|
||||
fmt.Printf("DeBros Network is now running!\n\n")
|
||||
|
||||
// Try to get and display peer ID
|
||||
peerID := getPeerID()
|
||||
if peerID != "" {
|
||||
fmt.Printf("🆔 Node Peer ID: %s\n\n", peerID)
|
||||
}
|
||||
|
||||
fmt.Printf("Service Management:\n")
|
||||
fmt.Printf(" network-cli service status all\n")
|
||||
fmt.Printf(" network-cli service logs node --follow\n")
|
||||
fmt.Printf(" network-cli service restart gateway\n\n")
|
||||
fmt.Printf("Access DeBros User:\n")
|
||||
fmt.Printf(" sudo -u debros bash\n\n")
|
||||
|
||||
// Check if HTTPS is enabled
|
||||
gatewayConfigPath := "/home/debros/.debros/gateway.yaml"
|
||||
httpsEnabled := false
|
||||
var domainName string
|
||||
if data, err := os.ReadFile(gatewayConfigPath); err == nil {
|
||||
var cfg config.Config
|
||||
if err := config.DecodeStrict(strings.NewReader(string(data)), &cfg); err == nil {
|
||||
// Try to parse as gateway config
|
||||
if strings.Contains(string(data), "enable_https: true") {
|
||||
httpsEnabled = true
|
||||
// Extract domain name from config
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "domain_name:") {
|
||||
parts := strings.Split(line, ":")
|
||||
if len(parts) > 1 {
|
||||
domainName = strings.Trim(strings.TrimSpace(parts[1]), "\"")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Verify Installation:\n")
|
||||
fmt.Printf(" curl http://localhost:6001/health\n")
|
||||
if httpsEnabled && domainName != "" {
|
||||
fmt.Printf(" curl https://%s/health\n", domainName)
|
||||
fmt.Printf(" curl http://localhost:6001/health (HTTP fallback)\n")
|
||||
} else {
|
||||
fmt.Printf(" curl http://localhost:6001/health\n")
|
||||
}
|
||||
fmt.Printf(" curl http://localhost:5001/status\n\n")
|
||||
|
||||
if httpsEnabled && domainName != "" {
|
||||
fmt.Printf("HTTPS Configuration:\n")
|
||||
fmt.Printf(" Domain: %s\n", domainName)
|
||||
fmt.Printf(" HTTPS endpoint: https://%s\n", domainName)
|
||||
fmt.Printf(" Certificate cache: /home/debros/.debros/tls-cache\n")
|
||||
fmt.Printf(" Certificates are automatically managed via Let's Encrypt (ACME)\n\n")
|
||||
}
|
||||
|
||||
fmt.Printf("Anyone Relay (Anon):\n")
|
||||
fmt.Printf(" sudo systemctl status anon\n")
|
||||
fmt.Printf(" sudo tail -f /home/debros/.debros/logs/anon/notices.log\n")
|
||||
fmt.Printf(" Proxy endpoint: POST http://localhost:6001/v1/proxy/anon\n\n")
|
||||
if httpsEnabled && domainName != "" {
|
||||
fmt.Printf(" Proxy endpoint: POST https://%s/v1/proxy/anon\n\n", domainName)
|
||||
} else {
|
||||
fmt.Printf(" Proxy endpoint: POST http://localhost:6001/v1/proxy/anon\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
// extractIPFromMultiaddr extracts the IP address from a multiaddr string
|
||||
// Format: /ip4/51.83.128.181/tcp/4001/p2p/12D3KooW...
|
||||
func extractIPFromMultiaddr(multiaddr string) string {
|
||||
if multiaddr == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Split by "/ip4/"
|
||||
parts := strings.Split(multiaddr, "/ip4/")
|
||||
if len(parts) < 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Get the part after "/ip4/"
|
||||
ipPart := parts[1]
|
||||
// Extract IP until the next "/"
|
||||
ipEnd := strings.Index(ipPart, "/")
|
||||
if ipEnd == -1 {
|
||||
// If no "/" found, the whole string might be the IP
|
||||
return strings.TrimSpace(ipPart)
|
||||
}
|
||||
|
||||
ip := strings.TrimSpace(ipPart[:ipEnd])
|
||||
// Validate it looks like an IP address
|
||||
if net.ParseIP(ip) != nil {
|
||||
return ip
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// getVPSIPv4Address gets the primary IPv4 address of the VPS
|
||||
func getVPSIPv4Address() (string, error) {
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, iface := range interfaces {
|
||||
// Skip loopback and down interfaces
|
||||
if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
ipNet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
ip := ipNet.IP
|
||||
// Check if it's IPv4 and not a loopback address
|
||||
if ip.To4() != nil && !ip.IsLoopback() {
|
||||
return ip.String(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("could not find a non-loopback IPv4 address")
|
||||
}
|
||||
|
||||
// getPeerID attempts to retrieve the peer ID from peer.info based on node type
|
||||
func getPeerID() string {
|
||||
debrosDir := "/home/debros/.debros"
|
||||
nodeConfigPath := filepath.Join(debrosDir, "node.yaml")
|
||||
|
||||
// Determine node type from config
|
||||
var nodeType string
|
||||
if file, err := os.Open(nodeConfigPath); err == nil {
|
||||
defer file.Close()
|
||||
var cfg config.Config
|
||||
if err := config.DecodeStrict(file, &cfg); err == nil {
|
||||
nodeType = cfg.Node.Type
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the peer.info path based on node type
|
||||
var peerInfoPath string
|
||||
if nodeType == "bootstrap" {
|
||||
peerInfoPath = filepath.Join(debrosDir, "bootstrap", "peer.info")
|
||||
} else {
|
||||
// Default to "node" directory for regular nodes
|
||||
peerInfoPath = filepath.Join(debrosDir, "node", "peer.info")
|
||||
}
|
||||
|
||||
// Try to read from peer.info file
|
||||
if data, err := os.ReadFile(peerInfoPath); err == nil {
|
||||
peerInfo := strings.TrimSpace(string(data))
|
||||
// Extract peer ID from multiaddr format: /ip4/.../p2p/<peer-id>
|
||||
if strings.Contains(peerInfo, "/p2p/") {
|
||||
parts := strings.Split(peerInfo, "/p2p/")
|
||||
if len(parts) == 2 {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
// If it's just the peer ID, return it
|
||||
if len(peerInfo) > 0 && !strings.Contains(peerInfo, "/") {
|
||||
return peerInfo
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func detectLinuxDistro() string {
|
||||
@ -214,6 +378,206 @@ func isValidHostPort(s string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// isPortAvailable checks if a port is available for binding
|
||||
func isPortAvailable(port int) bool {
|
||||
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ln.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
// checkPorts80And443 checks if ports 80 and 443 are available
|
||||
func checkPorts80And443() (bool, string) {
|
||||
port80Available := isPortAvailable(80)
|
||||
port443Available := isPortAvailable(443)
|
||||
|
||||
if !port80Available || !port443Available {
|
||||
var issues []string
|
||||
if !port80Available {
|
||||
issues = append(issues, "port 80")
|
||||
}
|
||||
if !port443Available {
|
||||
issues = append(issues, "port 443")
|
||||
}
|
||||
return false, strings.Join(issues, " and ")
|
||||
}
|
||||
|
||||
return true, ""
|
||||
}
|
||||
|
||||
// isValidDomain validates a domain name format
|
||||
func isValidDomain(domain string) bool {
|
||||
domain = strings.TrimSpace(domain)
|
||||
if domain == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Basic validation: domain should contain at least one dot
|
||||
// and not start/end with dot or hyphen
|
||||
if !strings.Contains(domain, ".") {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.HasPrefix(domain, "-") || strings.HasSuffix(domain, "-") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for valid characters (letters, numbers, dots, hyphens)
|
||||
for _, char := range domain {
|
||||
if !((char >= 'a' && char <= 'z') ||
|
||||
(char >= 'A' && char <= 'Z') ||
|
||||
(char >= '0' && char <= '9') ||
|
||||
char == '.' ||
|
||||
char == '-') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// verifyDNSResolution verifies that a domain resolves to the VPS IP
|
||||
func verifyDNSResolution(domain, expectedIP string) bool {
|
||||
ips, err := net.LookupIP(domain)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil && ip.String() == expectedIP {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// promptDomainForHTTPS prompts for domain name and verifies DNS configuration
|
||||
func promptDomainForHTTPS(reader *bufio.Reader, vpsIP string) string {
|
||||
for {
|
||||
fmt.Printf("\nEnter your domain name (e.g., example.com): ")
|
||||
domainInput, _ := reader.ReadString('\n')
|
||||
domain := strings.TrimSpace(domainInput)
|
||||
|
||||
if domain == "" {
|
||||
fmt.Printf(" Domain name cannot be empty. Skipping HTTPS configuration.\n")
|
||||
return ""
|
||||
}
|
||||
|
||||
if !isValidDomain(domain) {
|
||||
fmt.Printf(" ❌ Invalid domain format. Please enter a valid domain name.\n")
|
||||
continue
|
||||
}
|
||||
|
||||
// Verify DNS is configured
|
||||
fmt.Printf("\n Verifying DNS configuration...\n")
|
||||
fmt.Printf(" Please ensure your domain %s points to this server's IP (%s)\n", domain, vpsIP)
|
||||
fmt.Printf(" Have you configured the DNS record? (yes/no): ")
|
||||
dnsResponse, _ := reader.ReadString('\n')
|
||||
dnsResponse = strings.ToLower(strings.TrimSpace(dnsResponse))
|
||||
|
||||
if dnsResponse == "yes" || dnsResponse == "y" {
|
||||
// Try to verify DNS resolution
|
||||
fmt.Printf(" Checking DNS resolution...\n")
|
||||
if verifyDNSResolution(domain, vpsIP) {
|
||||
fmt.Printf(" ✓ DNS is correctly configured\n")
|
||||
return domain
|
||||
} else {
|
||||
fmt.Printf(" ⚠️ DNS does not resolve to this server's IP (%s)\n", vpsIP)
|
||||
fmt.Printf(" DNS may still be propagating. Continue anyway? (yes/no): ")
|
||||
continueResponse, _ := reader.ReadString('\n')
|
||||
continueResponse = strings.ToLower(strings.TrimSpace(continueResponse))
|
||||
if continueResponse == "yes" || continueResponse == "y" {
|
||||
fmt.Printf(" Continuing with domain configuration (DNS may need time to propagate)\n")
|
||||
return domain
|
||||
}
|
||||
// User chose not to continue, ask for domain again
|
||||
fmt.Printf(" Please configure DNS and try again, or press Enter to skip HTTPS\n")
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" Please configure DNS first. Type 'skip' to skip HTTPS configuration: ")
|
||||
skipResponse, _ := reader.ReadString('\n')
|
||||
skipResponse = strings.ToLower(strings.TrimSpace(skipResponse))
|
||||
if skipResponse == "skip" {
|
||||
return ""
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateGatewayConfigWithHTTPS updates an existing gateway.yaml file with HTTPS settings
|
||||
func updateGatewayConfigWithHTTPS(gatewayPath, domain string) error {
|
||||
// Read existing config
|
||||
data, err := os.ReadFile(gatewayPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read gateway config: %w", err)
|
||||
}
|
||||
|
||||
configContent := string(data)
|
||||
tlsCacheDir := "/home/debros/.debros/tls-cache"
|
||||
|
||||
// Check if HTTPS is already enabled
|
||||
if strings.Contains(configContent, "enable_https: true") {
|
||||
// Update existing HTTPS settings
|
||||
lines := strings.Split(configContent, "\n")
|
||||
var updatedLines []string
|
||||
domainUpdated := false
|
||||
cacheDirUpdated := false
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "enable_https:") {
|
||||
updatedLines = append(updatedLines, "enable_https: true")
|
||||
} else if strings.HasPrefix(trimmed, "domain_name:") {
|
||||
updatedLines = append(updatedLines, fmt.Sprintf("domain_name: \"%s\"", domain))
|
||||
domainUpdated = true
|
||||
} else if strings.HasPrefix(trimmed, "tls_cache_dir:") {
|
||||
updatedLines = append(updatedLines, fmt.Sprintf("tls_cache_dir: \"%s\"", tlsCacheDir))
|
||||
cacheDirUpdated = true
|
||||
} else {
|
||||
updatedLines = append(updatedLines, line)
|
||||
}
|
||||
}
|
||||
|
||||
// Add missing fields if not found
|
||||
if !domainUpdated {
|
||||
updatedLines = append(updatedLines, fmt.Sprintf("domain_name: \"%s\"", domain))
|
||||
}
|
||||
if !cacheDirUpdated {
|
||||
updatedLines = append(updatedLines, fmt.Sprintf("tls_cache_dir: \"%s\"", tlsCacheDir))
|
||||
}
|
||||
|
||||
configContent = strings.Join(updatedLines, "\n")
|
||||
} else {
|
||||
// Add HTTPS configuration at the end
|
||||
configContent = strings.TrimRight(configContent, "\n")
|
||||
if !strings.HasSuffix(configContent, "\n") && configContent != "" {
|
||||
configContent += "\n"
|
||||
}
|
||||
configContent += "enable_https: true\n"
|
||||
configContent += fmt.Sprintf("domain_name: \"%s\"\n", domain)
|
||||
configContent += fmt.Sprintf("tls_cache_dir: \"%s\"\n", tlsCacheDir)
|
||||
}
|
||||
|
||||
// Write updated config
|
||||
if err := os.WriteFile(gatewayPath, []byte(configContent), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write gateway config: %w", err)
|
||||
}
|
||||
|
||||
// Fix ownership
|
||||
exec.Command("chown", "debros:debros", gatewayPath).Run()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupDebrosUser() {
|
||||
fmt.Printf("👤 Setting up 'debros' user...\n")
|
||||
|
||||
@ -704,38 +1068,37 @@ func cloneAndBuild() {
|
||||
branch := promptBranch()
|
||||
fmt.Printf(" Using branch: %s\n", branch)
|
||||
|
||||
// Check if already cloned
|
||||
if _, err := os.Stat("/home/debros/src/.git"); err == nil {
|
||||
fmt.Printf(" Updating repository...\n")
|
||||
|
||||
// Check current branch and switch if needed
|
||||
currentBranchCmd := exec.Command("sudo", "-u", "debros", "git", "-C", "/home/debros/src", "rev-parse", "--abbrev-ref", "HEAD")
|
||||
if output, err := currentBranchCmd.Output(); err == nil {
|
||||
currentBranch := strings.TrimSpace(string(output))
|
||||
if currentBranch != branch {
|
||||
fmt.Printf(" Switching from %s to %s...\n", currentBranch, branch)
|
||||
// Fetch the target branch first (needed for shallow clones)
|
||||
exec.Command("sudo", "-u", "debros", "git", "-C", "/home/debros/src", "fetch", "origin", branch).Run()
|
||||
// Checkout the selected branch
|
||||
checkoutCmd := exec.Command("sudo", "-u", "debros", "git", "-C", "/home/debros/src", "checkout", branch)
|
||||
if err := checkoutCmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to switch branch: %v\n", err)
|
||||
}
|
||||
// Remove existing repository if it exists (always start fresh)
|
||||
if _, err := os.Stat("/home/debros/src"); err == nil {
|
||||
fmt.Printf(" Removing existing repository...\n")
|
||||
// Remove as root since we're running as root
|
||||
if err := os.RemoveAll("/home/debros/src"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to remove existing repo as root: %v\n", err)
|
||||
// Try as debros user as fallback (might work if files are owned by debros)
|
||||
removeCmd := exec.Command("sudo", "-u", "debros", "rm", "-rf", "/home/debros/src")
|
||||
if output, err := removeCmd.CombinedOutput(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to remove existing repo as debros user: %v\n%s\n", err, output)
|
||||
}
|
||||
}
|
||||
// Wait a moment to ensure filesystem syncs
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Pull latest changes
|
||||
cmd := exec.Command("sudo", "-u", "debros", "git", "-C", "/home/debros/src", "pull", "origin", branch)
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to update repo: %v\n", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" Cloning repository...\n")
|
||||
cmd := exec.Command("sudo", "-u", "debros", "git", "clone", "--branch", branch, "--depth", "1", "https://github.com/DeBrosOfficial/network.git", "/home/debros/src")
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to clone repo: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// Ensure parent directory exists and has correct permissions
|
||||
if err := os.MkdirAll("/home/debros", 0755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to ensure debros home directory exists: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := exec.Command("chown", "debros:debros", "/home/debros").Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to chown debros home directory: %v\n", err)
|
||||
}
|
||||
|
||||
// Clone fresh repository
|
||||
fmt.Printf(" Cloning repository...\n")
|
||||
cmd := exec.Command("sudo", "-u", "debros", "git", "clone", "--branch", branch, "--depth", "1", "https://github.com/DeBrosOfficial/network.git", "/home/debros/src")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to clone repo: %v\n%s\n", err, output)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Build
|
||||
@ -746,7 +1109,7 @@ func cloneAndBuild() {
|
||||
|
||||
// Use sudo with --preserve-env=PATH to pass Go path to debros user
|
||||
// Set HOME so Go knows where to create module cache
|
||||
cmd := exec.Command("sudo", "--preserve-env=PATH", "-u", "debros", "make", "build")
|
||||
cmd = exec.Command("sudo", "--preserve-env=PATH", "-u", "debros", "make", "build")
|
||||
cmd.Dir = "/home/debros/src"
|
||||
cmd.Env = append(os.Environ(), "HOME=/home/debros", "PATH="+os.Getenv("PATH")+":/usr/local/go/bin")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
@ -755,92 +1118,176 @@ func cloneAndBuild() {
|
||||
}
|
||||
|
||||
// Copy binaries
|
||||
exec.Command("sh", "-c", "cp -r /home/debros/src/bin/* /home/debros/bin/").Run()
|
||||
exec.Command("chown", "-R", "debros:debros", "/home/debros/bin").Run()
|
||||
exec.Command("chmod", "-R", "755", "/home/debros/bin").Run()
|
||||
copyCmd := exec.Command("sh", "-c", "cp -r /home/debros/src/bin/* /home/debros/bin/")
|
||||
if output, err := copyCmd.CombinedOutput(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to copy binaries: %v\n%s\n", err, output)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
chownCmd := exec.Command("chown", "-R", "debros:debros", "/home/debros/bin")
|
||||
if err := chownCmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to chown binaries: %v\n", err)
|
||||
}
|
||||
|
||||
chmodCmd := exec.Command("chmod", "-R", "755", "/home/debros/bin")
|
||||
if err := chmodCmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to chmod binaries: %v\n", err)
|
||||
}
|
||||
|
||||
fmt.Printf(" ✓ Built and installed\n")
|
||||
}
|
||||
|
||||
func generateConfigsInteractive(force bool) {
|
||||
fmt.Printf("⚙️ Generating configurations...\n")
|
||||
fmt.Printf("⚙️ Generating configurations...\n\n")
|
||||
|
||||
// For single-node VPS setup, use sensible defaults
|
||||
// This creates a bootstrap node that acts as the cluster leader
|
||||
fmt.Printf("\n")
|
||||
fmt.Printf("Setting up single-node configuration...\n")
|
||||
fmt.Printf(" • Bootstrap node (cluster leader)\n")
|
||||
fmt.Printf(" • No external peers required\n")
|
||||
fmt.Printf(" • Gateway connected to local node\n\n")
|
||||
|
||||
bootstrapPath := "/home/debros/.debros/bootstrap.yaml"
|
||||
nodeConfigPath := "/home/debros/.debros/node.yaml"
|
||||
gatewayPath := "/home/debros/.debros/gateway.yaml"
|
||||
|
||||
// Check if node.yaml already exists
|
||||
// Check if configs already exist
|
||||
nodeExists := false
|
||||
gatewayExists := false
|
||||
if _, err := os.Stat(nodeConfigPath); err == nil {
|
||||
nodeExists = true
|
||||
fmt.Printf(" ℹ️ node.yaml already exists, will not overwrite\n")
|
||||
}
|
||||
if _, err := os.Stat(gatewayPath); err == nil {
|
||||
gatewayExists = true
|
||||
}
|
||||
|
||||
// Generate bootstrap node config with explicit parameters
|
||||
// Pass empty bootstrap-peers and no join address for bootstrap node
|
||||
bootstrapArgs := []string{
|
||||
"-u", "debros",
|
||||
"/home/debros/bin/network-cli", "config", "init",
|
||||
"--type", "bootstrap",
|
||||
"--bootstrap-peers", "",
|
||||
}
|
||||
if force {
|
||||
bootstrapArgs = append(bootstrapArgs, "--force")
|
||||
}
|
||||
// If both configs exist and not forcing, skip configuration prompts
|
||||
if nodeExists && gatewayExists && !force {
|
||||
fmt.Printf(" ℹ️ Configuration files already exist (node.yaml and gateway.yaml)\n")
|
||||
fmt.Printf(" ℹ️ Skipping configuration generation\n\n")
|
||||
|
||||
cmd := exec.Command("sudo", bootstrapArgs...)
|
||||
cmd.Stdin = nil // Explicitly close stdin to prevent interactive prompts
|
||||
output, err := cmd.CombinedOutput()
|
||||
bootstrapCreated := (err == nil)
|
||||
// Only offer to add HTTPS if not already enabled
|
||||
httpsAlreadyEnabled := false
|
||||
if data, err := os.ReadFile(gatewayPath); err == nil {
|
||||
httpsAlreadyEnabled = strings.Contains(string(data), "enable_https: true")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Check if bootstrap.yaml already exists (config init failed because it exists)
|
||||
if _, statErr := os.Stat(bootstrapPath); statErr == nil {
|
||||
fmt.Printf(" ℹ️ bootstrap.yaml already exists, skipping creation\n")
|
||||
bootstrapCreated = true
|
||||
if !httpsAlreadyEnabled {
|
||||
fmt.Printf("🌐 Domain and HTTPS Configuration\n")
|
||||
fmt.Printf("Would you like to add HTTPS with a domain name to your existing gateway config? (yes/no) [default: no]: ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
addHTTPSResponse, _ := reader.ReadString('\n')
|
||||
addHTTPSResponse = strings.ToLower(strings.TrimSpace(addHTTPSResponse))
|
||||
|
||||
if addHTTPSResponse == "yes" || addHTTPSResponse == "y" {
|
||||
// Get VPS IP for DNS verification
|
||||
vpsIP, err := getVPSIPv4Address()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to detect IPv4 address: %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, " Using 0.0.0.0 as fallback\n")
|
||||
vpsIP = "0.0.0.0"
|
||||
}
|
||||
|
||||
// Check if ports 80 and 443 are available
|
||||
portsAvailable, portIssues := checkPorts80And443()
|
||||
if !portsAvailable {
|
||||
fmt.Fprintf(os.Stderr, "\n⚠️ Cannot enable HTTPS: %s is already in use\n", portIssues)
|
||||
fmt.Fprintf(os.Stderr, " You will need to configure HTTPS manually if you want to use a domain.\n\n")
|
||||
} else {
|
||||
// Prompt for domain and update existing config
|
||||
domain := promptDomainForHTTPS(reader, vpsIP)
|
||||
if domain != "" {
|
||||
// Update existing gateway config with HTTPS settings
|
||||
if err := updateGatewayConfigWithHTTPS(gatewayPath, domain); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to update gateway config with HTTPS: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ HTTPS configuration added to existing gateway.yaml\n")
|
||||
// Create TLS cache directory
|
||||
tlsCacheDir := "/home/debros/.debros/tls-cache"
|
||||
if err := os.MkdirAll(tlsCacheDir, 0755); err == nil {
|
||||
exec.Command("chown", "-R", "debros:debros", tlsCacheDir).Run()
|
||||
fmt.Printf(" ✓ TLS cache directory created: %s\n", tlsCacheDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to generate bootstrap config: %v\n", err)
|
||||
if len(output) > 0 {
|
||||
fmt.Fprintf(os.Stderr, " Output: %s\n", string(output))
|
||||
}
|
||||
fmt.Printf(" ℹ️ HTTPS is already enabled in gateway.yaml\n")
|
||||
}
|
||||
|
||||
fmt.Printf("\n ✓ Configurations ready\n")
|
||||
return
|
||||
}
|
||||
|
||||
// Get VPS IPv4 address
|
||||
fmt.Printf("Detecting VPS IPv4 address...\n")
|
||||
vpsIP, err := getVPSIPv4Address()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to detect IPv4 address: %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, " Using 0.0.0.0 as fallback. You may need to edit config files manually.\n")
|
||||
vpsIP = "0.0.0.0"
|
||||
} else {
|
||||
fmt.Printf(" ✓ Bootstrap node config created\n")
|
||||
fmt.Printf(" ✓ Detected IPv4 address: %s\n\n", vpsIP)
|
||||
}
|
||||
|
||||
// Rename bootstrap.yaml to node.yaml only if node.yaml doesn't exist
|
||||
if !nodeExists && bootstrapCreated {
|
||||
// Check if bootstrap.yaml exists before renaming
|
||||
if _, err := os.Stat(bootstrapPath); err == nil {
|
||||
renameCmd := exec.Command("sudo", "-u", "debros", "mv", bootstrapPath, nodeConfigPath)
|
||||
if err := renameCmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to rename config: %v\n", err)
|
||||
// Ask about node type
|
||||
fmt.Printf("What type of node is this?\n")
|
||||
fmt.Printf(" 1. Bootstrap node (cluster leader)\n")
|
||||
fmt.Printf(" 2. Regular node (joins existing cluster)\n")
|
||||
fmt.Printf("Enter choice (1 or 2): ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
choice, _ := reader.ReadString('\n')
|
||||
choice = strings.ToLower(strings.TrimSpace(choice))
|
||||
|
||||
isBootstrap := choice == "1" || choice == "bootstrap" || choice == "b"
|
||||
|
||||
var bootstrapPeers string
|
||||
if !isBootstrap {
|
||||
// Ask for bootstrap peer multiaddr
|
||||
fmt.Printf("\nEnter bootstrap peer multiaddr(s) (comma-separated if multiple):\n")
|
||||
fmt.Printf("Example: /ip4/192.168.1.100/tcp/4001/p2p/12D3KooW...\n")
|
||||
fmt.Printf("Bootstrap peer(s): ")
|
||||
bootstrapPeers, _ = reader.ReadString('\n')
|
||||
bootstrapPeers = strings.TrimSpace(bootstrapPeers)
|
||||
}
|
||||
|
||||
// Check if node.yaml already exists
|
||||
if _, err := os.Stat(nodeConfigPath); err == nil {
|
||||
nodeExists = true
|
||||
if !force {
|
||||
fmt.Printf("\n ℹ️ node.yaml already exists, will not overwrite\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Generate node config
|
||||
if !nodeExists || force {
|
||||
var nodeConfig string
|
||||
if isBootstrap {
|
||||
nodeConfig = generateBootstrapConfigWithIP("bootstrap", "", 4001, 5001, 7001, vpsIP)
|
||||
} else {
|
||||
// Extract IP from bootstrap peer multiaddr for rqlite_join_address
|
||||
// Use first bootstrap peer if multiple provided
|
||||
const defaultRQLiteHTTPPort = 5001
|
||||
var joinAddr string
|
||||
if bootstrapPeers != "" {
|
||||
firstPeer := strings.Split(bootstrapPeers, ",")[0]
|
||||
firstPeer = strings.TrimSpace(firstPeer)
|
||||
extractedIP := extractIPFromMultiaddr(firstPeer)
|
||||
if extractedIP != "" {
|
||||
joinAddr = fmt.Sprintf("%s:%d", extractedIP, defaultRQLiteHTTPPort)
|
||||
} else {
|
||||
joinAddr = fmt.Sprintf("localhost:%d", defaultRQLiteHTTPPort)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" ✓ Renamed bootstrap.yaml to node.yaml\n")
|
||||
joinAddr = fmt.Sprintf("localhost:%d", defaultRQLiteHTTPPort)
|
||||
}
|
||||
nodeConfig = generateNodeConfigWithIP("node", "", 4001, 5001, 7001, joinAddr, bootstrapPeers, vpsIP)
|
||||
}
|
||||
} else if nodeExists {
|
||||
// If node.yaml exists, we can optionally remove bootstrap.yaml if it was just created
|
||||
if bootstrapCreated && !force {
|
||||
// Clean up bootstrap.yaml if it was just created but node.yaml already exists
|
||||
if _, err := os.Stat(bootstrapPath); err == nil {
|
||||
exec.Command("sudo", "-u", "debros", "rm", "-f", bootstrapPath).Run()
|
||||
}
|
||||
|
||||
// Write node config
|
||||
if err := os.WriteFile(nodeConfigPath, []byte(nodeConfig), 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to write node config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf(" ℹ️ Using existing node.yaml\n")
|
||||
// Fix ownership
|
||||
exec.Command("chown", "debros:debros", nodeConfigPath).Run()
|
||||
fmt.Printf(" ✓ Node config created: %s\n", nodeConfigPath)
|
||||
}
|
||||
|
||||
// Generate gateway config with explicit empty bootstrap peers
|
||||
// Check if gateway.yaml already exists
|
||||
gatewayExists := false
|
||||
// Generate gateway config
|
||||
if _, err := os.Stat(gatewayPath); err == nil {
|
||||
gatewayExists = true
|
||||
if !force {
|
||||
@ -849,35 +1296,215 @@ func generateConfigsInteractive(force bool) {
|
||||
}
|
||||
|
||||
if !gatewayExists || force {
|
||||
gatewayArgs := []string{
|
||||
"-u", "debros",
|
||||
"/home/debros/bin/network-cli", "config", "init",
|
||||
"--type", "gateway",
|
||||
"--bootstrap-peers", "",
|
||||
}
|
||||
if force {
|
||||
gatewayArgs = append(gatewayArgs, "--force")
|
||||
}
|
||||
// Prompt for domain and HTTPS configuration
|
||||
var domain string
|
||||
var enableHTTPS bool
|
||||
var tlsCacheDir string
|
||||
|
||||
cmd = exec.Command("sudo", gatewayArgs...)
|
||||
cmd.Stdin = nil // Explicitly close stdin to prevent interactive prompts
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
// Check if gateway.yaml already exists (config init failed because it exists)
|
||||
if _, statErr := os.Stat(gatewayPath); statErr == nil {
|
||||
fmt.Printf(" ℹ️ gateway.yaml already exists, skipping creation\n")
|
||||
fmt.Printf("\n🌐 Domain and HTTPS Configuration\n")
|
||||
fmt.Printf("Would you like to configure HTTPS with a domain name? (yes/no) [default: no]: ")
|
||||
response, _ := reader.ReadString('\n')
|
||||
response = strings.ToLower(strings.TrimSpace(response))
|
||||
|
||||
if response == "yes" || response == "y" {
|
||||
// Check if ports 80 and 443 are available
|
||||
portsAvailable, portIssues := checkPorts80And443()
|
||||
if !portsAvailable {
|
||||
fmt.Fprintf(os.Stderr, "\n⚠️ Cannot enable HTTPS: %s is already in use\n", portIssues)
|
||||
fmt.Fprintf(os.Stderr, " You will need to configure HTTPS manually if you want to use a domain.\n")
|
||||
fmt.Fprintf(os.Stderr, " Continuing without HTTPS configuration...\n\n")
|
||||
enableHTTPS = false
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to generate gateway config: %v\n", err)
|
||||
if len(output) > 0 {
|
||||
fmt.Fprintf(os.Stderr, " Output: %s\n", string(output))
|
||||
// Prompt for domain name
|
||||
domain = promptDomainForHTTPS(reader, vpsIP)
|
||||
if domain != "" {
|
||||
enableHTTPS = true
|
||||
// Set TLS cache directory if HTTPS is enabled
|
||||
tlsCacheDir = "/home/debros/.debros/tls-cache"
|
||||
// Create TLS cache directory
|
||||
if err := os.MkdirAll(tlsCacheDir, 0755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to create TLS cache directory: %v\n", err)
|
||||
} else {
|
||||
exec.Command("chown", "-R", "debros:debros", tlsCacheDir).Run()
|
||||
fmt.Printf(" ✓ TLS cache directory created: %s\n", tlsCacheDir)
|
||||
}
|
||||
} else {
|
||||
enableHTTPS = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" ✓ Gateway config created\n")
|
||||
}
|
||||
|
||||
// Gateway config should include bootstrap peers if this is a regular node
|
||||
// (bootstrap nodes don't need bootstrap peers since they are the bootstrap)
|
||||
gatewayConfig := generateGatewayConfigDirect(bootstrapPeers, enableHTTPS, domain, tlsCacheDir)
|
||||
if err := os.WriteFile(gatewayPath, []byte(gatewayConfig), 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Failed to write gateway config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// Fix ownership
|
||||
exec.Command("chown", "debros:debros", gatewayPath).Run()
|
||||
fmt.Printf(" ✓ Gateway config created: %s\n", gatewayPath)
|
||||
}
|
||||
|
||||
fmt.Printf("\n ✓ Configurations ready\n")
|
||||
}
|
||||
|
||||
// generateBootstrapConfigWithIP generates a bootstrap config with actual IP address
|
||||
func generateBootstrapConfigWithIP(name, id string, listenPort, rqliteHTTPPort, rqliteRaftPort int, ipAddr string) string {
|
||||
nodeID := id
|
||||
if nodeID == "" {
|
||||
nodeID = "bootstrap"
|
||||
}
|
||||
|
||||
dataDir := "/home/debros/.debros/bootstrap"
|
||||
|
||||
return fmt.Sprintf(`node:
|
||||
id: "%s"
|
||||
type: "bootstrap"
|
||||
listen_addresses:
|
||||
- "/ip4/%s/tcp/%d"
|
||||
data_dir: "%s"
|
||||
max_connections: 50
|
||||
|
||||
database:
|
||||
data_dir: "%s/rqlite"
|
||||
replication_factor: 3
|
||||
shard_count: 16
|
||||
max_database_size: 1073741824
|
||||
backup_interval: "24h"
|
||||
rqlite_port: %d
|
||||
rqlite_raft_port: %d
|
||||
rqlite_join_address: ""
|
||||
cluster_sync_interval: "30s"
|
||||
peer_inactivity_limit: "24h"
|
||||
min_cluster_size: 1
|
||||
|
||||
discovery:
|
||||
bootstrap_peers: []
|
||||
discovery_interval: "15s"
|
||||
bootstrap_port: %d
|
||||
http_adv_address: "%s:%d"
|
||||
raft_adv_address: "%s:%d"
|
||||
node_namespace: "default"
|
||||
|
||||
security:
|
||||
enable_tls: false
|
||||
|
||||
logging:
|
||||
level: "info"
|
||||
format: "console"
|
||||
`, nodeID, ipAddr, listenPort, dataDir, dataDir, rqliteHTTPPort, rqliteRaftPort, 4001, ipAddr, rqliteHTTPPort, ipAddr, rqliteRaftPort)
|
||||
}
|
||||
|
||||
// generateNodeConfigWithIP generates a node config with actual IP address
|
||||
func generateNodeConfigWithIP(name, id string, listenPort, rqliteHTTPPort, rqliteRaftPort int, joinAddr, bootstrapPeers, ipAddr string) string {
|
||||
nodeID := id
|
||||
if nodeID == "" {
|
||||
nodeID = fmt.Sprintf("node-%d", time.Now().Unix())
|
||||
}
|
||||
|
||||
dataDir := "/home/debros/.debros/node"
|
||||
|
||||
// Parse bootstrap peers
|
||||
var peers []string
|
||||
if bootstrapPeers != "" {
|
||||
for _, p := range strings.Split(bootstrapPeers, ",") {
|
||||
if p = strings.TrimSpace(p); p != "" {
|
||||
peers = append(peers, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf(" ✓ Configurations ready\n")
|
||||
var peersYAML strings.Builder
|
||||
if len(peers) == 0 {
|
||||
peersYAML.WriteString(" bootstrap_peers: []")
|
||||
} else {
|
||||
peersYAML.WriteString(" bootstrap_peers:\n")
|
||||
for _, p := range peers {
|
||||
fmt.Fprintf(&peersYAML, " - \"%s\"\n", p)
|
||||
}
|
||||
}
|
||||
|
||||
if joinAddr == "" {
|
||||
joinAddr = fmt.Sprintf("localhost:%d", rqliteHTTPPort)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`node:
|
||||
id: "%s"
|
||||
type: "node"
|
||||
listen_addresses:
|
||||
- "/ip4/%s/tcp/%d"
|
||||
data_dir: "%s"
|
||||
max_connections: 50
|
||||
|
||||
database:
|
||||
data_dir: "%s/rqlite"
|
||||
replication_factor: 3
|
||||
shard_count: 16
|
||||
max_database_size: 1073741824
|
||||
backup_interval: "24h"
|
||||
rqlite_port: %d
|
||||
rqlite_raft_port: %d
|
||||
rqlite_join_address: "%s"
|
||||
cluster_sync_interval: "30s"
|
||||
peer_inactivity_limit: "24h"
|
||||
min_cluster_size: 1
|
||||
|
||||
discovery:
|
||||
%s
|
||||
discovery_interval: "15s"
|
||||
bootstrap_port: %d
|
||||
http_adv_address: "%s:%d"
|
||||
raft_adv_address: "%s:%d"
|
||||
node_namespace: "default"
|
||||
|
||||
security:
|
||||
enable_tls: false
|
||||
|
||||
logging:
|
||||
level: "info"
|
||||
format: "console"
|
||||
`, nodeID, ipAddr, listenPort, dataDir, dataDir, rqliteHTTPPort, rqliteRaftPort, joinAddr, peersYAML.String(), 4001, ipAddr, rqliteHTTPPort, ipAddr, rqliteRaftPort)
|
||||
}
|
||||
|
||||
// generateGatewayConfigDirect generates gateway config directly
|
||||
func generateGatewayConfigDirect(bootstrapPeers string, enableHTTPS bool, domain, tlsCacheDir string) string {
|
||||
var peers []string
|
||||
if bootstrapPeers != "" {
|
||||
for _, p := range strings.Split(bootstrapPeers, ",") {
|
||||
if p = strings.TrimSpace(p); p != "" {
|
||||
peers = append(peers, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var peersYAML strings.Builder
|
||||
if len(peers) == 0 {
|
||||
peersYAML.WriteString("bootstrap_peers: []")
|
||||
} else {
|
||||
peersYAML.WriteString("bootstrap_peers:\n")
|
||||
for _, p := range peers {
|
||||
fmt.Fprintf(&peersYAML, " - \"%s\"\n", p)
|
||||
}
|
||||
}
|
||||
|
||||
var httpsYAML strings.Builder
|
||||
if enableHTTPS && domain != "" {
|
||||
fmt.Fprintf(&httpsYAML, "enable_https: true\n")
|
||||
fmt.Fprintf(&httpsYAML, "domain_name: \"%s\"\n", domain)
|
||||
if tlsCacheDir != "" {
|
||||
fmt.Fprintf(&httpsYAML, "tls_cache_dir: \"%s\"\n", tlsCacheDir)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(&httpsYAML, "enable_https: false\n")
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`listen_addr: ":6001"
|
||||
client_namespace: "default"
|
||||
rqlite_dsn: ""
|
||||
%s
|
||||
%s
|
||||
`, peersYAML.String(), httpsYAML.String())
|
||||
}
|
||||
|
||||
func createSystemdServices() {
|
||||
@ -937,6 +1564,10 @@ StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=debros-gateway
|
||||
|
||||
# Allow binding to privileged ports (80, 443) for HTTPS/ACME
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
ProtectSystem=strict
|
||||
|
||||
@ -12,6 +12,21 @@ import (
|
||||
// DefaultBootstrapPeers returns the library's default bootstrap peer multiaddrs.
|
||||
// These can be overridden by environment variables or config.
|
||||
func DefaultBootstrapPeers() []string {
|
||||
// Check environment variable first
|
||||
if envPeers := os.Getenv("DEBROS_BOOTSTRAP_PEERS"); envPeers != "" {
|
||||
peers := splitCSVOrSpace(envPeers)
|
||||
// Filter out empty strings
|
||||
result := make([]string, 0, len(peers))
|
||||
for _, p := range peers {
|
||||
if p != "" {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
if len(result) > 0 {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
defaultCfg := config.DefaultConfig()
|
||||
return defaultCfg.Discovery.BootstrapPeers
|
||||
}
|
||||
|
||||
@ -10,11 +10,16 @@ import (
|
||||
func TestDefaultBootstrapPeersNonEmpty(t *testing.T) {
|
||||
old := os.Getenv("DEBROS_BOOTSTRAP_PEERS")
|
||||
t.Cleanup(func() { os.Setenv("DEBROS_BOOTSTRAP_PEERS", old) })
|
||||
_ = os.Setenv("DEBROS_BOOTSTRAP_PEERS", "") // ensure not set
|
||||
// Set a valid bootstrap peer
|
||||
validPeer := "/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"
|
||||
_ = os.Setenv("DEBROS_BOOTSTRAP_PEERS", validPeer)
|
||||
peers := DefaultBootstrapPeers()
|
||||
if len(peers) == 0 {
|
||||
t.Fatalf("expected non-empty default bootstrap peers")
|
||||
}
|
||||
if peers[0] != validPeer {
|
||||
t.Fatalf("expected bootstrap peer %s, got %s", validPeer, peers[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDatabaseEndpointsEnvOverride(t *testing.T) {
|
||||
|
||||
@ -5,6 +5,55 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// validConfigForType returns a valid config for the given node type
|
||||
func validConfigForType(nodeType string) *Config {
|
||||
validPeer := "/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"
|
||||
cfg := &Config{
|
||||
Node: NodeConfig{
|
||||
Type: nodeType,
|
||||
ID: "test-node-id",
|
||||
ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"},
|
||||
DataDir: ".",
|
||||
MaxConnections: 50,
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
DataDir: ".",
|
||||
ReplicationFactor: 3,
|
||||
ShardCount: 16,
|
||||
MaxDatabaseSize: 1024,
|
||||
BackupInterval: 1 * time.Hour,
|
||||
RQLitePort: 5001,
|
||||
RQLiteRaftPort: 7001,
|
||||
MinClusterSize: 1,
|
||||
},
|
||||
Discovery: DiscoveryConfig{
|
||||
BootstrapPeers: []string{validPeer},
|
||||
DiscoveryInterval: 15 * time.Second,
|
||||
BootstrapPort: 4001,
|
||||
HttpAdvAddress: "127.0.0.1:5001",
|
||||
RaftAdvAddress: "127.0.0.1:7001",
|
||||
NodeNamespace: "default",
|
||||
},
|
||||
Logging: LoggingConfig{
|
||||
Level: "info",
|
||||
Format: "console",
|
||||
},
|
||||
}
|
||||
|
||||
// Set rqlite_join_address based on node type
|
||||
if nodeType == "node" {
|
||||
cfg.Database.RQLiteJoinAddress = "localhost:5001"
|
||||
// Node type requires bootstrap peers
|
||||
cfg.Discovery.BootstrapPeers = []string{validPeer}
|
||||
} else {
|
||||
// Bootstrap type: empty join address and peers optional
|
||||
cfg.Database.RQLiteJoinAddress = ""
|
||||
cfg.Discovery.BootstrapPeers = []string{}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func TestValidateNodeType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -19,11 +68,11 @@ func TestValidateNodeType(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Node: NodeConfig{Type: tt.nodeType, ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
|
||||
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001},
|
||||
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: 15 * time.Second, BootstrapPort: 4001, NodeNamespace: "default"},
|
||||
Logging: LoggingConfig{Level: "info", Format: "console"},
|
||||
cfg := validConfigForType("bootstrap") // Start with valid bootstrap
|
||||
if tt.nodeType == "node" {
|
||||
cfg = validConfigForType("node")
|
||||
} else {
|
||||
cfg.Node.Type = tt.nodeType
|
||||
}
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
@ -53,12 +102,8 @@ func TestValidateListenAddresses(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Node: NodeConfig{Type: "node", ListenAddresses: tt.addresses, DataDir: ".", MaxConnections: 50},
|
||||
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001, RQLiteJoinAddress: "localhost:7001"},
|
||||
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: 15 * time.Second, BootstrapPort: 4001, NodeNamespace: "default"},
|
||||
Logging: LoggingConfig{Level: "info", Format: "console"},
|
||||
}
|
||||
cfg := validConfigForType("node")
|
||||
cfg.Node.ListenAddresses = tt.addresses
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
t.Errorf("expected error, got none")
|
||||
@ -85,12 +130,8 @@ func TestValidateReplicationFactor(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
|
||||
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: tt.replication, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001, RQLiteJoinAddress: "localhost:7001"},
|
||||
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: 15 * time.Second, BootstrapPort: 4001, NodeNamespace: "default"},
|
||||
Logging: LoggingConfig{Level: "info", Format: "console"},
|
||||
}
|
||||
cfg := validConfigForType("node")
|
||||
cfg.Database.ReplicationFactor = tt.replication
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
t.Errorf("expected error, got none")
|
||||
@ -119,12 +160,9 @@ func TestValidateRQLitePorts(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
|
||||
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: tt.httpPort, RQLiteRaftPort: tt.raftPort, RQLiteJoinAddress: "localhost:7001"},
|
||||
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: 15 * time.Second, BootstrapPort: 4001, NodeNamespace: "default"},
|
||||
Logging: LoggingConfig{Level: "info", Format: "console"},
|
||||
}
|
||||
cfg := validConfigForType("node")
|
||||
cfg.Database.RQLitePort = tt.httpPort
|
||||
cfg.Database.RQLiteRaftPort = tt.raftPort
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
t.Errorf("expected error, got none")
|
||||
@ -143,9 +181,9 @@ func TestValidateRQLiteJoinAddress(t *testing.T) {
|
||||
joinAddr string
|
||||
shouldError bool
|
||||
}{
|
||||
{"node with join", "node", "localhost:7001", false},
|
||||
{"node with join", "node", "localhost:5001", false},
|
||||
{"node without join", "node", "", true},
|
||||
{"bootstrap with join", "bootstrap", "localhost:7001", true},
|
||||
{"bootstrap with join", "bootstrap", "localhost:5001", true},
|
||||
{"bootstrap without join", "bootstrap", "", false},
|
||||
{"invalid join format", "node", "localhost", true},
|
||||
{"invalid join port", "node", "localhost:99999", true},
|
||||
@ -153,12 +191,8 @@ func TestValidateRQLiteJoinAddress(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Node: NodeConfig{Type: tt.nodeType, ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
|
||||
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001, RQLiteJoinAddress: tt.joinAddr},
|
||||
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: 15 * time.Second, BootstrapPort: 4001, NodeNamespace: "default"},
|
||||
Logging: LoggingConfig{Level: "info", Format: "console"},
|
||||
}
|
||||
cfg := validConfigForType(tt.nodeType)
|
||||
cfg.Database.RQLiteJoinAddress = tt.joinAddr
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
t.Errorf("expected error, got none")
|
||||
@ -190,12 +224,8 @@ func TestValidateBootstrapPeers(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Node: NodeConfig{Type: tt.nodeType, ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
|
||||
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001, RQLiteJoinAddress: ""},
|
||||
Discovery: DiscoveryConfig{BootstrapPeers: tt.peers, DiscoveryInterval: 15 * time.Second, BootstrapPort: 4001, NodeNamespace: "default"},
|
||||
Logging: LoggingConfig{Level: "info", Format: "console"},
|
||||
}
|
||||
cfg := validConfigForType(tt.nodeType)
|
||||
cfg.Discovery.BootstrapPeers = tt.peers
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
t.Errorf("expected error, got none")
|
||||
@ -223,12 +253,8 @@ func TestValidateLoggingLevel(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
|
||||
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001, RQLiteJoinAddress: "localhost:7001"},
|
||||
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: 15 * time.Second, BootstrapPort: 4001, NodeNamespace: "default"},
|
||||
Logging: LoggingConfig{Level: tt.level, Format: "console"},
|
||||
}
|
||||
cfg := validConfigForType("node")
|
||||
cfg.Logging.Level = tt.level
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
t.Errorf("expected error, got none")
|
||||
@ -254,12 +280,8 @@ func TestValidateLoggingFormat(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
|
||||
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001, RQLiteJoinAddress: "localhost:7001"},
|
||||
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: 15 * time.Second, BootstrapPort: 4001, NodeNamespace: "default"},
|
||||
Logging: LoggingConfig{Level: "info", Format: tt.format},
|
||||
}
|
||||
cfg := validConfigForType("node")
|
||||
cfg.Logging.Format = tt.format
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
t.Errorf("expected error, got none")
|
||||
@ -285,12 +307,8 @@ func TestValidateMaxConnections(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: tt.maxConn},
|
||||
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001, RQLiteJoinAddress: "localhost:7001"},
|
||||
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: 15 * time.Second, BootstrapPort: 4001, NodeNamespace: "default"},
|
||||
Logging: LoggingConfig{Level: "info", Format: "console"},
|
||||
}
|
||||
cfg := validConfigForType("node")
|
||||
cfg.Node.MaxConnections = tt.maxConn
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
t.Errorf("expected error, got none")
|
||||
@ -316,12 +334,8 @@ func TestValidateDiscoveryInterval(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
|
||||
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001, RQLiteJoinAddress: "localhost:7001"},
|
||||
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: tt.interval, BootstrapPort: 4001, NodeNamespace: "default"},
|
||||
Logging: LoggingConfig{Level: "info", Format: "console"},
|
||||
}
|
||||
cfg := validConfigForType("node")
|
||||
cfg.Discovery.DiscoveryInterval = tt.interval
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
t.Errorf("expected error, got none")
|
||||
@ -347,12 +361,8 @@ func TestValidateBootstrapPort(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
|
||||
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001, RQLiteJoinAddress: "localhost:7001"},
|
||||
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: 15 * time.Second, BootstrapPort: tt.port, NodeNamespace: "default"},
|
||||
Logging: LoggingConfig{Level: "info", Format: "console"},
|
||||
}
|
||||
cfg := validConfigForType("node")
|
||||
cfg.Discovery.BootstrapPort = tt.port
|
||||
errs := cfg.Validate()
|
||||
if tt.shouldError && len(errs) == 0 {
|
||||
t.Errorf("expected error, got none")
|
||||
@ -383,6 +393,7 @@ func TestValidateCompleteConfig(t *testing.T) {
|
||||
RQLitePort: 5002,
|
||||
RQLiteRaftPort: 7002,
|
||||
RQLiteJoinAddress: "127.0.0.1:7001",
|
||||
MinClusterSize: 1,
|
||||
},
|
||||
Discovery: DiscoveryConfig{
|
||||
BootstrapPeers: []string{
|
||||
@ -390,7 +401,8 @@ func TestValidateCompleteConfig(t *testing.T) {
|
||||
},
|
||||
DiscoveryInterval: 15 * time.Second,
|
||||
BootstrapPort: 4001,
|
||||
HttpAdvAddress: "127.0.0.1",
|
||||
HttpAdvAddress: "127.0.0.1:5001",
|
||||
RaftAdvAddress: "127.0.0.1:7001",
|
||||
NodeNamespace: "default",
|
||||
},
|
||||
Security: SecurityConfig{
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/libp2p/go-libp2p/core/host"
|
||||
@ -114,9 +115,41 @@ func (d *Manager) handlePeerExchangeStream(s network.Stream) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter addresses to only include configured listen addresses, not ephemeral ports
|
||||
// Ephemeral ports are typically > 32768, so we filter those out
|
||||
filteredAddrs := make([]multiaddr.Multiaddr, 0)
|
||||
for _, addr := range addrs {
|
||||
// Extract TCP port from multiaddr
|
||||
port, err := addr.ValueForProtocol(multiaddr.P_TCP)
|
||||
if err == nil {
|
||||
portNum, err := strconv.Atoi(port)
|
||||
if err == nil {
|
||||
// Only include ports that are reasonable (not ephemeral ports > 32768)
|
||||
// Common LibP2P ports are typically < 10000
|
||||
if portNum > 0 && portNum <= 32767 {
|
||||
filteredAddrs = append(filteredAddrs, addr)
|
||||
}
|
||||
} else {
|
||||
// If we can't parse port, include it anyway (might be non-TCP)
|
||||
filteredAddrs = append(filteredAddrs, addr)
|
||||
}
|
||||
} else {
|
||||
// If no TCP port found, include it anyway (might be non-TCP)
|
||||
filteredAddrs = append(filteredAddrs, addr)
|
||||
}
|
||||
}
|
||||
|
||||
// If no addresses remain after filtering, skip this peer
|
||||
if len(filteredAddrs) == 0 {
|
||||
d.logger.Debug("No valid addresses after filtering ephemeral ports",
|
||||
zap.String("peer_id", pid.String()[:8]+"..."),
|
||||
zap.Int("original_count", len(addrs)))
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert addresses to strings
|
||||
addrStrs := make([]string, len(addrs))
|
||||
for i, addr := range addrs {
|
||||
addrStrs := make([]string, len(filteredAddrs))
|
||||
for i, addr := range filteredAddrs {
|
||||
addrStrs[i] = addr.String()
|
||||
}
|
||||
|
||||
@ -320,7 +353,7 @@ func (d *Manager) discoverViaPeerExchange(ctx context.Context, maxConnections in
|
||||
d.host.Peerstore().AddAddrs(parsedID, addrs, time.Hour*24)
|
||||
|
||||
// Try to connect
|
||||
connectCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||||
connectCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
||||
peerAddrInfo := peer.AddrInfo{ID: parsedID, Addrs: addrs}
|
||||
|
||||
if err := d.host.Connect(connectCtx, peerAddrInfo); err != nil {
|
||||
|
||||
@ -206,9 +206,31 @@ func isHopByHopHeader(header string) bool {
|
||||
|
||||
// isPrivateOrLocalHost checks if a host is private, local, or loopback
|
||||
func isPrivateOrLocalHost(host string) bool {
|
||||
// Strip port if present
|
||||
if idx := strings.LastIndex(host, ":"); idx != -1 {
|
||||
host = host[:idx]
|
||||
// Strip port if present, handling IPv6 addresses properly
|
||||
// IPv6 addresses in URLs are bracketed: [::1]:8080
|
||||
if strings.HasPrefix(host, "[") {
|
||||
// IPv6 address with brackets
|
||||
if idx := strings.LastIndex(host, "]"); idx != -1 {
|
||||
if idx+1 < len(host) && host[idx+1] == ':' {
|
||||
// Port present, strip it
|
||||
host = host[1:idx] // Remove brackets and port
|
||||
} else {
|
||||
// No port, just remove brackets
|
||||
host = host[1:idx]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// IPv4 or hostname, check for port
|
||||
if idx := strings.LastIndex(host, ":"); idx != -1 {
|
||||
// Check if it's an IPv6 address without brackets (contains multiple colons)
|
||||
colonCount := strings.Count(host, ":")
|
||||
if colonCount == 1 {
|
||||
// Single colon, likely IPv4 with port
|
||||
host = host[:idx]
|
||||
}
|
||||
// If multiple colons, it's IPv6 without brackets and no port
|
||||
// Leave host as-is
|
||||
}
|
||||
}
|
||||
|
||||
// Check for localhost variants
|
||||
|
||||
@ -70,6 +70,21 @@ func (c *Config) ValidateConfig() []error {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate HTTPS configuration
|
||||
if c.EnableHTTPS {
|
||||
if c.DomainName == "" {
|
||||
errs = append(errs, fmt.Errorf("gateway.domain_name: must be set when enable_https is true"))
|
||||
} else {
|
||||
// Basic domain validation
|
||||
if !isValidDomainName(c.DomainName) {
|
||||
errs = append(errs, fmt.Errorf("gateway.domain_name: invalid domain format"))
|
||||
}
|
||||
}
|
||||
if c.TLSCacheDir == "" {
|
||||
errs = append(errs, fmt.Errorf("gateway.tls_cache_dir: must be set when enable_https is true"))
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
@ -135,3 +150,38 @@ func extractTCPPort(multiaddrStr string) string {
|
||||
|
||||
return portPart[:firstSlashIndex]
|
||||
}
|
||||
|
||||
// isValidDomainName validates a domain name format
|
||||
func isValidDomainName(domain string) bool {
|
||||
domain = strings.TrimSpace(domain)
|
||||
if domain == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Basic validation: domain should contain at least one dot
|
||||
// and not start/end with dot or hyphen
|
||||
if !strings.Contains(domain, ".") {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.HasPrefix(domain, "-") || strings.HasSuffix(domain, "-") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for valid characters (letters, numbers, dots, hyphens)
|
||||
for _, char := range domain {
|
||||
if !((char >= 'a' && char <= 'z') ||
|
||||
(char >= 'A' && char <= 'Z') ||
|
||||
(char >= '0' && char <= '9') ||
|
||||
char == '.' ||
|
||||
char == '-') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@ -26,6 +26,11 @@ type Config struct {
|
||||
// Optional DSN for rqlite database/sql driver, e.g. "http://localhost:4001"
|
||||
// If empty, defaults to "http://localhost:4001".
|
||||
RQLiteDSN string
|
||||
|
||||
// HTTPS configuration
|
||||
EnableHTTPS bool // Enable HTTPS with ACME (Let's Encrypt)
|
||||
DomainName string // Domain name for HTTPS certificate
|
||||
TLSCacheDir string // Directory to cache TLS certificates (default: ~/.debros/tls-cache)
|
||||
}
|
||||
|
||||
type Gateway struct {
|
||||
|
||||
@ -328,7 +328,6 @@ func (n *Node) startLibP2P() error {
|
||||
n.logger.ComponentInfo(logging.ComponentLibP2P, "Localhost detected - disabling NAT services for local development")
|
||||
// Don't add NAT/AutoRelay options for localhost
|
||||
} else {
|
||||
// Production: enable NAT traversal
|
||||
n.logger.ComponentInfo(logging.ComponentLibP2P, "Production mode - enabling NAT services")
|
||||
opts = append(opts,
|
||||
libp2p.EnableNATService(),
|
||||
|
||||
45
scripts/install-hooks.sh
Executable file
45
scripts/install-hooks.sh
Executable file
@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Install git hooks from .githooks/ to .git/hooks/
|
||||
# This ensures the pre-push hook runs automatically
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
GITHOOKS_DIR="$REPO_ROOT/.githooks"
|
||||
GIT_HOOKS_DIR="$REPO_ROOT/.git/hooks"
|
||||
|
||||
if [ ! -d "$GITHOOKS_DIR" ]; then
|
||||
echo "Error: .githooks directory not found at $GITHOOKS_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$GIT_HOOKS_DIR" ]; then
|
||||
echo "Error: .git/hooks directory not found at $GIT_HOOKS_DIR"
|
||||
echo "Are you in a git repository?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing git hooks..."
|
||||
|
||||
# Copy all hooks from .githooks/ to .git/hooks/
|
||||
for hook in "$GITHOOKS_DIR"/*; do
|
||||
if [ -f "$hook" ]; then
|
||||
hook_name=$(basename "$hook")
|
||||
dest="$GIT_HOOKS_DIR/$hook_name"
|
||||
|
||||
echo " Installing $hook_name..."
|
||||
cp "$hook" "$dest"
|
||||
chmod +x "$dest"
|
||||
|
||||
# Make sure the hook can find the repo root
|
||||
# The hooks already use relative paths, so this should work
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✓ Git hooks installed successfully!"
|
||||
echo ""
|
||||
echo "The following hooks are now active:"
|
||||
ls -1 "$GIT_HOOKS_DIR"/* 2>/dev/null | xargs -n1 basename || echo " (none)"
|
||||
|
||||
426
scripts/update_changelog.sh
Executable file
426
scripts/update_changelog.sh
Executable file
@ -0,0 +1,426 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NOCOLOR='\033[0m'
|
||||
|
||||
log() { echo -e "${CYAN}[update-changelog]${NOCOLOR} $1"; }
|
||||
error() { echo -e "${RED}[ERROR]${NOCOLOR} $1"; }
|
||||
success() { echo -e "${GREEN}[SUCCESS]${NOCOLOR} $1"; }
|
||||
warning() { echo -e "${YELLOW}[WARNING]${NOCOLOR} $1"; }
|
||||
|
||||
# File paths
|
||||
CHANGELOG_FILE="CHANGELOG.md"
|
||||
MAKEFILE="Makefile"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# Load environment variables from .env file if it exists
|
||||
if [ -f "$REPO_ROOT/.env" ]; then
|
||||
# Export variables from .env file (more portable than source <())
|
||||
set -a
|
||||
while IFS='=' read -r key value; do
|
||||
# Skip comments and empty lines
|
||||
[[ "$key" =~ ^#.*$ ]] && continue
|
||||
[[ -z "$key" ]] && continue
|
||||
# Remove quotes if present
|
||||
value=$(echo "$value" | sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")
|
||||
export "$key=$value"
|
||||
done < "$REPO_ROOT/.env"
|
||||
set +a
|
||||
fi
|
||||
|
||||
# OpenRouter API key
|
||||
# Priority: 1. Environment variable, 2. .env file, 3. Exit with error
|
||||
if [ -z "$OPENROUTER_API_KEY" ]; then
|
||||
error "OPENROUTER_API_KEY not found!"
|
||||
echo ""
|
||||
echo "Please set the API key in one of these ways:"
|
||||
echo " 1. Create a .env file in the repo root with:"
|
||||
echo " OPENROUTER_API_KEY=your-api-key-here"
|
||||
echo ""
|
||||
echo " 2. Set it as an environment variable:"
|
||||
echo " export OPENROUTER_API_KEY=your-api-key-here"
|
||||
echo ""
|
||||
echo " 3. Copy .env.example to .env and fill in your key:"
|
||||
echo " cp .env.example .env"
|
||||
echo ""
|
||||
echo "Get your API key from: https://openrouter.ai/keys"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check dependencies
|
||||
if ! command -v jq > /dev/null 2>&1; then
|
||||
error "jq is required but not installed. Install it with: brew install jq (macOS) or apt-get install jq (Linux)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v curl > /dev/null 2>&1; then
|
||||
error "curl is required but not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if we're in a git repo
|
||||
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
error "Not in a git repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get current branch
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
REMOTE_BRANCH="origin/$CURRENT_BRANCH"
|
||||
|
||||
# Check if remote branch exists
|
||||
if ! git rev-parse --verify "$REMOTE_BRANCH" > /dev/null 2>&1; then
|
||||
warning "Remote branch $REMOTE_BRANCH does not exist. Using main/master as baseline."
|
||||
if git rev-parse --verify "origin/main" > /dev/null 2>&1; then
|
||||
REMOTE_BRANCH="origin/main"
|
||||
elif git rev-parse --verify "origin/master" > /dev/null 2>&1; then
|
||||
REMOTE_BRANCH="origin/master"
|
||||
else
|
||||
warning "No remote branch found. Using HEAD as baseline."
|
||||
REMOTE_BRANCH="HEAD"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Gather all git diffs
|
||||
log "Collecting git diffs..."
|
||||
|
||||
# Check if running from pre-commit context
|
||||
if [ "$CHANGELOG_CONTEXT" = "pre-commit" ]; then
|
||||
log "Running in pre-commit context - analyzing staged changes only"
|
||||
|
||||
# Unstaged changes (usually none in pre-commit, but check anyway)
|
||||
UNSTAGED_DIFF=$(git diff 2>/dev/null || echo "")
|
||||
UNSTAGED_COUNT=$(echo "$UNSTAGED_DIFF" | grep -c "^diff\|^index" 2>/dev/null || echo "0")
|
||||
[ -z "$UNSTAGED_COUNT" ] && UNSTAGED_COUNT="0"
|
||||
|
||||
# Staged changes (these are what we're committing)
|
||||
STAGED_DIFF=$(git diff --cached 2>/dev/null || echo "")
|
||||
STAGED_COUNT=$(echo "$STAGED_DIFF" | grep -c "^diff\|^index" 2>/dev/null || echo "0")
|
||||
[ -z "$STAGED_COUNT" ] && STAGED_COUNT="0"
|
||||
|
||||
# No unpushed commits analysis in pre-commit context
|
||||
UNPUSHED_DIFF=""
|
||||
UNPUSHED_COMMITS="0"
|
||||
|
||||
log "Found: $UNSTAGED_COUNT unstaged file(s), $STAGED_COUNT staged file(s)"
|
||||
else
|
||||
# Pre-push context - analyze everything
|
||||
# Unstaged changes
|
||||
UNSTAGED_DIFF=$(git diff 2>/dev/null || echo "")
|
||||
UNSTAGED_COUNT=$(echo "$UNSTAGED_DIFF" | grep -c "^diff\|^index" 2>/dev/null || echo "0")
|
||||
[ -z "$UNSTAGED_COUNT" ] && UNSTAGED_COUNT="0"
|
||||
|
||||
# Staged changes
|
||||
STAGED_DIFF=$(git diff --cached 2>/dev/null || echo "")
|
||||
STAGED_COUNT=$(echo "$STAGED_DIFF" | grep -c "^diff\|^index" 2>/dev/null || echo "0")
|
||||
[ -z "$STAGED_COUNT" ] && STAGED_COUNT="0"
|
||||
|
||||
# Unpushed commits
|
||||
UNPUSHED_DIFF=$(git diff "$REMOTE_BRANCH"..HEAD 2>/dev/null || echo "")
|
||||
UNPUSHED_COMMITS=$(git rev-list --count "$REMOTE_BRANCH"..HEAD 2>/dev/null || echo "0")
|
||||
[ -z "$UNPUSHED_COMMITS" ] && UNPUSHED_COMMITS="0"
|
||||
|
||||
# Check if the only unpushed commit is a changelog update commit
|
||||
# If so, exclude it from the diff to avoid infinite loops
|
||||
if [ "$UNPUSHED_COMMITS" -gt 0 ]; then
|
||||
LATEST_COMMIT_MSG=$(git log -1 --pretty=%B HEAD 2>/dev/null || echo "")
|
||||
if echo "$LATEST_COMMIT_MSG" | grep -q "chore: update changelog and version"; then
|
||||
# If the latest commit is a changelog commit, check if there are other commits
|
||||
if [ "$UNPUSHED_COMMITS" -eq 1 ]; then
|
||||
log "Latest commit is a changelog update. No other changes detected. Skipping changelog update."
|
||||
# Clean up any old preview files
|
||||
rm -f "$REPO_ROOT/.changelog_preview.tmp" "$REPO_ROOT/.changelog_version.tmp"
|
||||
exit 0
|
||||
else
|
||||
# Multiple commits, exclude the latest changelog commit from diff
|
||||
log "Multiple unpushed commits detected. Excluding latest changelog commit from analysis."
|
||||
# Get all commits except the latest one
|
||||
UNPUSHED_DIFF=$(git diff "$REMOTE_BRANCH"..HEAD~1 2>/dev/null || echo "")
|
||||
UNPUSHED_COMMITS=$(git rev-list --count "$REMOTE_BRANCH"..HEAD~1 2>/dev/null || echo "0")
|
||||
[ -z "$UNPUSHED_COMMITS" ] && UNPUSHED_COMMITS="0"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Found: $UNSTAGED_COUNT unstaged file(s), $STAGED_COUNT staged file(s), $UNPUSHED_COMMITS unpushed commit(s)"
|
||||
fi
|
||||
|
||||
# Combine all diffs
|
||||
if [ "$CHANGELOG_CONTEXT" = "pre-commit" ]; then
|
||||
ALL_DIFFS="${UNSTAGED_DIFF}
|
||||
---
|
||||
STAGED CHANGES:
|
||||
---
|
||||
${STAGED_DIFF}"
|
||||
else
|
||||
ALL_DIFFS="${UNSTAGED_DIFF}
|
||||
---
|
||||
STAGED CHANGES:
|
||||
---
|
||||
${STAGED_DIFF}
|
||||
---
|
||||
UNPUSHED COMMITS:
|
||||
---
|
||||
${UNPUSHED_DIFF}"
|
||||
fi
|
||||
|
||||
# Check if there are any changes
|
||||
if [ "$CHANGELOG_CONTEXT" = "pre-commit" ]; then
|
||||
# In pre-commit, only check staged changes
|
||||
if [ -z "$(echo "$STAGED_DIFF" | tr -d '[:space:]')" ]; then
|
||||
log "No staged changes detected. Skipping changelog update."
|
||||
rm -f "$REPO_ROOT/.changelog_preview.tmp" "$REPO_ROOT/.changelog_version.tmp"
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
# In pre-push, check all changes
|
||||
if [ -z "$(echo "$UNSTAGED_DIFF$STAGED_DIFF$UNPUSHED_DIFF" | tr -d '[:space:]')" ]; then
|
||||
log "No changes detected (unstaged, staged, or unpushed). Skipping changelog update."
|
||||
rm -f "$REPO_ROOT/.changelog_preview.tmp" "$REPO_ROOT/.changelog_version.tmp"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get current version from Makefile
|
||||
CURRENT_VERSION=$(grep "^VERSION :=" "$MAKEFILE" | sed 's/.*:= *//' | tr -d ' ')
|
||||
|
||||
if [ -z "$CURRENT_VERSION" ]; then
|
||||
error "Could not find VERSION in Makefile"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Current version: $CURRENT_VERSION"
|
||||
|
||||
# Get today's date programmatically (YYYY-MM-DD format)
|
||||
TODAY_DATE=$(date +%Y-%m-%d)
|
||||
log "Using date: $TODAY_DATE"
|
||||
|
||||
# Prepare prompt for OpenRouter
|
||||
PROMPT="You are analyzing git diffs to create a changelog entry. Based on the following git diffs, create a simple, easy-to-understand changelog entry.
|
||||
|
||||
Current version: $CURRENT_VERSION
|
||||
|
||||
Git diffs:
|
||||
\`\`\`
|
||||
$ALL_DIFFS
|
||||
\`\`\`
|
||||
|
||||
Please respond with ONLY a valid JSON object in this exact format:
|
||||
{
|
||||
\"version\": \"x.y.z\",
|
||||
\"bump_type\": \"minor\" or \"patch\",
|
||||
\"added\": [\"item1\", \"item2\"],
|
||||
\"changed\": [\"item1\", \"item2\"],
|
||||
\"fixed\": [\"item1\", \"item2\"]
|
||||
}
|
||||
|
||||
Rules:
|
||||
- Bump version based on changes: use \"minor\" for new features, \"patch\" for bug fixes and small changes
|
||||
- Never bump major version (keep major version the same)
|
||||
- Keep descriptions simple and easy to understand (1-2 sentences max per item)
|
||||
- Only include items that actually changed
|
||||
- If a category is empty, use an empty array []
|
||||
- Do NOT include a date field - the date will be set programmatically"
|
||||
|
||||
# Call OpenRouter API
|
||||
log "Calling OpenRouter API to generate changelog..."
|
||||
|
||||
# Prepare the JSON payload properly
|
||||
PROMPT_ESCAPED=$(echo "$PROMPT" | jq -Rs .)
|
||||
REQUEST_BODY=$(cat <<EOF
|
||||
{
|
||||
"model": "google/gemini-2.5-flash-preview-09-2025",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": $PROMPT_ESCAPED
|
||||
}
|
||||
],
|
||||
"temperature": 0.3
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# Debug: Check API key format (first 10 chars only)
|
||||
API_KEY_PREFIX="${OPENROUTER_API_KEY:0:10}..."
|
||||
log "Using API key: $API_KEY_PREFIX (length: ${#OPENROUTER_API_KEY})"
|
||||
|
||||
set +e # Temporarily disable exit on error to check curl response
|
||||
RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X POST "https://openrouter.ai/api/v1/chat/completions" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
|
||||
-d "$REQUEST_BODY")
|
||||
CURL_EXIT_CODE=$?
|
||||
|
||||
# Extract HTTP code and response body
|
||||
HTTP_CODE=$(echo "$RESPONSE" | grep -o "HTTP_CODE:[0-9]*" | cut -d: -f2)
|
||||
RESPONSE_BODY=$(echo "$RESPONSE" | sed '/HTTP_CODE:/d')
|
||||
|
||||
set -e # Re-enable exit on error
|
||||
|
||||
log "HTTP Status Code: $HTTP_CODE"
|
||||
|
||||
# Check if API call succeeded
|
||||
if [ $CURL_EXIT_CODE -ne 0 ] || [ -z "$RESPONSE_BODY" ]; then
|
||||
error "Failed to call OpenRouter API"
|
||||
if [ $CURL_EXIT_CODE -ne 0 ]; then
|
||||
echo "Network error (curl exit code: $CURL_EXIT_CODE)"
|
||||
else
|
||||
echo "Empty response from API"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for API errors in response
|
||||
if echo "$RESPONSE_BODY" | jq -e '.error' > /dev/null 2>&1; then
|
||||
error "OpenRouter API error:"
|
||||
ERROR_MESSAGE=$(echo "$RESPONSE_BODY" | jq -r '.error.message // .error' 2>/dev/null || echo "$RESPONSE_BODY")
|
||||
echo "$ERROR_MESSAGE"
|
||||
echo ""
|
||||
error "Full API response:"
|
||||
echo "$RESPONSE_BODY" | jq '.' 2>/dev/null || echo "$RESPONSE_BODY"
|
||||
echo ""
|
||||
error "The API key may be invalid or expired. Please verify your OpenRouter API key at https://openrouter.ai/keys"
|
||||
echo ""
|
||||
error "To test your API key manually, run:"
|
||||
echo " curl https://openrouter.ai/api/v1/chat/completions \\"
|
||||
echo " -H \"Content-Type: application/json\" \\"
|
||||
echo " -H \"Authorization: Bearer YOUR_API_KEY\" \\"
|
||||
echo " -d '{\"model\": \"google/gemini-2.5-flash-preview-09-2025\", \"messages\": [{\"role\": \"user\", \"content\": \"test\"}]}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract JSON from response
|
||||
JSON_CONTENT=$(echo "$RESPONSE_BODY" | jq -r '.choices[0].message.content' 2>/dev/null)
|
||||
|
||||
# Check if content was extracted
|
||||
if [ -z "$JSON_CONTENT" ] || [ "$JSON_CONTENT" = "null" ]; then
|
||||
error "Failed to extract content from API response"
|
||||
echo "Response: $RESPONSE_BODY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Try to extract JSON if it's wrapped in markdown code blocks
|
||||
if echo "$JSON_CONTENT" | grep -q '```json'; then
|
||||
JSON_CONTENT=$(echo "$JSON_CONTENT" | sed -n '/```json/,/```/p' | sed '1d;$d')
|
||||
elif echo "$JSON_CONTENT" | grep -q '```'; then
|
||||
JSON_CONTENT=$(echo "$JSON_CONTENT" | sed -n '/```/,/```/p' | sed '1d;$d')
|
||||
fi
|
||||
|
||||
# Validate JSON
|
||||
if ! echo "$JSON_CONTENT" | jq . > /dev/null 2>&1; then
|
||||
error "Invalid JSON response from API:"
|
||||
echo "$JSON_CONTENT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse JSON
|
||||
NEW_VERSION=$(echo "$JSON_CONTENT" | jq -r '.version')
|
||||
BUMP_TYPE=$(echo "$JSON_CONTENT" | jq -r '.bump_type')
|
||||
ADDED=$(echo "$JSON_CONTENT" | jq -r '.added[]?' | sed 's/^/- /')
|
||||
CHANGED=$(echo "$JSON_CONTENT" | jq -r '.changed[]?' | sed 's/^/- /')
|
||||
FIXED=$(echo "$JSON_CONTENT" | jq -r '.fixed[]?' | sed 's/^/- /')
|
||||
|
||||
log "Generated version: $NEW_VERSION ($BUMP_TYPE bump)"
|
||||
log "Date: $TODAY_DATE"
|
||||
|
||||
# Validate version format
|
||||
if ! echo "$NEW_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
error "Invalid version format: $NEW_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate bump type
|
||||
if [ "$BUMP_TYPE" != "minor" ] && [ "$BUMP_TYPE" != "patch" ]; then
|
||||
error "Invalid bump type: $BUMP_TYPE (must be 'minor' or 'patch')"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Update Makefile
|
||||
log "Updating Makefile..."
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS sed requires backup extension
|
||||
sed -i '' "s/^VERSION := .*/VERSION := $NEW_VERSION/" "$MAKEFILE"
|
||||
else
|
||||
# Linux sed
|
||||
sed -i "s/^VERSION := .*/VERSION := $NEW_VERSION/" "$MAKEFILE"
|
||||
fi
|
||||
success "Makefile updated to version $NEW_VERSION"
|
||||
|
||||
# Update CHANGELOG.md
|
||||
log "Updating CHANGELOG.md..."
|
||||
|
||||
# Create changelog entry
|
||||
CHANGELOG_ENTRY="## [$NEW_VERSION] - $TODAY_DATE
|
||||
|
||||
### Added
|
||||
"
|
||||
if [ -n "$ADDED" ]; then
|
||||
CHANGELOG_ENTRY+="$ADDED"$'\n'
|
||||
else
|
||||
CHANGELOG_ENTRY+="\n"
|
||||
fi
|
||||
|
||||
CHANGELOG_ENTRY+="
|
||||
### Changed
|
||||
"
|
||||
if [ -n "$CHANGED" ]; then
|
||||
CHANGELOG_ENTRY+="$CHANGED"$'\n'
|
||||
else
|
||||
CHANGELOG_ENTRY+="\n"
|
||||
fi
|
||||
|
||||
CHANGELOG_ENTRY+="
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
"
|
||||
if [ -n "$FIXED" ]; then
|
||||
CHANGELOG_ENTRY+="$FIXED"$'\n'
|
||||
else
|
||||
CHANGELOG_ENTRY+="\n"
|
||||
fi
|
||||
|
||||
CHANGELOG_ENTRY+="
|
||||
"
|
||||
|
||||
# Save preview to temp file for pre-push hook
|
||||
PREVIEW_FILE="$REPO_ROOT/.changelog_preview.tmp"
|
||||
echo "$CHANGELOG_ENTRY" > "$PREVIEW_FILE"
|
||||
echo "$NEW_VERSION" > "$REPO_ROOT/.changelog_version.tmp"
|
||||
|
||||
# Insert after [Unreleased] section using awk (more portable)
|
||||
# Find the line number after [Unreleased] section (after the "### Fixed" line)
|
||||
INSERT_LINE=$(awk '/^## \[Unreleased\]/{found=1} found && /^### Fixed$/{print NR+1; exit}' "$CHANGELOG_FILE")
|
||||
|
||||
if [ -z "$INSERT_LINE" ]; then
|
||||
# Fallback: insert after line 16 (after [Unreleased] section)
|
||||
INSERT_LINE=16
|
||||
fi
|
||||
|
||||
# Use a temp file approach to insert multiline content
|
||||
TMP_FILE=$(mktemp)
|
||||
{
|
||||
head -n $((INSERT_LINE - 1)) "$CHANGELOG_FILE"
|
||||
printf '%s' "$CHANGELOG_ENTRY"
|
||||
tail -n +$INSERT_LINE "$CHANGELOG_FILE"
|
||||
} > "$TMP_FILE"
|
||||
mv "$TMP_FILE" "$CHANGELOG_FILE"
|
||||
|
||||
success "CHANGELOG.md updated with version $NEW_VERSION"
|
||||
|
||||
log "Changelog update complete!"
|
||||
log "New version: $NEW_VERSION"
|
||||
log "Bump type: $BUMP_TYPE"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user