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
|
#!/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
|
go test ./... # Runs all tests in your repo
|
||||||
status=$?
|
status=$?
|
||||||
if [ $status -ne 0 ]; then
|
if [ $status -ne 0 ]; then
|
||||||
echo "Push aborted: some tests failed."
|
echo -e "${RED}Push aborted: some tests failed.${NOCOLOR}"
|
||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
echo "All tests passed. Proceeding with push."
|
echo -e "${GREEN}All tests passed. Proceeding with push.${NOCOLOR}"
|
||||||
fi
|
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
|
### Deprecated
|
||||||
|
|
||||||
### Fixed
|
### 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
|
## [0.53.0] - 2025-10-31
|
||||||
|
|
||||||
|
|||||||
9
Makefile
9
Makefile
@ -19,9 +19,9 @@ test-e2e:
|
|||||||
# Network - Distributed P2P Database System
|
# Network - Distributed P2P Database System
|
||||||
# Makefile for development and build tasks
|
# 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)
|
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)' -X 'main.date=$(DATE)'
|
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
|
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"
|
@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 build artifacts
|
||||||
clean:
|
clean:
|
||||||
@echo "Cleaning build artifacts..."
|
@echo "Cleaning build artifacts..."
|
||||||
|
|||||||
@ -108,6 +108,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
cli.HandleConnectCommand(args[0], timeout)
|
cli.HandleConnectCommand(args[0], timeout)
|
||||||
|
|
||||||
|
// RQLite commands
|
||||||
|
case "rqlite":
|
||||||
|
cli.HandleRQLiteCommand(args)
|
||||||
|
|
||||||
// Help
|
// Help
|
||||||
case "help", "--help", "-h":
|
case "help", "--help", "-h":
|
||||||
showHelp()
|
showHelp()
|
||||||
@ -175,6 +179,9 @@ func showHelp() {
|
|||||||
fmt.Printf("🗄️ Database:\n")
|
fmt.Printf("🗄️ Database:\n")
|
||||||
fmt.Printf(" query <sql> 🔐 Execute database query\n\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:\n")
|
||||||
fmt.Printf(" pubsub publish <topic> <msg> 🔐 Publish message\n")
|
fmt.Printf(" pubsub publish <topic> <msg> 🔐 Publish message\n")
|
||||||
fmt.Printf(" pubsub subscribe <topic> 🔐 Subscribe to topic\n")
|
fmt.Printf(" pubsub subscribe <topic> 🔐 Subscribe to topic\n")
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/DeBrosOfficial/network/pkg/config"
|
"github.com/DeBrosOfficial/network/pkg/config"
|
||||||
@ -53,6 +54,9 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
|
|||||||
ClientNamespace string `yaml:"client_namespace"`
|
ClientNamespace string `yaml:"client_namespace"`
|
||||||
RQLiteDSN string `yaml:"rqlite_dsn"`
|
RQLiteDSN string `yaml:"rqlite_dsn"`
|
||||||
BootstrapPeers []string `yaml:"bootstrap_peers"`
|
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)
|
data, err := os.ReadFile(configPath)
|
||||||
@ -79,6 +83,9 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
|
|||||||
ClientNamespace: "default",
|
ClientNamespace: "default",
|
||||||
BootstrapPeers: nil,
|
BootstrapPeers: nil,
|
||||||
RQLiteDSN: "",
|
RQLiteDSN: "",
|
||||||
|
EnableHTTPS: false,
|
||||||
|
DomainName: "",
|
||||||
|
TLSCacheDir: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
if v := strings.TrimSpace(y.ListenAddr); v != "" {
|
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
|
// Validate configuration
|
||||||
if errs := cfg.ValidateConfig(); len(errs) > 0 {
|
if errs := cfg.ValidateConfig(); len(errs) > 0 {
|
||||||
fmt.Fprintf(os.Stderr, "\nGateway configuration errors (%d):\n", len(errs))
|
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/gateway"
|
||||||
"github.com/DeBrosOfficial/network/pkg/logging"
|
"github.com/DeBrosOfficial/network/pkg/logging"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/crypto/acme/autocert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupLogger() *logging.ColoredLogger {
|
func setupLogger() *logging.ColoredLogger {
|
||||||
@ -42,6 +43,123 @@ func main() {
|
|||||||
|
|
||||||
logger.ComponentInfo(logging.ComponentGeneral, "Creating HTTP server and routes...")
|
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{
|
server := &http.Server{
|
||||||
Addr: cfg.ListenAddr,
|
Addr: cfg.ListenAddr,
|
||||||
Handler: gw.Routes(),
|
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)
|
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 {
|
if err := os.WriteFile(node2Path, []byte(node2Content), 0644); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Failed to write node2 config: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Failed to write node2 config: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -334,7 +334,7 @@ func initFullStack(force bool) {
|
|||||||
os.Exit(1)
|
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 {
|
if err := os.WriteFile(node3Path, []byte(node3Content), 0644); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Failed to write node3 config: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Failed to write node3 config: %v\n", err)
|
||||||
os.Exit(1)
|
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 (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/DeBrosOfficial/network/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleSetupCommand handles the interactive 'setup' command for VPS installation
|
// HandleSetupCommand handles the interactive 'setup' command for VPS installation
|
||||||
@ -108,19 +112,179 @@ func HandleSetupCommand(args []string) {
|
|||||||
fmt.Printf("✅ Setup Complete!\n")
|
fmt.Printf("✅ Setup Complete!\n")
|
||||||
fmt.Printf(strings.Repeat("=", 70) + "\n\n")
|
fmt.Printf(strings.Repeat("=", 70) + "\n\n")
|
||||||
fmt.Printf("DeBros Network is now running!\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("Service Management:\n")
|
||||||
fmt.Printf(" network-cli service status all\n")
|
fmt.Printf(" network-cli service status all\n")
|
||||||
fmt.Printf(" network-cli service logs node --follow\n")
|
fmt.Printf(" network-cli service logs node --follow\n")
|
||||||
fmt.Printf(" network-cli service restart gateway\n\n")
|
fmt.Printf(" network-cli service restart gateway\n\n")
|
||||||
fmt.Printf("Access DeBros User:\n")
|
fmt.Printf("Access DeBros User:\n")
|
||||||
fmt.Printf(" sudo -u debros bash\n\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("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")
|
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("Anyone Relay (Anon):\n")
|
||||||
fmt.Printf(" sudo systemctl status anon\n")
|
fmt.Printf(" sudo systemctl status anon\n")
|
||||||
fmt.Printf(" sudo tail -f /home/debros/.debros/logs/anon/notices.log\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 {
|
func detectLinuxDistro() string {
|
||||||
@ -214,6 +378,206 @@ func isValidHostPort(s string) bool {
|
|||||||
return true
|
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() {
|
func setupDebrosUser() {
|
||||||
fmt.Printf("👤 Setting up 'debros' user...\n")
|
fmt.Printf("👤 Setting up 'debros' user...\n")
|
||||||
|
|
||||||
@ -704,38 +1068,37 @@ func cloneAndBuild() {
|
|||||||
branch := promptBranch()
|
branch := promptBranch()
|
||||||
fmt.Printf(" Using branch: %s\n", branch)
|
fmt.Printf(" Using branch: %s\n", branch)
|
||||||
|
|
||||||
// Check if already cloned
|
// Remove existing repository if it exists (always start fresh)
|
||||||
if _, err := os.Stat("/home/debros/src/.git"); err == nil {
|
if _, err := os.Stat("/home/debros/src"); err == nil {
|
||||||
fmt.Printf(" Updating repository...\n")
|
fmt.Printf(" Removing existing repository...\n")
|
||||||
|
// Remove as root since we're running as root
|
||||||
// Check current branch and switch if needed
|
if err := os.RemoveAll("/home/debros/src"); err != nil {
|
||||||
currentBranchCmd := exec.Command("sudo", "-u", "debros", "git", "-C", "/home/debros/src", "rev-parse", "--abbrev-ref", "HEAD")
|
fmt.Fprintf(os.Stderr, "⚠️ Failed to remove existing repo as root: %v\n", err)
|
||||||
if output, err := currentBranchCmd.Output(); err == nil {
|
// Try as debros user as fallback (might work if files are owned by debros)
|
||||||
currentBranch := strings.TrimSpace(string(output))
|
removeCmd := exec.Command("sudo", "-u", "debros", "rm", "-rf", "/home/debros/src")
|
||||||
if currentBranch != branch {
|
if output, err := removeCmd.CombinedOutput(); err != nil {
|
||||||
fmt.Printf(" Switching from %s to %s...\n", currentBranch, branch)
|
fmt.Fprintf(os.Stderr, "⚠️ Failed to remove existing repo as debros user: %v\n%s\n", err, output)
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Wait a moment to ensure filesystem syncs
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
// Pull latest changes
|
// Ensure parent directory exists and has correct permissions
|
||||||
cmd := exec.Command("sudo", "-u", "debros", "git", "-C", "/home/debros/src", "pull", "origin", branch)
|
if err := os.MkdirAll("/home/debros", 0755); err != nil {
|
||||||
if err := cmd.Run(); err != nil {
|
fmt.Fprintf(os.Stderr, "⚠️ Failed to ensure debros home directory exists: %v\n", err)
|
||||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to update repo: %v\n", err)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
} else {
|
if err := exec.Command("chown", "debros:debros", "/home/debros").Run(); err != nil {
|
||||||
fmt.Printf(" Cloning repository...\n")
|
fmt.Fprintf(os.Stderr, "⚠️ Failed to chown debros home directory: %v\n", err)
|
||||||
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)
|
// Clone fresh repository
|
||||||
os.Exit(1)
|
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
|
// Build
|
||||||
@ -746,7 +1109,7 @@ func cloneAndBuild() {
|
|||||||
|
|
||||||
// Use sudo with --preserve-env=PATH to pass Go path to debros user
|
// Use sudo with --preserve-env=PATH to pass Go path to debros user
|
||||||
// Set HOME so Go knows where to create module cache
|
// 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.Dir = "/home/debros/src"
|
||||||
cmd.Env = append(os.Environ(), "HOME=/home/debros", "PATH="+os.Getenv("PATH")+":/usr/local/go/bin")
|
cmd.Env = append(os.Environ(), "HOME=/home/debros", "PATH="+os.Getenv("PATH")+":/usr/local/go/bin")
|
||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
@ -755,92 +1118,176 @@ func cloneAndBuild() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copy binaries
|
// Copy binaries
|
||||||
exec.Command("sh", "-c", "cp -r /home/debros/src/bin/* /home/debros/bin/").Run()
|
copyCmd := exec.Command("sh", "-c", "cp -r /home/debros/src/bin/* /home/debros/bin/")
|
||||||
exec.Command("chown", "-R", "debros:debros", "/home/debros/bin").Run()
|
if output, err := copyCmd.CombinedOutput(); err != nil {
|
||||||
exec.Command("chmod", "-R", "755", "/home/debros/bin").Run()
|
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")
|
fmt.Printf(" ✓ Built and installed\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateConfigsInteractive(force bool) {
|
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"
|
nodeConfigPath := "/home/debros/.debros/node.yaml"
|
||||||
gatewayPath := "/home/debros/.debros/gateway.yaml"
|
gatewayPath := "/home/debros/.debros/gateway.yaml"
|
||||||
|
|
||||||
// Check if node.yaml already exists
|
// Check if configs already exist
|
||||||
nodeExists := false
|
nodeExists := false
|
||||||
|
gatewayExists := false
|
||||||
if _, err := os.Stat(nodeConfigPath); err == nil {
|
if _, err := os.Stat(nodeConfigPath); err == nil {
|
||||||
nodeExists = true
|
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
|
// If both configs exist and not forcing, skip configuration prompts
|
||||||
// Pass empty bootstrap-peers and no join address for bootstrap node
|
if nodeExists && gatewayExists && !force {
|
||||||
bootstrapArgs := []string{
|
fmt.Printf(" ℹ️ Configuration files already exist (node.yaml and gateway.yaml)\n")
|
||||||
"-u", "debros",
|
fmt.Printf(" ℹ️ Skipping configuration generation\n\n")
|
||||||
"/home/debros/bin/network-cli", "config", "init",
|
|
||||||
"--type", "bootstrap",
|
|
||||||
"--bootstrap-peers", "",
|
|
||||||
}
|
|
||||||
if force {
|
|
||||||
bootstrapArgs = append(bootstrapArgs, "--force")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command("sudo", bootstrapArgs...)
|
// Only offer to add HTTPS if not already enabled
|
||||||
cmd.Stdin = nil // Explicitly close stdin to prevent interactive prompts
|
httpsAlreadyEnabled := false
|
||||||
output, err := cmd.CombinedOutput()
|
if data, err := os.ReadFile(gatewayPath); err == nil {
|
||||||
bootstrapCreated := (err == nil)
|
httpsAlreadyEnabled = strings.Contains(string(data), "enable_https: true")
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if !httpsAlreadyEnabled {
|
||||||
// Check if bootstrap.yaml already exists (config init failed because it exists)
|
fmt.Printf("🌐 Domain and HTTPS Configuration\n")
|
||||||
if _, statErr := os.Stat(bootstrapPath); statErr == nil {
|
fmt.Printf("Would you like to add HTTPS with a domain name to your existing gateway config? (yes/no) [default: no]: ")
|
||||||
fmt.Printf(" ℹ️ bootstrap.yaml already exists, skipping creation\n")
|
reader := bufio.NewReader(os.Stdin)
|
||||||
bootstrapCreated = true
|
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 {
|
} else {
|
||||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to generate bootstrap config: %v\n", err)
|
fmt.Printf(" ℹ️ HTTPS is already enabled in gateway.yaml\n")
|
||||||
if len(output) > 0 {
|
|
||||||
fmt.Fprintf(os.Stderr, " Output: %s\n", string(output))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
} 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
|
// Ask about node type
|
||||||
if !nodeExists && bootstrapCreated {
|
fmt.Printf("What type of node is this?\n")
|
||||||
// Check if bootstrap.yaml exists before renaming
|
fmt.Printf(" 1. Bootstrap node (cluster leader)\n")
|
||||||
if _, err := os.Stat(bootstrapPath); err == nil {
|
fmt.Printf(" 2. Regular node (joins existing cluster)\n")
|
||||||
renameCmd := exec.Command("sudo", "-u", "debros", "mv", bootstrapPath, nodeConfigPath)
|
fmt.Printf("Enter choice (1 or 2): ")
|
||||||
if err := renameCmd.Run(); err != nil {
|
reader := bufio.NewReader(os.Stdin)
|
||||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to rename config: %v\n", err)
|
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 {
|
} 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
|
// Write node config
|
||||||
if bootstrapCreated && !force {
|
if err := os.WriteFile(nodeConfigPath, []byte(nodeConfig), 0644); err != nil {
|
||||||
// Clean up bootstrap.yaml if it was just created but node.yaml already exists
|
fmt.Fprintf(os.Stderr, "❌ Failed to write node config: %v\n", err)
|
||||||
if _, err := os.Stat(bootstrapPath); err == nil {
|
os.Exit(1)
|
||||||
exec.Command("sudo", "-u", "debros", "rm", "-f", bootstrapPath).Run()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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
|
// Generate gateway config
|
||||||
// Check if gateway.yaml already exists
|
|
||||||
gatewayExists := false
|
|
||||||
if _, err := os.Stat(gatewayPath); err == nil {
|
if _, err := os.Stat(gatewayPath); err == nil {
|
||||||
gatewayExists = true
|
gatewayExists = true
|
||||||
if !force {
|
if !force {
|
||||||
@ -849,35 +1296,215 @@ func generateConfigsInteractive(force bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !gatewayExists || force {
|
if !gatewayExists || force {
|
||||||
gatewayArgs := []string{
|
// Prompt for domain and HTTPS configuration
|
||||||
"-u", "debros",
|
var domain string
|
||||||
"/home/debros/bin/network-cli", "config", "init",
|
var enableHTTPS bool
|
||||||
"--type", "gateway",
|
var tlsCacheDir string
|
||||||
"--bootstrap-peers", "",
|
|
||||||
}
|
|
||||||
if force {
|
|
||||||
gatewayArgs = append(gatewayArgs, "--force")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd = exec.Command("sudo", gatewayArgs...)
|
fmt.Printf("\n🌐 Domain and HTTPS Configuration\n")
|
||||||
cmd.Stdin = nil // Explicitly close stdin to prevent interactive prompts
|
fmt.Printf("Would you like to configure HTTPS with a domain name? (yes/no) [default: no]: ")
|
||||||
output, err = cmd.CombinedOutput()
|
response, _ := reader.ReadString('\n')
|
||||||
if err != nil {
|
response = strings.ToLower(strings.TrimSpace(response))
|
||||||
// Check if gateway.yaml already exists (config init failed because it exists)
|
|
||||||
if _, statErr := os.Stat(gatewayPath); statErr == nil {
|
if response == "yes" || response == "y" {
|
||||||
fmt.Printf(" ℹ️ gateway.yaml already exists, skipping creation\n")
|
// 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 {
|
} else {
|
||||||
fmt.Fprintf(os.Stderr, "⚠️ Failed to generate gateway config: %v\n", err)
|
// Prompt for domain name
|
||||||
if len(output) > 0 {
|
domain = promptDomainForHTTPS(reader, vpsIP)
|
||||||
fmt.Fprintf(os.Stderr, " Output: %s\n", string(output))
|
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() {
|
func createSystemdServices() {
|
||||||
@ -937,6 +1564,10 @@ StandardOutput=journal
|
|||||||
StandardError=journal
|
StandardError=journal
|
||||||
SyslogIdentifier=debros-gateway
|
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
|
NoNewPrivileges=yes
|
||||||
PrivateTmp=yes
|
PrivateTmp=yes
|
||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
|
|||||||
@ -12,6 +12,21 @@ import (
|
|||||||
// DefaultBootstrapPeers returns the library's default bootstrap peer multiaddrs.
|
// DefaultBootstrapPeers returns the library's default bootstrap peer multiaddrs.
|
||||||
// These can be overridden by environment variables or config.
|
// These can be overridden by environment variables or config.
|
||||||
func DefaultBootstrapPeers() []string {
|
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()
|
defaultCfg := config.DefaultConfig()
|
||||||
return defaultCfg.Discovery.BootstrapPeers
|
return defaultCfg.Discovery.BootstrapPeers
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,11 +10,16 @@ import (
|
|||||||
func TestDefaultBootstrapPeersNonEmpty(t *testing.T) {
|
func TestDefaultBootstrapPeersNonEmpty(t *testing.T) {
|
||||||
old := os.Getenv("DEBROS_BOOTSTRAP_PEERS")
|
old := os.Getenv("DEBROS_BOOTSTRAP_PEERS")
|
||||||
t.Cleanup(func() { os.Setenv("DEBROS_BOOTSTRAP_PEERS", old) })
|
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()
|
peers := DefaultBootstrapPeers()
|
||||||
if len(peers) == 0 {
|
if len(peers) == 0 {
|
||||||
t.Fatalf("expected non-empty default bootstrap peers")
|
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) {
|
func TestDefaultDatabaseEndpointsEnvOverride(t *testing.T) {
|
||||||
|
|||||||
@ -5,6 +5,55 @@ import (
|
|||||||
"time"
|
"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) {
|
func TestValidateNodeType(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -19,11 +68,11 @@ func TestValidateNodeType(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cfg := &Config{
|
cfg := validConfigForType("bootstrap") // Start with valid bootstrap
|
||||||
Node: NodeConfig{Type: tt.nodeType, ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
|
if tt.nodeType == "node" {
|
||||||
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001},
|
cfg = validConfigForType("node")
|
||||||
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: 15 * time.Second, BootstrapPort: 4001, NodeNamespace: "default"},
|
} else {
|
||||||
Logging: LoggingConfig{Level: "info", Format: "console"},
|
cfg.Node.Type = tt.nodeType
|
||||||
}
|
}
|
||||||
errs := cfg.Validate()
|
errs := cfg.Validate()
|
||||||
if tt.shouldError && len(errs) == 0 {
|
if tt.shouldError && len(errs) == 0 {
|
||||||
@ -53,12 +102,8 @@ func TestValidateListenAddresses(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cfg := &Config{
|
cfg := validConfigForType("node")
|
||||||
Node: NodeConfig{Type: "node", ListenAddresses: tt.addresses, DataDir: ".", MaxConnections: 50},
|
cfg.Node.ListenAddresses = tt.addresses
|
||||||
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"},
|
|
||||||
}
|
|
||||||
errs := cfg.Validate()
|
errs := cfg.Validate()
|
||||||
if tt.shouldError && len(errs) == 0 {
|
if tt.shouldError && len(errs) == 0 {
|
||||||
t.Errorf("expected error, got none")
|
t.Errorf("expected error, got none")
|
||||||
@ -85,12 +130,8 @@ func TestValidateReplicationFactor(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cfg := &Config{
|
cfg := validConfigForType("node")
|
||||||
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
|
cfg.Database.ReplicationFactor = tt.replication
|
||||||
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"},
|
|
||||||
}
|
|
||||||
errs := cfg.Validate()
|
errs := cfg.Validate()
|
||||||
if tt.shouldError && len(errs) == 0 {
|
if tt.shouldError && len(errs) == 0 {
|
||||||
t.Errorf("expected error, got none")
|
t.Errorf("expected error, got none")
|
||||||
@ -119,12 +160,9 @@ func TestValidateRQLitePorts(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cfg := &Config{
|
cfg := validConfigForType("node")
|
||||||
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
|
cfg.Database.RQLitePort = tt.httpPort
|
||||||
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: tt.httpPort, RQLiteRaftPort: tt.raftPort, RQLiteJoinAddress: "localhost:7001"},
|
cfg.Database.RQLiteRaftPort = tt.raftPort
|
||||||
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"},
|
|
||||||
}
|
|
||||||
errs := cfg.Validate()
|
errs := cfg.Validate()
|
||||||
if tt.shouldError && len(errs) == 0 {
|
if tt.shouldError && len(errs) == 0 {
|
||||||
t.Errorf("expected error, got none")
|
t.Errorf("expected error, got none")
|
||||||
@ -143,9 +181,9 @@ func TestValidateRQLiteJoinAddress(t *testing.T) {
|
|||||||
joinAddr string
|
joinAddr string
|
||||||
shouldError bool
|
shouldError bool
|
||||||
}{
|
}{
|
||||||
{"node with join", "node", "localhost:7001", false},
|
{"node with join", "node", "localhost:5001", false},
|
||||||
{"node without join", "node", "", true},
|
{"node without join", "node", "", true},
|
||||||
{"bootstrap with join", "bootstrap", "localhost:7001", true},
|
{"bootstrap with join", "bootstrap", "localhost:5001", true},
|
||||||
{"bootstrap without join", "bootstrap", "", false},
|
{"bootstrap without join", "bootstrap", "", false},
|
||||||
{"invalid join format", "node", "localhost", true},
|
{"invalid join format", "node", "localhost", true},
|
||||||
{"invalid join port", "node", "localhost:99999", true},
|
{"invalid join port", "node", "localhost:99999", true},
|
||||||
@ -153,12 +191,8 @@ func TestValidateRQLiteJoinAddress(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cfg := &Config{
|
cfg := validConfigForType(tt.nodeType)
|
||||||
Node: NodeConfig{Type: tt.nodeType, ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
|
cfg.Database.RQLiteJoinAddress = tt.joinAddr
|
||||||
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"},
|
|
||||||
}
|
|
||||||
errs := cfg.Validate()
|
errs := cfg.Validate()
|
||||||
if tt.shouldError && len(errs) == 0 {
|
if tt.shouldError && len(errs) == 0 {
|
||||||
t.Errorf("expected error, got none")
|
t.Errorf("expected error, got none")
|
||||||
@ -190,12 +224,8 @@ func TestValidateBootstrapPeers(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cfg := &Config{
|
cfg := validConfigForType(tt.nodeType)
|
||||||
Node: NodeConfig{Type: tt.nodeType, ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
|
cfg.Discovery.BootstrapPeers = tt.peers
|
||||||
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"},
|
|
||||||
}
|
|
||||||
errs := cfg.Validate()
|
errs := cfg.Validate()
|
||||||
if tt.shouldError && len(errs) == 0 {
|
if tt.shouldError && len(errs) == 0 {
|
||||||
t.Errorf("expected error, got none")
|
t.Errorf("expected error, got none")
|
||||||
@ -223,12 +253,8 @@ func TestValidateLoggingLevel(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cfg := &Config{
|
cfg := validConfigForType("node")
|
||||||
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
|
cfg.Logging.Level = tt.level
|
||||||
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"},
|
|
||||||
}
|
|
||||||
errs := cfg.Validate()
|
errs := cfg.Validate()
|
||||||
if tt.shouldError && len(errs) == 0 {
|
if tt.shouldError && len(errs) == 0 {
|
||||||
t.Errorf("expected error, got none")
|
t.Errorf("expected error, got none")
|
||||||
@ -254,12 +280,8 @@ func TestValidateLoggingFormat(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cfg := &Config{
|
cfg := validConfigForType("node")
|
||||||
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
|
cfg.Logging.Format = tt.format
|
||||||
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},
|
|
||||||
}
|
|
||||||
errs := cfg.Validate()
|
errs := cfg.Validate()
|
||||||
if tt.shouldError && len(errs) == 0 {
|
if tt.shouldError && len(errs) == 0 {
|
||||||
t.Errorf("expected error, got none")
|
t.Errorf("expected error, got none")
|
||||||
@ -285,12 +307,8 @@ func TestValidateMaxConnections(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cfg := &Config{
|
cfg := validConfigForType("node")
|
||||||
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: tt.maxConn},
|
cfg.Node.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"},
|
|
||||||
}
|
|
||||||
errs := cfg.Validate()
|
errs := cfg.Validate()
|
||||||
if tt.shouldError && len(errs) == 0 {
|
if tt.shouldError && len(errs) == 0 {
|
||||||
t.Errorf("expected error, got none")
|
t.Errorf("expected error, got none")
|
||||||
@ -316,12 +334,8 @@ func TestValidateDiscoveryInterval(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cfg := &Config{
|
cfg := validConfigForType("node")
|
||||||
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
|
cfg.Discovery.DiscoveryInterval = tt.interval
|
||||||
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"},
|
|
||||||
}
|
|
||||||
errs := cfg.Validate()
|
errs := cfg.Validate()
|
||||||
if tt.shouldError && len(errs) == 0 {
|
if tt.shouldError && len(errs) == 0 {
|
||||||
t.Errorf("expected error, got none")
|
t.Errorf("expected error, got none")
|
||||||
@ -347,12 +361,8 @@ func TestValidateBootstrapPort(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cfg := &Config{
|
cfg := validConfigForType("node")
|
||||||
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
|
cfg.Discovery.BootstrapPort = tt.port
|
||||||
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"},
|
|
||||||
}
|
|
||||||
errs := cfg.Validate()
|
errs := cfg.Validate()
|
||||||
if tt.shouldError && len(errs) == 0 {
|
if tt.shouldError && len(errs) == 0 {
|
||||||
t.Errorf("expected error, got none")
|
t.Errorf("expected error, got none")
|
||||||
@ -383,6 +393,7 @@ func TestValidateCompleteConfig(t *testing.T) {
|
|||||||
RQLitePort: 5002,
|
RQLitePort: 5002,
|
||||||
RQLiteRaftPort: 7002,
|
RQLiteRaftPort: 7002,
|
||||||
RQLiteJoinAddress: "127.0.0.1:7001",
|
RQLiteJoinAddress: "127.0.0.1:7001",
|
||||||
|
MinClusterSize: 1,
|
||||||
},
|
},
|
||||||
Discovery: DiscoveryConfig{
|
Discovery: DiscoveryConfig{
|
||||||
BootstrapPeers: []string{
|
BootstrapPeers: []string{
|
||||||
@ -390,7 +401,8 @@ func TestValidateCompleteConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
DiscoveryInterval: 15 * time.Second,
|
DiscoveryInterval: 15 * time.Second,
|
||||||
BootstrapPort: 4001,
|
BootstrapPort: 4001,
|
||||||
HttpAdvAddress: "127.0.0.1",
|
HttpAdvAddress: "127.0.0.1:5001",
|
||||||
|
RaftAdvAddress: "127.0.0.1:7001",
|
||||||
NodeNamespace: "default",
|
NodeNamespace: "default",
|
||||||
},
|
},
|
||||||
Security: SecurityConfig{
|
Security: SecurityConfig{
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/libp2p/go-libp2p/core/host"
|
"github.com/libp2p/go-libp2p/core/host"
|
||||||
@ -114,9 +115,41 @@ func (d *Manager) handlePeerExchangeStream(s network.Stream) {
|
|||||||
continue
|
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
|
// Convert addresses to strings
|
||||||
addrStrs := make([]string, len(addrs))
|
addrStrs := make([]string, len(filteredAddrs))
|
||||||
for i, addr := range addrs {
|
for i, addr := range filteredAddrs {
|
||||||
addrStrs[i] = addr.String()
|
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)
|
d.host.Peerstore().AddAddrs(parsedID, addrs, time.Hour*24)
|
||||||
|
|
||||||
// Try to connect
|
// 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}
|
peerAddrInfo := peer.AddrInfo{ID: parsedID, Addrs: addrs}
|
||||||
|
|
||||||
if err := d.host.Connect(connectCtx, peerAddrInfo); err != nil {
|
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
|
// isPrivateOrLocalHost checks if a host is private, local, or loopback
|
||||||
func isPrivateOrLocalHost(host string) bool {
|
func isPrivateOrLocalHost(host string) bool {
|
||||||
// Strip port if present
|
// Strip port if present, handling IPv6 addresses properly
|
||||||
if idx := strings.LastIndex(host, ":"); idx != -1 {
|
// IPv6 addresses in URLs are bracketed: [::1]:8080
|
||||||
host = host[:idx]
|
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
|
// 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
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,3 +150,38 @@ func extractTCPPort(multiaddrStr string) string {
|
|||||||
|
|
||||||
return portPart[:firstSlashIndex]
|
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"
|
// Optional DSN for rqlite database/sql driver, e.g. "http://localhost:4001"
|
||||||
// If empty, defaults to "http://localhost:4001".
|
// If empty, defaults to "http://localhost:4001".
|
||||||
RQLiteDSN string
|
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 {
|
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")
|
n.logger.ComponentInfo(logging.ComponentLibP2P, "Localhost detected - disabling NAT services for local development")
|
||||||
// Don't add NAT/AutoRelay options for localhost
|
// Don't add NAT/AutoRelay options for localhost
|
||||||
} else {
|
} else {
|
||||||
// Production: enable NAT traversal
|
|
||||||
n.logger.ComponentInfo(logging.ComponentLibP2P, "Production mode - enabling NAT services")
|
n.logger.ComponentInfo(logging.ComponentLibP2P, "Production mode - enabling NAT services")
|
||||||
opts = append(opts,
|
opts = append(opts,
|
||||||
libp2p.EnableNATService(),
|
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