mirror of
https://github.com/DeBrosOfficial/network.git
synced 2025-12-15 23:18:49 +00:00
commit
fe05240362
73
.github/workflows/release.yaml
vendored
Normal file
73
.github/workflows/release.yaml
vendored
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Need full history for changelog
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Run GoReleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v5
|
||||||
|
with:
|
||||||
|
distribution: goreleaser
|
||||||
|
version: latest
|
||||||
|
args: release --clean
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release-artifacts
|
||||||
|
path: dist/
|
||||||
|
retention-days: 5
|
||||||
|
|
||||||
|
# Optional: Publish to GitHub Packages (requires additional setup)
|
||||||
|
publish-packages:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build-release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release-artifacts
|
||||||
|
path: dist/
|
||||||
|
|
||||||
|
- name: Publish to GitHub Packages
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
echo "Publishing Debian packages to GitHub Packages..."
|
||||||
|
for deb in dist/*.deb; do
|
||||||
|
if [ -f "$deb" ]; then
|
||||||
|
curl -H "Authorization: token $GITHUB_TOKEN" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @"$deb" \
|
||||||
|
"https://uploads.github.com/repos/${{ github.repository }}/releases/upload?name=$(basename "$deb")"
|
||||||
|
fi
|
||||||
|
done
|
||||||
@ -1,64 +1,66 @@
|
|||||||
# GoReleaser config for network
|
# GoReleaser Configuration for DeBros Network
|
||||||
project_name: network
|
# Builds and releases the network-cli binary for multiple platforms
|
||||||
|
# Other binaries (node, gateway, identity) are installed via: network-cli setup
|
||||||
|
|
||||||
before:
|
project_name: debros-network
|
||||||
hooks:
|
|
||||||
- go mod tidy
|
env:
|
||||||
|
- GO111MODULE=on
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
- id: network-node
|
# network-cli binary - only build the CLI
|
||||||
main: ./cmd/node
|
|
||||||
binary: network-node
|
|
||||||
env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
flags: ["-trimpath"]
|
|
||||||
ldflags:
|
|
||||||
- -s -w
|
|
||||||
- -X main.version={{.Version}}
|
|
||||||
- -X main.commit={{.Commit}}
|
|
||||||
- -X main.date={{.Date}}
|
|
||||||
goos: [linux, darwin, windows]
|
|
||||||
goarch: [amd64, arm64]
|
|
||||||
mod_timestamp: '{{ .CommitDate }}'
|
|
||||||
|
|
||||||
- id: network-cli
|
- id: network-cli
|
||||||
main: ./cmd/cli
|
main: ./cmd/cli
|
||||||
binary: network-cli
|
binary: network-cli
|
||||||
env:
|
goos:
|
||||||
- CGO_ENABLED=0
|
- linux
|
||||||
flags: ["-trimpath"]
|
- darwin
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w
|
- -s -w
|
||||||
- -X main.version={{.Version}}
|
- -X main.version={{.Version}}
|
||||||
- -X main.commit={{.Commit}}
|
- -X main.commit={{.ShortCommit}}
|
||||||
- -X main.date={{.Date}}
|
- -X main.date={{.Date}}
|
||||||
goos: [linux, darwin, windows]
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
goarch: [amd64, arm64]
|
|
||||||
mod_timestamp: '{{ .CommitDate }}'
|
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- id: default
|
# Tar.gz archives for network-cli
|
||||||
builds: [network-node, network-cli]
|
- id: binaries
|
||||||
format: tar.gz
|
format: tar.gz
|
||||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||||
files:
|
files:
|
||||||
- LICENSE*
|
|
||||||
- README.md
|
- README.md
|
||||||
|
- LICENSE
|
||||||
|
- CHANGELOG.md
|
||||||
|
format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
format: zip
|
||||||
|
|
||||||
checksum:
|
checksum:
|
||||||
name_template: "checksums.txt"
|
name_template: "checksums.txt"
|
||||||
|
algorithm: sha256
|
||||||
|
|
||||||
signs:
|
snapshot:
|
||||||
- artifacts: checksum
|
name_template: "{{ incpatch .Version }}-next"
|
||||||
|
|
||||||
changelog:
|
changelog:
|
||||||
sort: asc
|
sort: asc
|
||||||
use: git
|
abbrev: -1
|
||||||
filters:
|
filters:
|
||||||
exclude:
|
exclude:
|
||||||
- '^docs:'
|
- '^docs:'
|
||||||
- '^test:'
|
- '^test:'
|
||||||
|
- '^chore:'
|
||||||
- '^ci:'
|
- '^ci:'
|
||||||
|
- Merge pull request
|
||||||
|
- Merge branch
|
||||||
|
|
||||||
release:
|
release:
|
||||||
|
github:
|
||||||
|
owner: DeBrosOfficial
|
||||||
|
name: network
|
||||||
|
draft: false
|
||||||
prerelease: auto
|
prerelease: auto
|
||||||
|
name_template: "Release {{.Version}}"
|
||||||
|
|||||||
46
CHANGELOG.md
46
CHANGELOG.md
@ -10,8 +10,53 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- **GoReleaser**: Updated to build only `network-cli` binary (v0.52.2+)
|
||||||
|
- Other binaries (node, gateway, identity) now installed via `network-cli setup`
|
||||||
|
- Cleaner, smaller release packages
|
||||||
|
- Resolves archive mismatch errors
|
||||||
|
- **GitHub Actions**: Updated artifact actions from v3 to v4 (deprecated versions)
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed install script to be more clear and bug fixing
|
||||||
|
|
||||||
|
## [0.52.1] - 2025-10-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **CLI Refactor**: Modularized monolithic CLI into `pkg/cli/` package structure for better maintainability
|
||||||
|
- New `environment.go`: Multi-environment management system (local, devnet, testnet)
|
||||||
|
- New `env_commands.go`: Environment switching commands (`env list`, `env switch`, `devnet enable`, `testnet enable`)
|
||||||
|
- New `setup.go`: Interactive VPS installation command (`network-cli setup`) that replaces bash install script
|
||||||
|
- New `service.go`: Systemd service management commands (`service start|stop|restart|status|logs`)
|
||||||
|
- New `auth_commands.go`, `config_commands.go`, `basic_commands.go`: Refactored commands into modular pkg/cli
|
||||||
|
- **Release Pipeline**: Complete automated release infrastructure via `.goreleaser.yaml` and GitHub Actions
|
||||||
|
- Multi-platform binary builds (Linux/macOS, amd64/arm64)
|
||||||
|
- Automatic GitHub Release creation with changelog and artifacts
|
||||||
|
- Semantic versioning support with pre-release handling
|
||||||
|
- **Environment Configuration**: Multi-environment switching system
|
||||||
|
- Default environments: local (http://localhost:6001), devnet (https://devnet.debros.network), testnet (https://testnet.debros.network)
|
||||||
|
- Stored in `~/.debros/environments.json`
|
||||||
|
- CLI auto-uses active environment for authentication and operations
|
||||||
|
- **Comprehensive Documentation**
|
||||||
|
- `.cursor/RELEASES.md`: Overview and quick start
|
||||||
|
- `.cursor/goreleaser-guide.md`: Detailed distribution guide
|
||||||
|
- `.cursor/release-checklist.md`: Quick reference
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **CLI Refactoring**: `cmd/cli/main.go` reduced from 1340 → 180 lines (thin router pattern)
|
||||||
|
- All business logic moved to modular `pkg/cli/` functions
|
||||||
|
- Easier to test, maintain, and extend individual commands
|
||||||
|
- **Installation**: `scripts/install-debros-network.sh` now APT-ready with fallback to source build
|
||||||
|
- **Setup Process**: Consolidated all installation logic into `network-cli setup` command
|
||||||
|
- Single unified installation regardless of installation method
|
||||||
|
- Interactive user experience with clear progress indicators
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
## [0.51.9] - 2025-10-25
|
## [0.51.9] - 2025-10-25
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@ -241,3 +286,4 @@ _Initial release._
|
|||||||
|
|
||||||
[keepachangelog]: https://keepachangelog.com/en/1.1.0/
|
[keepachangelog]: https://keepachangelog.com/en/1.1.0/
|
||||||
[semver]: https://semver.org/spec/v2.0.0.html
|
[semver]: https://semver.org/spec/v2.0.0.html
|
||||||
|
|
||||||
|
|||||||
2
Makefile
2
Makefile
@ -21,7 +21,7 @@ test-e2e:
|
|||||||
|
|
||||||
.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
|
||||||
|
|
||||||
VERSION := 0.51.9-beta
|
VERSION := 0.52.14
|
||||||
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)'
|
||||||
|
|||||||
1344
cmd/cli/main.go
1344
cmd/cli/main.go
File diff suppressed because it is too large
Load Diff
173
pkg/cli/auth_commands.go
Normal file
173
pkg/cli/auth_commands.go
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/DeBrosOfficial/network/pkg/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleAuthCommand handles authentication commands
|
||||||
|
func HandleAuthCommand(args []string) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
showAuthHelp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subcommand := args[0]
|
||||||
|
switch subcommand {
|
||||||
|
case "login":
|
||||||
|
handleAuthLogin()
|
||||||
|
case "logout":
|
||||||
|
handleAuthLogout()
|
||||||
|
case "whoami":
|
||||||
|
handleAuthWhoami()
|
||||||
|
case "status":
|
||||||
|
handleAuthStatus()
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "Unknown auth command: %s\n", subcommand)
|
||||||
|
showAuthHelp()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func showAuthHelp() {
|
||||||
|
fmt.Printf("🔐 Authentication Commands\n\n")
|
||||||
|
fmt.Printf("Usage: network-cli auth <subcommand>\n\n")
|
||||||
|
fmt.Printf("Subcommands:\n")
|
||||||
|
fmt.Printf(" login - Authenticate with wallet\n")
|
||||||
|
fmt.Printf(" logout - Clear stored credentials\n")
|
||||||
|
fmt.Printf(" whoami - Show current authentication status\n")
|
||||||
|
fmt.Printf(" status - Show detailed authentication info\n\n")
|
||||||
|
fmt.Printf("Examples:\n")
|
||||||
|
fmt.Printf(" network-cli auth login\n")
|
||||||
|
fmt.Printf(" network-cli auth whoami\n")
|
||||||
|
fmt.Printf(" network-cli auth status\n")
|
||||||
|
fmt.Printf(" network-cli auth logout\n\n")
|
||||||
|
fmt.Printf("Environment Variables:\n")
|
||||||
|
fmt.Printf(" DEBROS_GATEWAY_URL - Gateway URL (overrides environment config)\n\n")
|
||||||
|
fmt.Printf("Note: Authentication uses the currently active environment.\n")
|
||||||
|
fmt.Printf(" Use 'network-cli env current' to see your active environment.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAuthLogin() {
|
||||||
|
gatewayURL := getGatewayURL()
|
||||||
|
fmt.Printf("🔐 Authenticating with gateway at: %s\n", gatewayURL)
|
||||||
|
|
||||||
|
// Use the wallet authentication flow
|
||||||
|
creds, err := auth.PerformWalletAuthentication(gatewayURL)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Authentication failed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save credentials to file
|
||||||
|
if err := auth.SaveCredentialsForDefaultGateway(creds); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to save credentials: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
credsPath, _ := auth.GetCredentialsPath()
|
||||||
|
fmt.Printf("✅ Authentication successful!\n")
|
||||||
|
fmt.Printf("📁 Credentials saved to: %s\n", credsPath)
|
||||||
|
fmt.Printf("🎯 Wallet: %s\n", creds.Wallet)
|
||||||
|
fmt.Printf("🏢 Namespace: %s\n", creds.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAuthLogout() {
|
||||||
|
if err := auth.ClearAllCredentials(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to clear credentials: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println("✅ Logged out successfully - all credentials have been cleared")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAuthWhoami() {
|
||||||
|
store, err := auth.LoadCredentials()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to load credentials: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
gatewayURL := getGatewayURL()
|
||||||
|
creds, exists := store.GetCredentialsForGateway(gatewayURL)
|
||||||
|
|
||||||
|
if !exists || !creds.IsValid() {
|
||||||
|
fmt.Println("❌ Not authenticated - run 'network-cli auth login' to authenticate")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("✅ Authenticated")
|
||||||
|
fmt.Printf(" Wallet: %s\n", creds.Wallet)
|
||||||
|
fmt.Printf(" Namespace: %s\n", creds.Namespace)
|
||||||
|
fmt.Printf(" Issued At: %s\n", creds.IssuedAt.Format("2006-01-02 15:04:05"))
|
||||||
|
if !creds.ExpiresAt.IsZero() {
|
||||||
|
fmt.Printf(" Expires At: %s\n", creds.ExpiresAt.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
if !creds.LastUsedAt.IsZero() {
|
||||||
|
fmt.Printf(" Last Used: %s\n", creds.LastUsedAt.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
if creds.Plan != "" {
|
||||||
|
fmt.Printf(" Plan: %s\n", creds.Plan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAuthStatus() {
|
||||||
|
store, err := auth.LoadCredentials()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to load credentials: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
gatewayURL := getGatewayURL()
|
||||||
|
creds, exists := store.GetCredentialsForGateway(gatewayURL)
|
||||||
|
|
||||||
|
// Show active environment
|
||||||
|
env, err := GetActiveEnvironment()
|
||||||
|
if err == nil {
|
||||||
|
fmt.Printf("🌍 Active Environment: %s\n", env.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("🔐 Authentication Status")
|
||||||
|
fmt.Printf(" Gateway URL: %s\n", gatewayURL)
|
||||||
|
|
||||||
|
if !exists || creds == nil {
|
||||||
|
fmt.Println(" Status: ❌ Not authenticated")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !creds.IsValid() {
|
||||||
|
fmt.Println(" Status: ⚠️ Credentials expired")
|
||||||
|
if !creds.ExpiresAt.IsZero() {
|
||||||
|
fmt.Printf(" Expired At: %s\n", creds.ExpiresAt.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(" Status: ✅ Authenticated")
|
||||||
|
fmt.Printf(" Wallet: %s\n", creds.Wallet)
|
||||||
|
fmt.Printf(" Namespace: %s\n", creds.Namespace)
|
||||||
|
if !creds.ExpiresAt.IsZero() {
|
||||||
|
fmt.Printf(" Expires: %s\n", creds.ExpiresAt.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
if !creds.LastUsedAt.IsZero() {
|
||||||
|
fmt.Printf(" Last Used: %s\n", creds.LastUsedAt.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getGatewayURL returns the gateway URL based on environment or env var
|
||||||
|
func getGatewayURL() string {
|
||||||
|
// Check environment variable first (for backwards compatibility)
|
||||||
|
if url := os.Getenv("DEBROS_GATEWAY_URL"); url != "" {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get from active environment
|
||||||
|
env, err := GetActiveEnvironment()
|
||||||
|
if err == nil {
|
||||||
|
return env.GatewayURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to default
|
||||||
|
return "http://localhost:6001"
|
||||||
|
}
|
||||||
414
pkg/cli/basic_commands.go
Normal file
414
pkg/cli/basic_commands.go
Normal file
@ -0,0 +1,414 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/DeBrosOfficial/network/pkg/auth"
|
||||||
|
"github.com/DeBrosOfficial/network/pkg/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleHealthCommand handles the health command
|
||||||
|
func HandleHealthCommand(format string, timeout time.Duration) {
|
||||||
|
cli, err := createClient()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer cli.Disconnect()
|
||||||
|
|
||||||
|
health, err := cli.Health()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to get health: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if format == "json" {
|
||||||
|
printJSON(health)
|
||||||
|
} else {
|
||||||
|
printHealth(health)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlePeersCommand handles the peers command
|
||||||
|
func HandlePeersCommand(format string, timeout time.Duration) {
|
||||||
|
cli, err := createClient()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer cli.Disconnect()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
peers, err := cli.Network().GetPeers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to get peers: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if format == "json" {
|
||||||
|
printJSON(peers)
|
||||||
|
} else {
|
||||||
|
printPeers(peers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleStatusCommand handles the status command
|
||||||
|
func HandleStatusCommand(format string, timeout time.Duration) {
|
||||||
|
cli, err := createClient()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer cli.Disconnect()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
status, err := cli.Network().GetStatus(ctx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to get status: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if format == "json" {
|
||||||
|
printJSON(status)
|
||||||
|
} else {
|
||||||
|
printStatus(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleQueryCommand handles the query command
|
||||||
|
func HandleQueryCommand(sql, format string, timeout time.Duration) {
|
||||||
|
// Ensure user is authenticated
|
||||||
|
_ = ensureAuthenticated()
|
||||||
|
|
||||||
|
cli, err := createClient()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer cli.Disconnect()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result, err := cli.Database().Query(ctx, sql)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to execute query: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if format == "json" {
|
||||||
|
printJSON(result)
|
||||||
|
} else {
|
||||||
|
printQueryResult(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleConnectCommand handles the connect command
|
||||||
|
func HandleConnectCommand(peerAddr string, timeout time.Duration) {
|
||||||
|
cli, err := createClient()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer cli.Disconnect()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = cli.Network().ConnectToPeer(ctx, peerAddr)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to connect to peer: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✅ Connected to peer: %s\n", peerAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlePeerIDCommand handles the peer-id command
|
||||||
|
func HandlePeerIDCommand(format string, timeout time.Duration) {
|
||||||
|
cli, err := createClient()
|
||||||
|
if err == nil {
|
||||||
|
defer cli.Disconnect()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if status, err := cli.Network().GetStatus(ctx); err == nil {
|
||||||
|
if format == "json" {
|
||||||
|
printJSON(map[string]string{"peer_id": status.NodeID})
|
||||||
|
} else {
|
||||||
|
fmt.Printf("🆔 Peer ID: %s\n", status.NodeID)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Could not find peer ID. Make sure the node is running or identity files exist.\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlePubSubCommand handles pubsub commands
|
||||||
|
func HandlePubSubCommand(args []string, format string, timeout time.Duration) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: network-cli pubsub <publish|subscribe|topics> [args...]\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure user is authenticated
|
||||||
|
_ = ensureAuthenticated()
|
||||||
|
|
||||||
|
cli, err := createClient()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer cli.Disconnect()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
subcommand := args[0]
|
||||||
|
switch subcommand {
|
||||||
|
case "publish":
|
||||||
|
if len(args) < 3 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: network-cli pubsub publish <topic> <message>\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
err := cli.PubSub().Publish(ctx, args[1], []byte(args[2]))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to publish message: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("✅ Published message to topic: %s\n", args[1])
|
||||||
|
|
||||||
|
case "subscribe":
|
||||||
|
if len(args) < 2 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: network-cli pubsub subscribe <topic> [duration]\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
duration := 30 * time.Second
|
||||||
|
if len(args) > 2 {
|
||||||
|
if d, err := time.ParseDuration(args[2]); err == nil {
|
||||||
|
duration = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), duration)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
fmt.Printf("🔔 Subscribing to topic '%s' for %v...\n", args[1], duration)
|
||||||
|
|
||||||
|
messageHandler := func(topic string, data []byte) error {
|
||||||
|
fmt.Printf("📨 [%s] %s: %s\n", time.Now().Format("15:04:05"), topic, string(data))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cli.PubSub().Subscribe(ctx, args[1], messageHandler)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to subscribe: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
fmt.Printf("✅ Subscription ended\n")
|
||||||
|
|
||||||
|
case "topics":
|
||||||
|
topics, err := cli.PubSub().ListTopics(ctx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to list topics: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if format == "json" {
|
||||||
|
printJSON(topics)
|
||||||
|
} else {
|
||||||
|
for _, topic := range topics {
|
||||||
|
fmt.Println(topic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "Unknown pubsub command: %s\n", subcommand)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
|
||||||
|
func createClient() (client.NetworkClient, error) {
|
||||||
|
config := client.DefaultClientConfig("network-cli")
|
||||||
|
|
||||||
|
// Check for existing credentials using enhanced authentication
|
||||||
|
creds, err := auth.GetValidEnhancedCredentials()
|
||||||
|
if err != nil {
|
||||||
|
// No valid credentials found, use the enhanced authentication flow
|
||||||
|
gatewayURL := getGatewayURL()
|
||||||
|
|
||||||
|
newCreds, authErr := auth.GetOrPromptForCredentials(gatewayURL)
|
||||||
|
if authErr != nil {
|
||||||
|
return nil, fmt.Errorf("authentication failed: %w", authErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
creds = newCreds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure client with API key
|
||||||
|
config.APIKey = creds.APIKey
|
||||||
|
|
||||||
|
// Update last used time - the enhanced store handles saving automatically
|
||||||
|
creds.UpdateLastUsed()
|
||||||
|
|
||||||
|
networkClient, err := client.NewClient(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := networkClient.Connect(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return networkClient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureAuthenticated() *auth.Credentials {
|
||||||
|
gatewayURL := getGatewayURL()
|
||||||
|
|
||||||
|
credentials, err := auth.GetOrPromptForCredentials(gatewayURL)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Authentication failed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return credentials
|
||||||
|
}
|
||||||
|
|
||||||
|
func printHealth(health *client.HealthStatus) {
|
||||||
|
fmt.Printf("🏥 Network Health\n")
|
||||||
|
fmt.Printf("Status: %s\n", getStatusEmoji(health.Status)+health.Status)
|
||||||
|
fmt.Printf("Last Updated: %s\n", health.LastUpdated.Format("2006-01-02 15:04:05"))
|
||||||
|
fmt.Printf("Response Time: %v\n", health.ResponseTime)
|
||||||
|
fmt.Printf("\nChecks:\n")
|
||||||
|
for check, status := range health.Checks {
|
||||||
|
emoji := "✅"
|
||||||
|
if status != "ok" {
|
||||||
|
emoji = "❌"
|
||||||
|
}
|
||||||
|
fmt.Printf(" %s %s: %s\n", emoji, check, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printPeers(peers []client.PeerInfo) {
|
||||||
|
fmt.Printf("👥 Connected Peers (%d)\n\n", len(peers))
|
||||||
|
if len(peers) == 0 {
|
||||||
|
fmt.Printf("No peers connected\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, peer := range peers {
|
||||||
|
connEmoji := "🔴"
|
||||||
|
if peer.Connected {
|
||||||
|
connEmoji = "🟢"
|
||||||
|
}
|
||||||
|
fmt.Printf("%d. %s %s\n", i+1, connEmoji, peer.ID)
|
||||||
|
fmt.Printf(" Addresses: %v\n", peer.Addresses)
|
||||||
|
fmt.Printf(" Last Seen: %s\n", peer.LastSeen.Format("2006-01-02 15:04:05"))
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printStatus(status *client.NetworkStatus) {
|
||||||
|
fmt.Printf("🌐 Network Status\n")
|
||||||
|
fmt.Printf("Node ID: %s\n", status.NodeID)
|
||||||
|
fmt.Printf("Connected: %s\n", getBoolEmoji(status.Connected)+strconv.FormatBool(status.Connected))
|
||||||
|
fmt.Printf("Peer Count: %d\n", status.PeerCount)
|
||||||
|
fmt.Printf("Database Size: %s\n", formatBytes(status.DatabaseSize))
|
||||||
|
fmt.Printf("Uptime: %v\n", status.Uptime.Round(time.Second))
|
||||||
|
}
|
||||||
|
|
||||||
|
func printQueryResult(result *client.QueryResult) {
|
||||||
|
fmt.Printf("📊 Query Result\n")
|
||||||
|
fmt.Printf("Rows: %d\n\n", result.Count)
|
||||||
|
|
||||||
|
if len(result.Rows) == 0 {
|
||||||
|
fmt.Printf("No data returned\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print header
|
||||||
|
for i, col := range result.Columns {
|
||||||
|
if i > 0 {
|
||||||
|
fmt.Printf(" | ")
|
||||||
|
}
|
||||||
|
fmt.Printf("%-15s", col)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Print separator
|
||||||
|
for i := range result.Columns {
|
||||||
|
if i > 0 {
|
||||||
|
fmt.Printf("-+-")
|
||||||
|
}
|
||||||
|
fmt.Printf("%-15s", "---------------")
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Print rows
|
||||||
|
for _, row := range result.Rows {
|
||||||
|
for i, cell := range row {
|
||||||
|
if i > 0 {
|
||||||
|
fmt.Printf(" | ")
|
||||||
|
}
|
||||||
|
fmt.Printf("%-15v", cell)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printJSON(data interface{}) {
|
||||||
|
jsonData, err := json.MarshalIndent(data, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to marshal JSON: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(string(jsonData))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStatusEmoji(status string) string {
|
||||||
|
switch status {
|
||||||
|
case "healthy":
|
||||||
|
return "🟢 "
|
||||||
|
case "degraded":
|
||||||
|
return "🟡 "
|
||||||
|
case "unhealthy":
|
||||||
|
return "🔴 "
|
||||||
|
default:
|
||||||
|
return "⚪ "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBoolEmoji(b bool) string {
|
||||||
|
if b {
|
||||||
|
return "✅ "
|
||||||
|
}
|
||||||
|
return "❌ "
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatBytes(bytes int64) string {
|
||||||
|
const unit = 1024
|
||||||
|
if bytes < unit {
|
||||||
|
return fmt.Sprintf("%d B", bytes)
|
||||||
|
}
|
||||||
|
div, exp := int64(unit), 0
|
||||||
|
for n := bytes / unit; n >= unit; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
||||||
|
}
|
||||||
513
pkg/cli/config_commands.go
Normal file
513
pkg/cli/config_commands.go
Normal file
@ -0,0 +1,513 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/DeBrosOfficial/network/pkg/config"
|
||||||
|
"github.com/DeBrosOfficial/network/pkg/encryption"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleConfigCommand handles config management commands
|
||||||
|
func HandleConfigCommand(args []string) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
showConfigHelp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subcommand := args[0]
|
||||||
|
subargs := args[1:]
|
||||||
|
|
||||||
|
switch subcommand {
|
||||||
|
case "init":
|
||||||
|
handleConfigInit(subargs)
|
||||||
|
case "validate":
|
||||||
|
handleConfigValidate(subargs)
|
||||||
|
case "help":
|
||||||
|
showConfigHelp()
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "Unknown config subcommand: %s\n", subcommand)
|
||||||
|
showConfigHelp()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func showConfigHelp() {
|
||||||
|
fmt.Printf("Config Management Commands\n\n")
|
||||||
|
fmt.Printf("Usage: network-cli config <subcommand> [options]\n\n")
|
||||||
|
fmt.Printf("Subcommands:\n")
|
||||||
|
fmt.Printf(" init - Generate full network stack in ~/.debros (bootstrap + 2 nodes + gateway)\n")
|
||||||
|
fmt.Printf(" validate --name <file> - Validate a config file\n\n")
|
||||||
|
fmt.Printf("Init Default Behavior (no --type):\n")
|
||||||
|
fmt.Printf(" Generates bootstrap.yaml, node2.yaml, node3.yaml, gateway.yaml with:\n")
|
||||||
|
fmt.Printf(" - Auto-generated identities for bootstrap, node2, node3\n")
|
||||||
|
fmt.Printf(" - Correct bootstrap_peers and join addresses\n")
|
||||||
|
fmt.Printf(" - Default ports: P2P 4001-4003, HTTP 5001-5003, Raft 7001-7003\n\n")
|
||||||
|
fmt.Printf("Init Options:\n")
|
||||||
|
fmt.Printf(" --type <type> - Single config type: node, bootstrap, gateway (skips stack generation)\n")
|
||||||
|
fmt.Printf(" --name <file> - Output filename (default: depends on --type or 'stack' for full stack)\n")
|
||||||
|
fmt.Printf(" --force - Overwrite existing config/stack files\n\n")
|
||||||
|
fmt.Printf("Single Config Options (with --type):\n")
|
||||||
|
fmt.Printf(" --id <id> - Node ID for bootstrap peers\n")
|
||||||
|
fmt.Printf(" --listen-port <port> - LibP2P listen port (default: 4001)\n")
|
||||||
|
fmt.Printf(" --rqlite-http-port <port> - RQLite HTTP port (default: 5001)\n")
|
||||||
|
fmt.Printf(" --rqlite-raft-port <port> - RQLite Raft port (default: 7001)\n")
|
||||||
|
fmt.Printf(" --join <host:port> - RQLite address to join (required for non-bootstrap)\n")
|
||||||
|
fmt.Printf(" --bootstrap-peers <peers> - Comma-separated bootstrap peer multiaddrs\n\n")
|
||||||
|
fmt.Printf("Examples:\n")
|
||||||
|
fmt.Printf(" network-cli config init # Generate full stack\n")
|
||||||
|
fmt.Printf(" network-cli config init --force # Overwrite existing stack\n")
|
||||||
|
fmt.Printf(" network-cli config init --type bootstrap # Single bootstrap config (legacy)\n")
|
||||||
|
fmt.Printf(" network-cli config validate --name node.yaml\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleConfigInit(args []string) {
|
||||||
|
// Parse flags
|
||||||
|
var (
|
||||||
|
cfgType = ""
|
||||||
|
name = "" // Will be set based on type if not provided
|
||||||
|
id string
|
||||||
|
listenPort = 4001
|
||||||
|
rqliteHTTPPort = 5001
|
||||||
|
rqliteRaftPort = 7001
|
||||||
|
joinAddr string
|
||||||
|
bootstrapPeers string
|
||||||
|
force bool
|
||||||
|
)
|
||||||
|
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
switch args[i] {
|
||||||
|
case "--type":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
cfgType = args[i+1]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case "--name":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
name = args[i+1]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case "--id":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
id = args[i+1]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case "--listen-port":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
if p, err := strconv.Atoi(args[i+1]); err == nil {
|
||||||
|
listenPort = p
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case "--rqlite-http-port":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
if p, err := strconv.Atoi(args[i+1]); err == nil {
|
||||||
|
rqliteHTTPPort = p
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case "--rqlite-raft-port":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
if p, err := strconv.Atoi(args[i+1]); err == nil {
|
||||||
|
rqliteRaftPort = p
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case "--join":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
joinAddr = args[i+1]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case "--bootstrap-peers":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
bootstrapPeers = args[i+1]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
case "--force":
|
||||||
|
force = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If --type is not specified, generate full stack
|
||||||
|
if cfgType == "" {
|
||||||
|
initFullStack(force)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, continue with single-file generation
|
||||||
|
// Validate type
|
||||||
|
if cfgType != "node" && cfgType != "bootstrap" && cfgType != "gateway" {
|
||||||
|
fmt.Fprintf(os.Stderr, "Invalid --type: %s (expected: node, bootstrap, or gateway)\n", cfgType)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default name based on type if not provided
|
||||||
|
if name == "" {
|
||||||
|
switch cfgType {
|
||||||
|
case "bootstrap":
|
||||||
|
name = "bootstrap.yaml"
|
||||||
|
case "gateway":
|
||||||
|
name = "gateway.yaml"
|
||||||
|
default:
|
||||||
|
name = "node.yaml"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure config directory exists
|
||||||
|
configDir, err := config.EnsureConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to ensure config directory: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := filepath.Join(configDir, name)
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if !force {
|
||||||
|
if _, err := os.Stat(configPath); err == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Config file already exists at %s (use --force to overwrite)\n", configPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate config based on type
|
||||||
|
var configContent string
|
||||||
|
switch cfgType {
|
||||||
|
case "node":
|
||||||
|
configContent = GenerateNodeConfig(name, id, listenPort, rqliteHTTPPort, rqliteRaftPort, joinAddr, bootstrapPeers)
|
||||||
|
case "bootstrap":
|
||||||
|
configContent = GenerateBootstrapConfig(name, id, listenPort, rqliteHTTPPort, rqliteRaftPort)
|
||||||
|
case "gateway":
|
||||||
|
configContent = GenerateGatewayConfig(bootstrapPeers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write config file
|
||||||
|
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to write config file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✅ Configuration file created: %s\n", configPath)
|
||||||
|
fmt.Printf(" Type: %s\n", cfgType)
|
||||||
|
fmt.Printf("\nYou can now start the %s using the generated config.\n", cfgType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleConfigValidate(args []string) {
|
||||||
|
var name string
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
if args[i] == "--name" && i+1 < len(args) {
|
||||||
|
name = args[i+1]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "Missing --name flag\n")
|
||||||
|
showConfigHelp()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
configDir, err := config.ConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to get config directory: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := filepath.Join(configDir, name)
|
||||||
|
file, err := os.Open(configPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to open config file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var cfg config.Config
|
||||||
|
if err := config.DecodeStrict(file, &cfg); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to parse config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run validation
|
||||||
|
errs := cfg.Validate()
|
||||||
|
if len(errs) > 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "\n❌ Configuration errors (%d):\n", len(errs))
|
||||||
|
for _, err := range errs {
|
||||||
|
fmt.Fprintf(os.Stderr, " - %s\n", err)
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✅ Config is valid: %s\n", configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initFullStack(force bool) {
|
||||||
|
fmt.Printf("🚀 Initializing full network stack...\n")
|
||||||
|
|
||||||
|
// Ensure ~/.debros directory exists
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to get home directory: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
debrosDir := filepath.Join(homeDir, ".debros")
|
||||||
|
if err := os.MkdirAll(debrosDir, 0755); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to create ~/.debros directory: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Generate bootstrap identity
|
||||||
|
bootstrapIdentityDir := filepath.Join(debrosDir, "bootstrap")
|
||||||
|
bootstrapIdentityPath := filepath.Join(bootstrapIdentityDir, "identity.key")
|
||||||
|
|
||||||
|
if !force {
|
||||||
|
if _, err := os.Stat(bootstrapIdentityPath); err == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Bootstrap identity already exists at %s (use --force to overwrite)\n", bootstrapIdentityPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrapInfo, err := encryption.GenerateIdentity()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to generate bootstrap identity: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(bootstrapIdentityDir, 0755); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to create bootstrap data directory: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := encryption.SaveIdentity(bootstrapInfo, bootstrapIdentityPath); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to save bootstrap identity: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("✅ Generated bootstrap identity: %s (Peer ID: %s)\n", bootstrapIdentityPath, bootstrapInfo.PeerID.String())
|
||||||
|
|
||||||
|
// Construct bootstrap multiaddr
|
||||||
|
bootstrapMultiaddr := fmt.Sprintf("/ip4/127.0.0.1/tcp/4001/p2p/%s", bootstrapInfo.PeerID.String())
|
||||||
|
fmt.Printf(" Bootstrap multiaddr: %s\n", bootstrapMultiaddr)
|
||||||
|
|
||||||
|
// Generate configs for all nodes...
|
||||||
|
// (rest of the implementation - similar to what was in main.go)
|
||||||
|
// I'll keep it similar to the original for consistency
|
||||||
|
|
||||||
|
// Step 2: Generate bootstrap.yaml
|
||||||
|
bootstrapName := "bootstrap.yaml"
|
||||||
|
bootstrapPath := filepath.Join(debrosDir, bootstrapName)
|
||||||
|
if !force {
|
||||||
|
if _, err := os.Stat(bootstrapPath); err == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Bootstrap config already exists at %s (use --force to overwrite)\n", bootstrapPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bootstrapContent := GenerateBootstrapConfig(bootstrapName, "", 4001, 5001, 7001)
|
||||||
|
if err := os.WriteFile(bootstrapPath, []byte(bootstrapContent), 0644); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to write bootstrap config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("✅ Generated bootstrap config: %s\n", bootstrapPath)
|
||||||
|
|
||||||
|
// Step 3: Generate node2.yaml
|
||||||
|
node2Name := "node2.yaml"
|
||||||
|
node2Path := filepath.Join(debrosDir, node2Name)
|
||||||
|
if !force {
|
||||||
|
if _, err := os.Stat(node2Path); err == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Node2 config already exists at %s (use --force to overwrite)\n", node2Path)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node2Content := GenerateNodeConfig(node2Name, "", 4002, 5002, 7002, "localhost:7001", bootstrapMultiaddr)
|
||||||
|
if err := os.WriteFile(node2Path, []byte(node2Content), 0644); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to write node2 config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("✅ Generated node2 config: %s\n", node2Path)
|
||||||
|
|
||||||
|
// Step 4: Generate node3.yaml
|
||||||
|
node3Name := "node3.yaml"
|
||||||
|
node3Path := filepath.Join(debrosDir, node3Name)
|
||||||
|
if !force {
|
||||||
|
if _, err := os.Stat(node3Path); err == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Node3 config already exists at %s (use --force to overwrite)\n", node3Path)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node3Content := GenerateNodeConfig(node3Name, "", 4003, 5003, 7003, "localhost:7001", bootstrapMultiaddr)
|
||||||
|
if err := os.WriteFile(node3Path, []byte(node3Content), 0644); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to write node3 config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("✅ Generated node3 config: %s\n", node3Path)
|
||||||
|
|
||||||
|
// Step 5: Generate gateway.yaml
|
||||||
|
gatewayName := "gateway.yaml"
|
||||||
|
gatewayPath := filepath.Join(debrosDir, gatewayName)
|
||||||
|
if !force {
|
||||||
|
if _, err := os.Stat(gatewayPath); err == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Gateway config already exists at %s (use --force to overwrite)\n", gatewayPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gatewayContent := GenerateGatewayConfig(bootstrapMultiaddr)
|
||||||
|
if err := os.WriteFile(gatewayPath, []byte(gatewayContent), 0644); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to write gateway config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("✅ Generated gateway config: %s\n", gatewayPath)
|
||||||
|
|
||||||
|
fmt.Printf("\n" + strings.Repeat("=", 60) + "\n")
|
||||||
|
fmt.Printf("✅ Full network stack initialized successfully!\n")
|
||||||
|
fmt.Printf(strings.Repeat("=", 60) + "\n")
|
||||||
|
fmt.Printf("\nBootstrap Peer ID: %s\n", bootstrapInfo.PeerID.String())
|
||||||
|
fmt.Printf("Bootstrap Multiaddr: %s\n", bootstrapMultiaddr)
|
||||||
|
fmt.Printf("\nGenerated configs:\n")
|
||||||
|
fmt.Printf(" - %s\n", bootstrapPath)
|
||||||
|
fmt.Printf(" - %s\n", node2Path)
|
||||||
|
fmt.Printf(" - %s\n", node3Path)
|
||||||
|
fmt.Printf(" - %s\n", gatewayPath)
|
||||||
|
fmt.Printf("\nStart the network with: make dev\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateNodeConfig generates a node configuration
|
||||||
|
func GenerateNodeConfig(name, id string, listenPort, rqliteHTTPPort, rqliteRaftPort int, joinAddr, bootstrapPeers string) string {
|
||||||
|
nodeID := id
|
||||||
|
if nodeID == "" {
|
||||||
|
nodeID = fmt.Sprintf("node-%d", time.Now().Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse bootstrap peers
|
||||||
|
var peers []string
|
||||||
|
if bootstrapPeers != "" {
|
||||||
|
for _, p := range strings.Split(bootstrapPeers, ",") {
|
||||||
|
if p = strings.TrimSpace(p); p != "" {
|
||||||
|
peers = append(peers, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct data_dir from name stem (remove .yaml)
|
||||||
|
dataDir := strings.TrimSuffix(name, ".yaml")
|
||||||
|
dataDir = filepath.Join(os.ExpandEnv("~"), ".debros", dataDir)
|
||||||
|
|
||||||
|
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 = "localhost:5001"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(`node:
|
||||||
|
id: "%s"
|
||||||
|
type: "node"
|
||||||
|
listen_addresses:
|
||||||
|
- "/ip4/0.0.0.0/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"
|
||||||
|
|
||||||
|
discovery:
|
||||||
|
%s
|
||||||
|
discovery_interval: "15s"
|
||||||
|
bootstrap_port: %d
|
||||||
|
http_adv_address: "127.0.0.1:%d"
|
||||||
|
raft_adv_address: "127.0.0.1:%d"
|
||||||
|
node_namespace: "default"
|
||||||
|
|
||||||
|
security:
|
||||||
|
enable_tls: false
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level: "info"
|
||||||
|
format: "console"
|
||||||
|
`, nodeID, listenPort, dataDir, dataDir, rqliteHTTPPort, rqliteRaftPort, joinAddr, peersYAML.String(), 4001, rqliteHTTPPort, rqliteRaftPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateBootstrapConfig generates a bootstrap configuration
|
||||||
|
func GenerateBootstrapConfig(name, id string, listenPort, rqliteHTTPPort, rqliteRaftPort int) string {
|
||||||
|
nodeID := id
|
||||||
|
if nodeID == "" {
|
||||||
|
nodeID = "bootstrap"
|
||||||
|
}
|
||||||
|
|
||||||
|
dataDir := filepath.Join(os.ExpandEnv("~"), ".debros", "bootstrap")
|
||||||
|
|
||||||
|
return fmt.Sprintf(`node:
|
||||||
|
id: "%s"
|
||||||
|
type: "bootstrap"
|
||||||
|
listen_addresses:
|
||||||
|
- "/ip4/0.0.0.0/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: ""
|
||||||
|
|
||||||
|
discovery:
|
||||||
|
bootstrap_peers: []
|
||||||
|
discovery_interval: "15s"
|
||||||
|
bootstrap_port: %d
|
||||||
|
http_adv_address: "127.0.0.1:%d"
|
||||||
|
raft_adv_address: "127.0.0.1:%d"
|
||||||
|
node_namespace: "default"
|
||||||
|
|
||||||
|
security:
|
||||||
|
enable_tls: false
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level: "info"
|
||||||
|
format: "console"
|
||||||
|
`, nodeID, listenPort, dataDir, dataDir, rqliteHTTPPort, rqliteRaftPort, 4001, rqliteHTTPPort, rqliteRaftPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateGatewayConfig generates a gateway configuration
|
||||||
|
func GenerateGatewayConfig(bootstrapPeers 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(`listen_addr: ":6001"
|
||||||
|
client_namespace: "default"
|
||||||
|
rqlite_dsn: ""
|
||||||
|
%s
|
||||||
|
`, peersYAML.String())
|
||||||
|
}
|
||||||
142
pkg/cli/env_commands.go
Normal file
142
pkg/cli/env_commands.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleEnvCommand handles the 'env' command and its subcommands
|
||||||
|
func HandleEnvCommand(args []string) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
showEnvHelp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subcommand := args[0]
|
||||||
|
subargs := args[1:]
|
||||||
|
|
||||||
|
switch subcommand {
|
||||||
|
case "list":
|
||||||
|
handleEnvList()
|
||||||
|
case "current":
|
||||||
|
handleEnvCurrent()
|
||||||
|
case "switch":
|
||||||
|
handleEnvSwitch(subargs)
|
||||||
|
case "enable":
|
||||||
|
handleEnvEnable(subargs)
|
||||||
|
case "help":
|
||||||
|
showEnvHelp()
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "Unknown env subcommand: %s\n", subcommand)
|
||||||
|
showEnvHelp()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func showEnvHelp() {
|
||||||
|
fmt.Printf("🌍 Environment Management Commands\n\n")
|
||||||
|
fmt.Printf("Usage: network-cli env <subcommand>\n\n")
|
||||||
|
fmt.Printf("Subcommands:\n")
|
||||||
|
fmt.Printf(" list - List all available environments\n")
|
||||||
|
fmt.Printf(" current - Show current active environment\n")
|
||||||
|
fmt.Printf(" switch - Switch to a different environment\n")
|
||||||
|
fmt.Printf(" enable - Alias for 'switch' (e.g., 'devnet enable')\n\n")
|
||||||
|
fmt.Printf("Available Environments:\n")
|
||||||
|
fmt.Printf(" local - Local development (http://localhost:6001)\n")
|
||||||
|
fmt.Printf(" devnet - Development network (https://devnet.debros.network)\n")
|
||||||
|
fmt.Printf(" testnet - Test network (https://testnet.debros.network)\n\n")
|
||||||
|
fmt.Printf("Examples:\n")
|
||||||
|
fmt.Printf(" network-cli env list\n")
|
||||||
|
fmt.Printf(" network-cli env current\n")
|
||||||
|
fmt.Printf(" network-cli env switch devnet\n")
|
||||||
|
fmt.Printf(" network-cli env enable testnet\n")
|
||||||
|
fmt.Printf(" network-cli devnet enable # Shorthand for switch to devnet\n")
|
||||||
|
fmt.Printf(" network-cli testnet enable # Shorthand for switch to testnet\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleEnvList() {
|
||||||
|
// Initialize environments if needed
|
||||||
|
if err := InitializeEnvironments(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to initialize environments: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
envConfig, err := LoadEnvironmentConfig()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to load environment config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("🌍 Available Environments:\n\n")
|
||||||
|
for _, env := range envConfig.Environments {
|
||||||
|
active := ""
|
||||||
|
if env.Name == envConfig.ActiveEnvironment {
|
||||||
|
active = " ✅ (active)"
|
||||||
|
}
|
||||||
|
fmt.Printf(" %s%s\n", env.Name, active)
|
||||||
|
fmt.Printf(" Gateway: %s\n", env.GatewayURL)
|
||||||
|
fmt.Printf(" Description: %s\n\n", env.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleEnvCurrent() {
|
||||||
|
// Initialize environments if needed
|
||||||
|
if err := InitializeEnvironments(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to initialize environments: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
env, err := GetActiveEnvironment()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to get active environment: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✅ Current Environment: %s\n", env.Name)
|
||||||
|
fmt.Printf(" Gateway URL: %s\n", env.GatewayURL)
|
||||||
|
fmt.Printf(" Description: %s\n", env.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleEnvSwitch(args []string) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: network-cli env switch <environment>\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "Available: local, devnet, testnet\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
envName := args[0]
|
||||||
|
|
||||||
|
// Initialize environments if needed
|
||||||
|
if err := InitializeEnvironments(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to initialize environments: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get old environment
|
||||||
|
oldEnv, _ := GetActiveEnvironment()
|
||||||
|
|
||||||
|
// Switch environment
|
||||||
|
if err := SwitchEnvironment(envName); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to switch environment: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get new environment
|
||||||
|
newEnv, err := GetActiveEnvironment()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to get new environment: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldEnv != nil && oldEnv.Name != newEnv.Name {
|
||||||
|
fmt.Printf("✅ Switched environment: %s → %s\n", oldEnv.Name, newEnv.Name)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✅ Environment set to: %s\n", newEnv.Name)
|
||||||
|
}
|
||||||
|
fmt.Printf(" Gateway URL: %s\n", newEnv.GatewayURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleEnvEnable(args []string) {
|
||||||
|
// 'enable' is just an alias for 'switch'
|
||||||
|
handleEnvSwitch(args)
|
||||||
|
}
|
||||||
191
pkg/cli/environment.go
Normal file
191
pkg/cli/environment.go
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/DeBrosOfficial/network/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Environment represents a DeBros network environment
|
||||||
|
type Environment struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
GatewayURL string `json:"gateway_url"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnvironmentConfig stores all configured environments
|
||||||
|
type EnvironmentConfig struct {
|
||||||
|
Environments []Environment `json:"environments"`
|
||||||
|
ActiveEnvironment string `json:"active_environment"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default environments
|
||||||
|
var DefaultEnvironments = []Environment{
|
||||||
|
{
|
||||||
|
Name: "local",
|
||||||
|
GatewayURL: "http://localhost:6001",
|
||||||
|
Description: "Local development environment",
|
||||||
|
IsActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "devnet",
|
||||||
|
GatewayURL: "https://devnet.debros.network",
|
||||||
|
Description: "Development network (testnet)",
|
||||||
|
IsActive: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "testnet",
|
||||||
|
GatewayURL: "https://testnet.debros.network",
|
||||||
|
Description: "Test network (staging)",
|
||||||
|
IsActive: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEnvironmentConfigPath returns the path to the environment config file
|
||||||
|
func GetEnvironmentConfigPath() (string, error) {
|
||||||
|
configDir, err := config.ConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get config directory: %w", err)
|
||||||
|
}
|
||||||
|
return filepath.Join(configDir, "environments.json"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadEnvironmentConfig loads the environment configuration
|
||||||
|
func LoadEnvironmentConfig() (*EnvironmentConfig, error) {
|
||||||
|
path, err := GetEnvironmentConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If file doesn't exist, return default config
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return &EnvironmentConfig{
|
||||||
|
Environments: DefaultEnvironments,
|
||||||
|
ActiveEnvironment: "local",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read environment config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var envConfig EnvironmentConfig
|
||||||
|
if err := json.Unmarshal(data, &envConfig); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse environment config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &envConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveEnvironmentConfig saves the environment configuration
|
||||||
|
func SaveEnvironmentConfig(envConfig *EnvironmentConfig) error {
|
||||||
|
path, err := GetEnvironmentConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure config directory exists
|
||||||
|
configDir := filepath.Dir(path)
|
||||||
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create config directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(envConfig, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal environment config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write environment config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActiveEnvironment returns the currently active environment
|
||||||
|
func GetActiveEnvironment() (*Environment, error) {
|
||||||
|
envConfig, err := LoadEnvironmentConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, env := range envConfig.Environments {
|
||||||
|
if env.Name == envConfig.ActiveEnvironment {
|
||||||
|
return &env, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to local if active environment not found
|
||||||
|
for _, env := range envConfig.Environments {
|
||||||
|
if env.Name == "local" {
|
||||||
|
return &env, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("no active environment found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwitchEnvironment switches to a different environment
|
||||||
|
func SwitchEnvironment(name string) error {
|
||||||
|
envConfig, err := LoadEnvironmentConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if environment exists
|
||||||
|
found := false
|
||||||
|
for _, env := range envConfig.Environments {
|
||||||
|
if env.Name == name {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("environment '%s' not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
envConfig.ActiveEnvironment = name
|
||||||
|
return SaveEnvironmentConfig(envConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEnvironmentByName returns an environment by name
|
||||||
|
func GetEnvironmentByName(name string) (*Environment, error) {
|
||||||
|
envConfig, err := LoadEnvironmentConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, env := range envConfig.Environments {
|
||||||
|
if env.Name == name {
|
||||||
|
return &env, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("environment '%s' not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitializeEnvironments initializes the environment config with defaults
|
||||||
|
func InitializeEnvironments() error {
|
||||||
|
path, err := GetEnvironmentConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't overwrite existing config
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
envConfig := &EnvironmentConfig{
|
||||||
|
Environments: DefaultEnvironments,
|
||||||
|
ActiveEnvironment: "local",
|
||||||
|
}
|
||||||
|
|
||||||
|
return SaveEnvironmentConfig(envConfig)
|
||||||
|
}
|
||||||
243
pkg/cli/service.go
Normal file
243
pkg/cli/service.go
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleServiceCommand handles systemd service management commands
|
||||||
|
func HandleServiceCommand(args []string) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
showServiceHelp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Service commands are only supported on Linux with systemd\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
subcommand := args[0]
|
||||||
|
subargs := args[1:]
|
||||||
|
|
||||||
|
switch subcommand {
|
||||||
|
case "start":
|
||||||
|
handleServiceStart(subargs)
|
||||||
|
case "stop":
|
||||||
|
handleServiceStop(subargs)
|
||||||
|
case "restart":
|
||||||
|
handleServiceRestart(subargs)
|
||||||
|
case "status":
|
||||||
|
handleServiceStatus(subargs)
|
||||||
|
case "logs":
|
||||||
|
handleServiceLogs(subargs)
|
||||||
|
case "help":
|
||||||
|
showServiceHelp()
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "Unknown service subcommand: %s\n", subcommand)
|
||||||
|
showServiceHelp()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func showServiceHelp() {
|
||||||
|
fmt.Printf("🔧 Service Management Commands\n\n")
|
||||||
|
fmt.Printf("Usage: network-cli service <subcommand> <target> [options]\n\n")
|
||||||
|
fmt.Printf("Subcommands:\n")
|
||||||
|
fmt.Printf(" start <target> - Start services\n")
|
||||||
|
fmt.Printf(" stop <target> - Stop services\n")
|
||||||
|
fmt.Printf(" restart <target> - Restart services\n")
|
||||||
|
fmt.Printf(" status <target> - Show service status\n")
|
||||||
|
fmt.Printf(" logs <target> - View service logs\n\n")
|
||||||
|
fmt.Printf("Targets:\n")
|
||||||
|
fmt.Printf(" node - DeBros node service\n")
|
||||||
|
fmt.Printf(" gateway - DeBros gateway service\n")
|
||||||
|
fmt.Printf(" all - All DeBros services\n\n")
|
||||||
|
fmt.Printf("Logs Options:\n")
|
||||||
|
fmt.Printf(" --follow - Follow logs in real-time (-f)\n")
|
||||||
|
fmt.Printf(" --since=<time> - Show logs since time (e.g., '1h', '30m', '2d')\n")
|
||||||
|
fmt.Printf(" -n <lines> - Show last N lines\n\n")
|
||||||
|
fmt.Printf("Examples:\n")
|
||||||
|
fmt.Printf(" network-cli service start node\n")
|
||||||
|
fmt.Printf(" network-cli service status all\n")
|
||||||
|
fmt.Printf(" network-cli service restart gateway\n")
|
||||||
|
fmt.Printf(" network-cli service logs node --follow\n")
|
||||||
|
fmt.Printf(" network-cli service logs gateway --since=1h\n")
|
||||||
|
fmt.Printf(" network-cli service logs node -n 100\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServices(target string) []string {
|
||||||
|
switch target {
|
||||||
|
case "node":
|
||||||
|
return []string{"debros-node"}
|
||||||
|
case "gateway":
|
||||||
|
return []string{"debros-gateway"}
|
||||||
|
case "all":
|
||||||
|
return []string{"debros-node", "debros-gateway"}
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Invalid target: %s (use: node, gateway, or all)\n", target)
|
||||||
|
os.Exit(1)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireRoot() {
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ This command requires root privileges\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " Run with: sudo network-cli service ...\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleServiceStart(args []string) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: network-cli service start <node|gateway|all>\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
requireRoot()
|
||||||
|
|
||||||
|
target := args[0]
|
||||||
|
services := getServices(target)
|
||||||
|
|
||||||
|
fmt.Printf("🚀 Starting services...\n")
|
||||||
|
for _, service := range services {
|
||||||
|
cmd := exec.Command("systemctl", "start", service)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to start %s: %v\n", service, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Printf(" ✓ Started %s\n", service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleServiceStop(args []string) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: network-cli service stop <node|gateway|all>\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
requireRoot()
|
||||||
|
|
||||||
|
target := args[0]
|
||||||
|
services := getServices(target)
|
||||||
|
|
||||||
|
fmt.Printf("⏹️ Stopping services...\n")
|
||||||
|
for _, service := range services {
|
||||||
|
cmd := exec.Command("systemctl", "stop", service)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to stop %s: %v\n", service, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Printf(" ✓ Stopped %s\n", service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleServiceRestart(args []string) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: network-cli service restart <node|gateway|all>\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
requireRoot()
|
||||||
|
|
||||||
|
target := args[0]
|
||||||
|
services := getServices(target)
|
||||||
|
|
||||||
|
fmt.Printf("🔄 Restarting services...\n")
|
||||||
|
for _, service := range services {
|
||||||
|
cmd := exec.Command("systemctl", "restart", service)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to restart %s: %v\n", service, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Printf(" ✓ Restarted %s\n", service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleServiceStatus(args []string) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
args = []string{"all"} // Default to all
|
||||||
|
}
|
||||||
|
|
||||||
|
target := args[0]
|
||||||
|
services := getServices(target)
|
||||||
|
|
||||||
|
fmt.Printf("📊 Service Status:\n\n")
|
||||||
|
for _, service := range services {
|
||||||
|
// Use systemctl is-active to get simple status
|
||||||
|
cmd := exec.Command("systemctl", "is-active", service)
|
||||||
|
output, _ := cmd.Output()
|
||||||
|
status := strings.TrimSpace(string(output))
|
||||||
|
|
||||||
|
emoji := "❌"
|
||||||
|
if status == "active" {
|
||||||
|
emoji = "✅"
|
||||||
|
} else if status == "inactive" {
|
||||||
|
emoji = "⚪"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s %s: %s\n", emoji, service, status)
|
||||||
|
|
||||||
|
// Show detailed status
|
||||||
|
cmd = exec.Command("systemctl", "status", service, "--no-pager", "-l")
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Run()
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleServiceLogs(args []string) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: network-cli service logs <node|gateway> [--follow] [--since=<time>] [-n <lines>]\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
target := args[0]
|
||||||
|
if target == "all" {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Cannot show logs for 'all' - specify 'node' or 'gateway'\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
services := getServices(target)
|
||||||
|
if len(services) == 0 {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
service := services[0]
|
||||||
|
|
||||||
|
// Parse options
|
||||||
|
journalArgs := []string{"-u", service, "--no-pager"}
|
||||||
|
|
||||||
|
for i := 1; i < len(args); i++ {
|
||||||
|
arg := args[i]
|
||||||
|
switch {
|
||||||
|
case arg == "--follow" || arg == "-f":
|
||||||
|
journalArgs = append(journalArgs, "-f")
|
||||||
|
case strings.HasPrefix(arg, "--since="):
|
||||||
|
since := strings.TrimPrefix(arg, "--since=")
|
||||||
|
journalArgs = append(journalArgs, "--since="+since)
|
||||||
|
case arg == "-n":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
journalArgs = append(journalArgs, "-n", args[i+1])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("📜 Logs for %s:\n\n", service)
|
||||||
|
|
||||||
|
cmd := exec.Command("journalctl", journalArgs...)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to show logs: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
605
pkg/cli/setup.go
Normal file
605
pkg/cli/setup.go
Normal file
@ -0,0 +1,605 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleSetupCommand handles the interactive 'setup' command for VPS installation
|
||||||
|
func HandleSetupCommand(args []string) {
|
||||||
|
// Parse flags
|
||||||
|
force := false
|
||||||
|
for _, arg := range args {
|
||||||
|
if arg == "--force" {
|
||||||
|
force = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("🚀 DeBros Network Setup\n\n")
|
||||||
|
|
||||||
|
// Check if running as root
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ This command must be run as root (use sudo)\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check OS compatibility
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Setup command is only supported on Linux\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " For other platforms, please install manually\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect OS
|
||||||
|
osInfo := detectLinuxDistro()
|
||||||
|
fmt.Printf("📋 Detected OS: %s\n", osInfo)
|
||||||
|
|
||||||
|
if !isSupportedOS(osInfo) {
|
||||||
|
fmt.Fprintf(os.Stderr, "⚠️ Unsupported OS: %s\n", osInfo)
|
||||||
|
fmt.Fprintf(os.Stderr, " Supported: Ubuntu 22.04/24.04/25.04, Debian 12\n")
|
||||||
|
fmt.Printf("\nContinue anyway? (yes/no): ")
|
||||||
|
if !promptYesNo() {
|
||||||
|
fmt.Println("Setup cancelled.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show setup plan
|
||||||
|
fmt.Printf("\n" + strings.Repeat("=", 70) + "\n")
|
||||||
|
fmt.Printf("Setup Plan:\n")
|
||||||
|
fmt.Printf(" 1. Create 'debros' system user (if needed)\n")
|
||||||
|
fmt.Printf(" 2. Install system dependencies (curl, git, make, build tools)\n")
|
||||||
|
fmt.Printf(" 3. Install Go 1.21+ (if needed)\n")
|
||||||
|
fmt.Printf(" 4. Install RQLite database\n")
|
||||||
|
fmt.Printf(" 5. Create directories (/home/debros/bin, /home/debros/src)\n")
|
||||||
|
fmt.Printf(" 6. Clone and build DeBros Network\n")
|
||||||
|
fmt.Printf(" 7. Generate configuration files\n")
|
||||||
|
fmt.Printf(" 8. Create systemd services (debros-node, debros-gateway)\n")
|
||||||
|
fmt.Printf(" 9. Start and enable services\n")
|
||||||
|
fmt.Printf(strings.Repeat("=", 70) + "\n\n")
|
||||||
|
|
||||||
|
fmt.Printf("Ready to begin setup? (yes/no): ")
|
||||||
|
if !promptYesNo() {
|
||||||
|
fmt.Println("Setup cancelled.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n")
|
||||||
|
|
||||||
|
// Step 1: Setup debros user
|
||||||
|
setupDebrosUser()
|
||||||
|
|
||||||
|
// Step 2: Install dependencies
|
||||||
|
installSystemDependencies()
|
||||||
|
|
||||||
|
// Step 3: Install Go
|
||||||
|
ensureGo()
|
||||||
|
|
||||||
|
// Step 4: Install RQLite
|
||||||
|
installRQLite()
|
||||||
|
|
||||||
|
// Step 5: Setup directories
|
||||||
|
setupDirectories()
|
||||||
|
|
||||||
|
// Step 6: Clone and build
|
||||||
|
cloneAndBuild()
|
||||||
|
|
||||||
|
// Step 7: Generate configs (interactive)
|
||||||
|
generateConfigsInteractive(force)
|
||||||
|
|
||||||
|
// Step 8: Create systemd services
|
||||||
|
createSystemdServices()
|
||||||
|
|
||||||
|
// Step 9: Start services
|
||||||
|
startServices()
|
||||||
|
|
||||||
|
// Done!
|
||||||
|
fmt.Printf("\n" + strings.Repeat("=", 70) + "\n")
|
||||||
|
fmt.Printf("✅ Setup Complete!\n")
|
||||||
|
fmt.Printf(strings.Repeat("=", 70) + "\n\n")
|
||||||
|
fmt.Printf("DeBros Network is now running!\n\n")
|
||||||
|
fmt.Printf("Service Management:\n")
|
||||||
|
fmt.Printf(" network-cli service status all\n")
|
||||||
|
fmt.Printf(" network-cli service logs node --follow\n")
|
||||||
|
fmt.Printf(" network-cli service restart gateway\n\n")
|
||||||
|
fmt.Printf("Access DeBros User:\n")
|
||||||
|
fmt.Printf(" sudo -u debros bash\n\n")
|
||||||
|
fmt.Printf("Verify Installation:\n")
|
||||||
|
fmt.Printf(" curl http://localhost:6001/health\n")
|
||||||
|
fmt.Printf(" curl http://localhost:5001/status\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectLinuxDistro() string {
|
||||||
|
if data, err := os.ReadFile("/etc/os-release"); err == nil {
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
var id, version string
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.HasPrefix(line, "ID=") {
|
||||||
|
id = strings.Trim(strings.TrimPrefix(line, "ID="), "\"")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, "VERSION_ID=") {
|
||||||
|
version = strings.Trim(strings.TrimPrefix(line, "VERSION_ID="), "\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if id != "" && version != "" {
|
||||||
|
return fmt.Sprintf("%s %s", id, version)
|
||||||
|
}
|
||||||
|
if id != "" {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSupportedOS(osInfo string) bool {
|
||||||
|
supported := []string{
|
||||||
|
"ubuntu 22.04",
|
||||||
|
"ubuntu 24.04",
|
||||||
|
"ubuntu 25.04",
|
||||||
|
"debian 12",
|
||||||
|
}
|
||||||
|
for _, s := range supported {
|
||||||
|
if strings.Contains(strings.ToLower(osInfo), s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptYesNo() bool {
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
response, _ := reader.ReadString('\n')
|
||||||
|
response = strings.ToLower(strings.TrimSpace(response))
|
||||||
|
return response == "yes" || response == "y"
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidMultiaddr validates bootstrap peer multiaddr format
|
||||||
|
func isValidMultiaddr(s string) bool {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !(strings.HasPrefix(s, "/ip4/") || strings.HasPrefix(s, "/ip6/")) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains(s, "/p2p/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidHostPort validates host:port format
|
||||||
|
func isValidHostPort(s string) bool {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
parts := strings.Split(s, ":")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
host := strings.TrimSpace(parts[0])
|
||||||
|
port := strings.TrimSpace(parts[1])
|
||||||
|
if host == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Port must be a valid number between 1 and 65535
|
||||||
|
if portNum, err := strconv.Atoi(port); err != nil || portNum < 1 || portNum > 65535 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupDebrosUser() {
|
||||||
|
fmt.Printf("👤 Setting up 'debros' user...\n")
|
||||||
|
|
||||||
|
// Check if user exists
|
||||||
|
userExists := false
|
||||||
|
if _, err := exec.Command("id", "debros").CombinedOutput(); err == nil {
|
||||||
|
fmt.Printf(" ✓ User 'debros' already exists\n")
|
||||||
|
userExists = true
|
||||||
|
} else {
|
||||||
|
// Create user
|
||||||
|
cmd := exec.Command("useradd", "-r", "-m", "-s", "/bin/bash", "-d", "/home/debros", "debros")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to create user 'debros': %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf(" ✓ Created user 'debros'\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the user who invoked sudo (the actual user, not root)
|
||||||
|
sudoUser := os.Getenv("SUDO_USER")
|
||||||
|
if sudoUser == "" {
|
||||||
|
// If not running via sudo, skip sudoers setup
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create sudoers rule to allow passwordless access to debros user
|
||||||
|
sudoersRule := fmt.Sprintf("%s ALL=(debros) NOPASSWD: ALL\n", sudoUser)
|
||||||
|
sudoersFile := "/etc/sudoers.d/debros-access"
|
||||||
|
|
||||||
|
// Check if sudoers rule already exists
|
||||||
|
if existing, err := os.ReadFile(sudoersFile); err == nil {
|
||||||
|
if strings.Contains(string(existing), sudoUser) {
|
||||||
|
if !userExists {
|
||||||
|
fmt.Printf(" ✓ Sudoers access configured\n")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write sudoers rule
|
||||||
|
if err := os.WriteFile(sudoersFile, []byte(sudoersRule), 0440); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "⚠️ Failed to create sudoers rule: %v\n", err)
|
||||||
|
fmt.Fprintf(os.Stderr, " You can manually switch to debros using: sudo -u debros bash\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the sudoers file
|
||||||
|
if err := exec.Command("visudo", "-c", "-f", sudoersFile).Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "⚠️ Sudoers rule validation failed, removing file\n")
|
||||||
|
os.Remove(sudoersFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(" ✓ Sudoers access configured\n")
|
||||||
|
fmt.Printf(" You can now run: sudo -u debros bash\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func installSystemDependencies() {
|
||||||
|
fmt.Printf("📦 Installing system dependencies...\n")
|
||||||
|
|
||||||
|
// Detect package manager
|
||||||
|
var installCmd *exec.Cmd
|
||||||
|
if _, err := exec.LookPath("apt"); err == nil {
|
||||||
|
installCmd = exec.Command("apt", "update")
|
||||||
|
if err := installCmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "⚠️ apt update failed: %v\n", err)
|
||||||
|
}
|
||||||
|
installCmd = exec.Command("apt", "install", "-y", "curl", "git", "make", "build-essential", "wget")
|
||||||
|
} else if _, err := exec.LookPath("yum"); err == nil {
|
||||||
|
installCmd = exec.Command("yum", "install", "-y", "curl", "git", "make", "gcc", "wget")
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ No supported package manager found\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := installCmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to install dependencies: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(" ✓ Dependencies installed\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureGo() {
|
||||||
|
fmt.Printf("🔧 Checking Go installation...\n")
|
||||||
|
|
||||||
|
// Check if Go is already installed
|
||||||
|
if _, err := exec.LookPath("go"); err == nil {
|
||||||
|
fmt.Printf(" ✓ Go already installed\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(" Installing Go 1.21.6...\n")
|
||||||
|
|
||||||
|
// Download Go
|
||||||
|
arch := "amd64"
|
||||||
|
if runtime.GOARCH == "arm64" {
|
||||||
|
arch = "arm64"
|
||||||
|
}
|
||||||
|
goTarball := fmt.Sprintf("go1.21.6.linux-%s.tar.gz", arch)
|
||||||
|
goURL := fmt.Sprintf("https://go.dev/dl/%s", goTarball)
|
||||||
|
|
||||||
|
// Download
|
||||||
|
cmd := exec.Command("wget", "-q", goURL, "-O", "/tmp/"+goTarball)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to download Go: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract
|
||||||
|
cmd = exec.Command("tar", "-C", "/usr/local", "-xzf", "/tmp/"+goTarball)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to extract Go: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to PATH for current process
|
||||||
|
os.Setenv("PATH", os.Getenv("PATH")+":/usr/local/go/bin")
|
||||||
|
|
||||||
|
// Also add to debros user's .bashrc for persistent availability
|
||||||
|
debrosHome := "/home/debros"
|
||||||
|
bashrc := debrosHome + "/.bashrc"
|
||||||
|
pathLine := "\nexport PATH=$PATH:/usr/local/go/bin\n"
|
||||||
|
|
||||||
|
// Read existing bashrc
|
||||||
|
existing, _ := os.ReadFile(bashrc)
|
||||||
|
existingStr := string(existing)
|
||||||
|
|
||||||
|
// Add PATH if not already present
|
||||||
|
if !strings.Contains(existingStr, "/usr/local/go/bin") {
|
||||||
|
if err := os.WriteFile(bashrc, []byte(existingStr+pathLine), 0644); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "⚠️ Failed to update debros .bashrc: %v\n", err)
|
||||||
|
}
|
||||||
|
// Fix ownership
|
||||||
|
exec.Command("chown", "debros:debros", bashrc).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(" ✓ Go installed\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func installRQLite() {
|
||||||
|
fmt.Printf("🗄️ Installing RQLite...\n")
|
||||||
|
|
||||||
|
// Check if already installed
|
||||||
|
if _, err := exec.LookPath("rqlited"); err == nil {
|
||||||
|
fmt.Printf(" ✓ RQLite already installed\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
arch := "amd64"
|
||||||
|
switch runtime.GOARCH {
|
||||||
|
case "arm64":
|
||||||
|
arch = "arm64"
|
||||||
|
case "arm":
|
||||||
|
arch = "arm"
|
||||||
|
}
|
||||||
|
|
||||||
|
version := "8.43.0"
|
||||||
|
tarball := fmt.Sprintf("rqlite-v%s-linux-%s.tar.gz", version, arch)
|
||||||
|
url := fmt.Sprintf("https://github.com/rqlite/rqlite/releases/download/v%s/%s", version, tarball)
|
||||||
|
|
||||||
|
// Download
|
||||||
|
cmd := exec.Command("wget", "-q", url, "-O", "/tmp/"+tarball)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to download RQLite: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract
|
||||||
|
cmd = exec.Command("tar", "-C", "/tmp", "-xzf", "/tmp/"+tarball)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to extract RQLite: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy binaries
|
||||||
|
dir := fmt.Sprintf("/tmp/rqlite-v%s-linux-%s", version, arch)
|
||||||
|
exec.Command("cp", dir+"/rqlited", "/usr/local/bin/").Run()
|
||||||
|
exec.Command("cp", dir+"/rqlite", "/usr/local/bin/").Run()
|
||||||
|
exec.Command("chmod", "+x", "/usr/local/bin/rqlited").Run()
|
||||||
|
exec.Command("chmod", "+x", "/usr/local/bin/rqlite").Run()
|
||||||
|
|
||||||
|
fmt.Printf(" ✓ RQLite installed\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupDirectories() {
|
||||||
|
fmt.Printf("📁 Creating directories...\n")
|
||||||
|
|
||||||
|
dirs := []string{
|
||||||
|
"/home/debros/bin",
|
||||||
|
"/home/debros/src",
|
||||||
|
"/home/debros/.debros",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range dirs {
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to create %s: %v\n", dir, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
// Change ownership to debros
|
||||||
|
cmd := exec.Command("chown", "-R", "debros:debros", dir)
|
||||||
|
cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(" ✓ Directories created\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneAndBuild() {
|
||||||
|
fmt.Printf("🔨 Cloning and building DeBros Network...\n")
|
||||||
|
|
||||||
|
// Check if already cloned
|
||||||
|
if _, err := os.Stat("/home/debros/src/.git"); err == nil {
|
||||||
|
fmt.Printf(" Updating repository...\n")
|
||||||
|
cmd := exec.Command("sudo", "-u", "debros", "git", "-C", "/home/debros/src", "pull", "origin", "nightly")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "⚠️ Failed to update repo: %v\n", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" Cloning repository...\n")
|
||||||
|
cmd := exec.Command("sudo", "-u", "debros", "git", "clone", "--branch", "nightly", "--depth", "1", "https://github.com/DeBrosOfficial/network.git", "/home/debros/src")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to clone repo: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build
|
||||||
|
fmt.Printf(" Building binaries...\n")
|
||||||
|
|
||||||
|
// Ensure Go is in PATH for the build
|
||||||
|
os.Setenv("PATH", os.Getenv("PATH")+":/usr/local/go/bin")
|
||||||
|
|
||||||
|
// Use sudo with --preserve-env=PATH to pass Go path to debros user
|
||||||
|
cmd := exec.Command("sudo", "--preserve-env=PATH", "-u", "debros", "make", "build")
|
||||||
|
cmd.Dir = "/home/debros/src"
|
||||||
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to build: %v\n%s\n", err, output)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy binaries
|
||||||
|
exec.Command("sh", "-c", "cp -r /home/debros/src/bin/* /home/debros/bin/").Run()
|
||||||
|
exec.Command("chown", "-R", "debros:debros", "/home/debros/bin").Run()
|
||||||
|
exec.Command("chmod", "-R", "755", "/home/debros/bin").Run()
|
||||||
|
|
||||||
|
fmt.Printf(" ✓ Built and installed\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateConfigsInteractive(force bool) {
|
||||||
|
fmt.Printf("⚙️ Generating configurations...\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")
|
||||||
|
|
||||||
|
// Generate bootstrap node config with explicit parameters
|
||||||
|
// Pass empty bootstrap-peers and no join address for bootstrap node
|
||||||
|
bootstrapArgs := []string{
|
||||||
|
"-u", "debros",
|
||||||
|
"/home/debros/bin/network-cli", "config", "init",
|
||||||
|
"--type", "bootstrap",
|
||||||
|
"--bootstrap-peers", "",
|
||||||
|
}
|
||||||
|
if force {
|
||||||
|
bootstrapArgs = append(bootstrapArgs, "--force")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("sudo", bootstrapArgs...)
|
||||||
|
cmd.Stdin = nil // Explicitly close stdin to prevent interactive prompts
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "⚠️ Failed to generate bootstrap config: %v\n", err)
|
||||||
|
if len(output) > 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, " Output: %s\n", string(output))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ✓ Bootstrap node config created\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename bootstrap.yaml to node.yaml so the service can find it
|
||||||
|
renameCmd := exec.Command("sudo", "-u", "debros", "mv", "/home/debros/.debros/bootstrap.yaml", "/home/debros/.debros/node.yaml")
|
||||||
|
if err := renameCmd.Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "⚠️ Failed to rename config: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate gateway config with explicit empty bootstrap peers
|
||||||
|
gatewayArgs := []string{
|
||||||
|
"-u", "debros",
|
||||||
|
"/home/debros/bin/network-cli", "config", "init",
|
||||||
|
"--type", "gateway",
|
||||||
|
"--bootstrap-peers", "",
|
||||||
|
}
|
||||||
|
if force {
|
||||||
|
gatewayArgs = append(gatewayArgs, "--force")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = exec.Command("sudo", gatewayArgs...)
|
||||||
|
cmd.Stdin = nil // Explicitly close stdin to prevent interactive prompts
|
||||||
|
output, err = cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "⚠️ Failed to generate gateway config: %v\n", err)
|
||||||
|
if len(output) > 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, " Output: %s\n", string(output))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ✓ Gateway config created\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(" ✓ Configurations generated\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSystemdServices() {
|
||||||
|
fmt.Printf("🔧 Creating systemd services...\n")
|
||||||
|
|
||||||
|
// Node service
|
||||||
|
nodeService := `[Unit]
|
||||||
|
Description=DeBros Network Node
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=debros
|
||||||
|
Group=debros
|
||||||
|
WorkingDirectory=/home/debros/src
|
||||||
|
ExecStart=/home/debros/bin/node --config node.yaml
|
||||||
|
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||||
|
Environment=HOME=/home/debros
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=debros-node
|
||||||
|
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
PrivateTmp=yes
|
||||||
|
ProtectSystem=strict
|
||||||
|
ReadWritePaths=/home/debros
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
`
|
||||||
|
|
||||||
|
if err := os.WriteFile("/etc/systemd/system/debros-node.service", []byte(nodeService), 0644); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to create node service: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gateway service
|
||||||
|
gatewayService := `[Unit]
|
||||||
|
Description=DeBros Gateway
|
||||||
|
After=debros-node.service
|
||||||
|
Wants=debros-node.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=debros
|
||||||
|
Group=debros
|
||||||
|
WorkingDirectory=/home/debros/src
|
||||||
|
ExecStart=/home/debros/bin/gateway
|
||||||
|
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||||
|
Environment=HOME=/home/debros
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=debros-gateway
|
||||||
|
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
PrivateTmp=yes
|
||||||
|
ProtectSystem=strict
|
||||||
|
ReadWritePaths=/home/debros
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
`
|
||||||
|
|
||||||
|
if err := os.WriteFile("/etc/systemd/system/debros-gateway.service", []byte(gatewayService), 0644); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ Failed to create gateway service: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload systemd
|
||||||
|
exec.Command("systemctl", "daemon-reload").Run()
|
||||||
|
exec.Command("systemctl", "enable", "debros-node").Run()
|
||||||
|
exec.Command("systemctl", "enable", "debros-gateway").Run()
|
||||||
|
|
||||||
|
fmt.Printf(" ✓ Services created and enabled\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func startServices() {
|
||||||
|
fmt.Printf("🚀 Starting services...\n")
|
||||||
|
|
||||||
|
// Start node
|
||||||
|
if err := exec.Command("systemctl", "start", "debros-node").Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "⚠️ Failed to start node service: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ✓ Node service started\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start gateway
|
||||||
|
if err := exec.Command("systemctl", "start", "debros-gateway").Run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "⚠️ Failed to start gateway service: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ✓ Gateway service started\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -158,6 +158,8 @@ func (c *Client) Connect() error {
|
|||||||
var ps *libp2ppubsub.PubSub
|
var ps *libp2ppubsub.PubSub
|
||||||
ps, err = libp2ppubsub.NewGossipSub(context.Background(), h,
|
ps, err = libp2ppubsub.NewGossipSub(context.Background(), h,
|
||||||
libp2ppubsub.WithPeerExchange(true),
|
libp2ppubsub.WithPeerExchange(true),
|
||||||
|
libp2ppubsub.WithFloodPublish(true), // Ensure messages reach all peers, not just mesh
|
||||||
|
libp2ppubsub.WithDirectPeers(nil), // Enable direct peer connections
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.Close()
|
h.Close()
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
package gateway
|
package gateway
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -143,6 +145,7 @@ func (g *Gateway) verifyHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
Nonce string `json:"nonce"`
|
Nonce string `json:"nonce"`
|
||||||
Signature string `json:"signature"`
|
Signature string `json:"signature"`
|
||||||
Namespace string `json:"namespace"`
|
Namespace string `json:"namespace"`
|
||||||
|
ChainType string `json:"chain_type"` // "ETH" or "SOL", defaults to "ETH"
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
writeError(w, http.StatusBadRequest, "invalid json body")
|
writeError(w, http.StatusBadRequest, "invalid json body")
|
||||||
@ -176,8 +179,19 @@ func (g *Gateway) verifyHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
nonceID := nres.Rows[0][0]
|
nonceID := nres.Rows[0][0]
|
||||||
|
|
||||||
|
// Determine chain type (default to ETH for backward compatibility)
|
||||||
|
chainType := strings.ToUpper(strings.TrimSpace(req.ChainType))
|
||||||
|
if chainType == "" {
|
||||||
|
chainType = "ETH"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify signature based on chain type
|
||||||
|
var verified bool
|
||||||
|
var verifyErr error
|
||||||
|
|
||||||
|
switch chainType {
|
||||||
|
case "ETH":
|
||||||
// EVM personal_sign verification of the nonce
|
// EVM personal_sign verification of the nonce
|
||||||
// Hash: keccak256("\x19Ethereum Signed Message:\n" + len(nonce) + nonce)
|
|
||||||
msg := []byte(req.Nonce)
|
msg := []byte(req.Nonce)
|
||||||
prefix := []byte("\x19Ethereum Signed Message:\n" + strconv.Itoa(len(msg)))
|
prefix := []byte("\x19Ethereum Signed Message:\n" + strconv.Itoa(len(msg)))
|
||||||
hash := ethcrypto.Keccak256(prefix, msg)
|
hash := ethcrypto.Keccak256(prefix, msg)
|
||||||
@ -208,6 +222,51 @@ func (g *Gateway) verifyHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusUnauthorized, "signature does not match wallet")
|
writeError(w, http.StatusUnauthorized, "signature does not match wallet")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
verified = true
|
||||||
|
|
||||||
|
case "SOL":
|
||||||
|
// Solana uses Ed25519 signatures
|
||||||
|
// Signature is base64-encoded, public key is the wallet address (base58)
|
||||||
|
|
||||||
|
// Decode base64 signature (Solana signatures are 64 bytes)
|
||||||
|
sig, err := base64.StdEncoding.DecodeString(req.Signature)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid base64 signature: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(sig) != 64 {
|
||||||
|
writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid signature length: expected 64 bytes, got %d", len(sig)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode base58 public key (Solana wallet address)
|
||||||
|
pubKeyBytes, err := base58Decode(req.Wallet)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid wallet address: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(pubKeyBytes) != 32 {
|
||||||
|
writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid public key length: expected 32 bytes, got %d", len(pubKeyBytes)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify Ed25519 signature
|
||||||
|
message := []byte(req.Nonce)
|
||||||
|
if !ed25519.Verify(ed25519.PublicKey(pubKeyBytes), message, sig) {
|
||||||
|
writeError(w, http.StatusUnauthorized, "signature verification failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
verified = true
|
||||||
|
|
||||||
|
default:
|
||||||
|
writeError(w, http.StatusBadRequest, fmt.Sprintf("unsupported chain type: %s (must be ETH or SOL)", chainType))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !verified {
|
||||||
|
writeError(w, http.StatusUnauthorized, fmt.Sprintf("signature verification failed: %v", verifyErr))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Mark nonce used now (after successful verification)
|
// Mark nonce used now (after successful verification)
|
||||||
if _, err := db.Query(internalCtx, "UPDATE nonces SET used_at = datetime('now') WHERE id = ?", nonceID); err != nil {
|
if _, err := db.Query(internalCtx, "UPDATE nonces SET used_at = datetime('now') WHERE id = ?", nonceID); err != nil {
|
||||||
@ -235,6 +294,45 @@ func (g *Gateway) verifyHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusInternalServerError, err.Error())
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure API key exists for this (namespace, wallet) and record ownerships
|
||||||
|
// This is done automatically after successful verification; no second nonce needed
|
||||||
|
var apiKey string
|
||||||
|
|
||||||
|
// Try existing linkage
|
||||||
|
r1, err := db.Query(internalCtx,
|
||||||
|
"SELECT api_keys.key FROM wallet_api_keys JOIN api_keys ON wallet_api_keys.api_key_id = api_keys.id WHERE wallet_api_keys.namespace_id = ? AND LOWER(wallet_api_keys.wallet) = LOWER(?) LIMIT 1",
|
||||||
|
nsID, req.Wallet,
|
||||||
|
)
|
||||||
|
if err == nil && r1 != nil && r1.Count > 0 && len(r1.Rows) > 0 && len(r1.Rows[0]) > 0 {
|
||||||
|
if s, ok := r1.Rows[0][0].(string); ok {
|
||||||
|
apiKey = s
|
||||||
|
} else {
|
||||||
|
b, _ := json.Marshal(r1.Rows[0][0])
|
||||||
|
_ = json.Unmarshal(b, &apiKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(apiKey) == "" {
|
||||||
|
// Create new API key with format ak_<random>:<namespace>
|
||||||
|
buf := make([]byte, 18)
|
||||||
|
if _, err := rand.Read(buf); err == nil {
|
||||||
|
apiKey = "ak_" + base64.RawURLEncoding.EncodeToString(buf) + ":" + ns
|
||||||
|
if _, err := db.Query(internalCtx, "INSERT INTO api_keys(key, name, namespace_id) VALUES (?, ?, ?)", apiKey, "", nsID); err == nil {
|
||||||
|
// Link wallet -> api_key
|
||||||
|
rid, err := db.Query(internalCtx, "SELECT id FROM api_keys WHERE key = ? LIMIT 1", apiKey)
|
||||||
|
if err == nil && rid != nil && rid.Count > 0 && len(rid.Rows) > 0 && len(rid.Rows[0]) > 0 {
|
||||||
|
apiKeyID := rid.Rows[0][0]
|
||||||
|
_, _ = db.Query(internalCtx, "INSERT OR IGNORE INTO wallet_api_keys(namespace_id, wallet, api_key_id) VALUES (?, ?, ?)", nsID, strings.ToLower(req.Wallet), apiKeyID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record ownerships (best-effort)
|
||||||
|
_, _ = db.Query(internalCtx, "INSERT OR IGNORE INTO namespace_ownership(namespace_id, owner_type, owner_id) VALUES (?, 'api_key', ?)", nsID, apiKey)
|
||||||
|
_, _ = db.Query(internalCtx, "INSERT OR IGNORE INTO namespace_ownership(namespace_id, owner_type, owner_id) VALUES (?, 'wallet', ?)", nsID, req.Wallet)
|
||||||
|
|
||||||
writeJSON(w, http.StatusOK, map[string]any{
|
writeJSON(w, http.StatusOK, map[string]any{
|
||||||
"access_token": token,
|
"access_token": token,
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
@ -242,6 +340,7 @@ func (g *Gateway) verifyHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
"refresh_token": refresh,
|
"refresh_token": refresh,
|
||||||
"subject": req.Wallet,
|
"subject": req.Wallet,
|
||||||
"namespace": ns,
|
"namespace": ns,
|
||||||
|
"api_key": apiKey,
|
||||||
"nonce": req.Nonce,
|
"nonce": req.Nonce,
|
||||||
"signature_verified": true,
|
"signature_verified": true,
|
||||||
})
|
})
|
||||||
@ -1025,3 +1124,41 @@ func (g *Gateway) logoutHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
writeError(w, http.StatusBadRequest, "nothing to revoke: provide refresh_token or all=true")
|
writeError(w, http.StatusBadRequest, "nothing to revoke: provide refresh_token or all=true")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// base58Decode decodes a base58-encoded string (Bitcoin alphabet)
|
||||||
|
// Used for decoding Solana public keys (base58-encoded 32-byte ed25519 public keys)
|
||||||
|
func base58Decode(encoded string) ([]byte, error) {
|
||||||
|
const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||||
|
|
||||||
|
// Build reverse lookup map
|
||||||
|
lookup := make(map[rune]int)
|
||||||
|
for i, c := range alphabet {
|
||||||
|
lookup[c] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to big integer
|
||||||
|
num := big.NewInt(0)
|
||||||
|
base := big.NewInt(58)
|
||||||
|
|
||||||
|
for _, c := range encoded {
|
||||||
|
val, ok := lookup[c]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid base58 character: %c", c)
|
||||||
|
}
|
||||||
|
num.Mul(num, base)
|
||||||
|
num.Add(num, big.NewInt(int64(val)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to bytes
|
||||||
|
decoded := num.Bytes()
|
||||||
|
|
||||||
|
// Add leading zeros for each leading '1' in the input
|
||||||
|
for _, c := range encoded {
|
||||||
|
if c != '1' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
decoded = append([]byte{0}, decoded...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoded, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/DeBrosOfficial/network/pkg/client"
|
"github.com/DeBrosOfficial/network/pkg/client"
|
||||||
@ -39,6 +40,16 @@ type Gateway struct {
|
|||||||
sqlDB *sql.DB
|
sqlDB *sql.DB
|
||||||
ormClient rqlite.Client
|
ormClient rqlite.Client
|
||||||
ormHTTP *rqlite.HTTPGateway
|
ormHTTP *rqlite.HTTPGateway
|
||||||
|
|
||||||
|
// Local pub/sub bypass for same-gateway subscribers
|
||||||
|
localSubscribers map[string][]*localSubscriber // topic+namespace -> subscribers
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// localSubscriber represents a WebSocket subscriber for local message delivery
|
||||||
|
type localSubscriber struct {
|
||||||
|
msgChan chan []byte
|
||||||
|
namespace string
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates and initializes a new Gateway instance
|
// New creates and initializes a new Gateway instance
|
||||||
@ -75,6 +86,7 @@ func New(logger *logging.ColoredLogger, cfg *Config) (*Gateway, error) {
|
|||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
client: c,
|
client: c,
|
||||||
startedAt: time.Now(),
|
startedAt: time.Now(),
|
||||||
|
localSubscribers: make(map[string][]*localSubscriber),
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.ComponentInfo(logging.ComponentGeneral, "Generating RSA signing key...")
|
logger.ComponentInfo(logging.ComponentGeneral, "Generating RSA signing key...")
|
||||||
@ -126,3 +138,12 @@ func (g *Gateway) Close() {
|
|||||||
_ = g.sqlDB.Close()
|
_ = g.sqlDB.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getLocalSubscribers returns all local subscribers for a given topic and namespace
|
||||||
|
func (g *Gateway) getLocalSubscribers(topic, namespace string) []*localSubscriber {
|
||||||
|
topicKey := namespace + "." + topic
|
||||||
|
if subs, ok := g.localSubscribers[topicKey]; ok {
|
||||||
|
return subs
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -214,6 +214,8 @@ func (g *Gateway) authorizationMiddleware(next http.Handler) http.Handler {
|
|||||||
// Identify actor from context
|
// Identify actor from context
|
||||||
ownerType := ""
|
ownerType := ""
|
||||||
ownerID := ""
|
ownerID := ""
|
||||||
|
apiKeyFallback := ""
|
||||||
|
|
||||||
if v := ctx.Value(ctxKeyJWT); v != nil {
|
if v := ctx.Value(ctxKeyJWT); v != nil {
|
||||||
if claims, ok := v.(*jwtClaims); ok && claims != nil && strings.TrimSpace(claims.Sub) != "" {
|
if claims, ok := v.(*jwtClaims); ok && claims != nil && strings.TrimSpace(claims.Sub) != "" {
|
||||||
// Determine subject type.
|
// Determine subject type.
|
||||||
@ -237,6 +239,13 @@ func (g *Gateway) authorizationMiddleware(next http.Handler) http.Handler {
|
|||||||
ownerID = strings.TrimSpace(s)
|
ownerID = strings.TrimSpace(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if ownerType == "wallet" {
|
||||||
|
// If we have a JWT wallet, also capture the API key as fallback
|
||||||
|
if v := ctx.Value(ctxKeyAPIKey); v != nil {
|
||||||
|
if s, ok := v.(string); ok && strings.TrimSpace(s) != "" {
|
||||||
|
apiKeyFallback = strings.TrimSpace(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ownerType == "" || ownerID == "" {
|
if ownerType == "" || ownerID == "" {
|
||||||
@ -244,6 +253,12 @@ func (g *Gateway) authorizationMiddleware(next http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g.logger.ComponentInfo("gateway", "namespace auth check",
|
||||||
|
zap.String("namespace", ns),
|
||||||
|
zap.String("owner_type", ownerType),
|
||||||
|
zap.String("owner_id", ownerID),
|
||||||
|
)
|
||||||
|
|
||||||
// Check ownership in DB using internal auth context
|
// Check ownership in DB using internal auth context
|
||||||
db := g.client.Database()
|
db := g.client.Database()
|
||||||
internalCtx := client.WithInternalAuth(ctx)
|
internalCtx := client.WithInternalAuth(ctx)
|
||||||
@ -261,6 +276,12 @@ func (g *Gateway) authorizationMiddleware(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
q := "SELECT 1 FROM namespace_ownership WHERE namespace_id = ? AND owner_type = ? AND owner_id = ? LIMIT 1"
|
q := "SELECT 1 FROM namespace_ownership WHERE namespace_id = ? AND owner_type = ? AND owner_id = ? LIMIT 1"
|
||||||
res, err := db.Query(internalCtx, q, nsID, ownerType, ownerID)
|
res, err := db.Query(internalCtx, q, nsID, ownerType, ownerID)
|
||||||
|
|
||||||
|
// If primary owner check fails and we have a JWT wallet with API key fallback, try the API key
|
||||||
|
if (err != nil || res == nil || res.Count == 0) && ownerType == "wallet" && apiKeyFallback != "" {
|
||||||
|
res, err = db.Query(internalCtx, q, nsID, "api_key", apiKeyFallback)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil || res == nil || res.Count == 0 {
|
if err != nil || res == nil || res.Count == 0 {
|
||||||
writeError(w, http.StatusForbidden, "forbidden: not an owner of namespace")
|
writeError(w, http.StatusForbidden, "forbidden: not an owner of namespace")
|
||||||
return
|
return
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
package gateway
|
package gateway
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/DeBrosOfficial/network/pkg/client"
|
"github.com/DeBrosOfficial/network/pkg/client"
|
||||||
|
"github.com/DeBrosOfficial/network/pkg/pubsub"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
@ -58,53 +60,103 @@ func (g *Gateway) pubsubWebsocketHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
// Channel to deliver PubSub messages to WS writer
|
// Channel to deliver PubSub messages to WS writer
|
||||||
msgs := make(chan []byte, 128)
|
msgs := make(chan []byte, 128)
|
||||||
|
|
||||||
|
// NEW: Register as local subscriber for direct message delivery
|
||||||
|
localSub := &localSubscriber{
|
||||||
|
msgChan: msgs,
|
||||||
|
namespace: ns,
|
||||||
|
}
|
||||||
|
topicKey := fmt.Sprintf("%s.%s", ns, topic)
|
||||||
|
|
||||||
|
g.mu.Lock()
|
||||||
|
g.localSubscribers[topicKey] = append(g.localSubscribers[topicKey], localSub)
|
||||||
|
subscriberCount := len(g.localSubscribers[topicKey])
|
||||||
|
g.mu.Unlock()
|
||||||
|
|
||||||
|
g.logger.ComponentInfo("gateway", "pubsub ws: registered local subscriber",
|
||||||
|
zap.String("topic", topic),
|
||||||
|
zap.String("namespace", ns),
|
||||||
|
zap.Int("total_subscribers", subscriberCount))
|
||||||
|
|
||||||
|
// Unregister on close
|
||||||
|
defer func() {
|
||||||
|
g.mu.Lock()
|
||||||
|
subs := g.localSubscribers[topicKey]
|
||||||
|
for i, sub := range subs {
|
||||||
|
if sub == localSub {
|
||||||
|
g.localSubscribers[topicKey] = append(subs[:i], subs[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remainingCount := len(g.localSubscribers[topicKey])
|
||||||
|
if remainingCount == 0 {
|
||||||
|
delete(g.localSubscribers, topicKey)
|
||||||
|
}
|
||||||
|
g.mu.Unlock()
|
||||||
|
g.logger.ComponentInfo("gateway", "pubsub ws: unregistered local subscriber",
|
||||||
|
zap.String("topic", topic),
|
||||||
|
zap.Int("remaining_subscribers", remainingCount))
|
||||||
|
}()
|
||||||
|
|
||||||
// Use internal auth context when interacting with client to avoid circular auth requirements
|
// Use internal auth context when interacting with client to avoid circular auth requirements
|
||||||
ctx := client.WithInternalAuth(r.Context())
|
ctx := client.WithInternalAuth(r.Context())
|
||||||
// Subscribe to the topic; push data into msgs with simple per-connection de-dup
|
// Apply namespace isolation
|
||||||
recent := make(map[string]time.Time)
|
ctx = pubsub.WithNamespace(ctx, ns)
|
||||||
h := func(_ string, data []byte) error {
|
|
||||||
// Drop duplicates seen in the last 2 seconds
|
|
||||||
sum := sha256.Sum256(data)
|
|
||||||
key := hex.EncodeToString(sum[:])
|
|
||||||
if t, ok := recent[key]; ok && time.Since(t) < 2*time.Second {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
recent[key] = time.Now()
|
|
||||||
select {
|
|
||||||
case msgs <- data:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
// Drop if client is slow to avoid blocking network
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := g.client.PubSub().Subscribe(ctx, topic, h); err != nil {
|
|
||||||
g.logger.ComponentWarn("gateway", "pubsub ws: subscribe failed")
|
|
||||||
writeError(w, http.StatusInternalServerError, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer func() { _ = g.client.PubSub().Unsubscribe(ctx, topic) }()
|
|
||||||
|
|
||||||
// no extra fan-out; rely on libp2p subscription
|
// Writer loop - START THIS FIRST before libp2p subscription
|
||||||
|
|
||||||
// Writer loop
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
|
g.logger.ComponentInfo("gateway", "pubsub ws: writer goroutine started",
|
||||||
|
zap.String("topic", topic))
|
||||||
|
defer g.logger.ComponentInfo("gateway", "pubsub ws: writer goroutine exiting",
|
||||||
|
zap.String("topic", topic))
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case b, ok := <-msgs:
|
case b, ok := <-msgs:
|
||||||
if !ok {
|
if !ok {
|
||||||
|
g.logger.ComponentWarn("gateway", "pubsub ws: message channel closed",
|
||||||
|
zap.String("topic", topic))
|
||||||
_ = conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(5*time.Second))
|
_ = conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(5*time.Second))
|
||||||
close(done)
|
close(done)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g.logger.ComponentInfo("gateway", "pubsub ws: sending message to client",
|
||||||
|
zap.String("topic", topic),
|
||||||
|
zap.Int("data_len", len(b)))
|
||||||
|
|
||||||
|
// Format message as JSON envelope with data (base64 encoded), timestamp, and topic
|
||||||
|
// This matches the SDK's Message interface: {data: string, timestamp: number, topic: string}
|
||||||
|
envelope := map[string]interface{}{
|
||||||
|
"data": base64.StdEncoding.EncodeToString(b),
|
||||||
|
"timestamp": time.Now().UnixMilli(),
|
||||||
|
"topic": topic,
|
||||||
|
}
|
||||||
|
envelopeJSON, err := json.Marshal(envelope)
|
||||||
|
if err != nil {
|
||||||
|
g.logger.ComponentWarn("gateway", "pubsub ws: failed to marshal envelope",
|
||||||
|
zap.String("topic", topic),
|
||||||
|
zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
g.logger.ComponentDebug("gateway", "pubsub ws: envelope created",
|
||||||
|
zap.String("topic", topic),
|
||||||
|
zap.Int("envelope_len", len(envelopeJSON)))
|
||||||
|
|
||||||
conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
|
conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
|
||||||
if err := conn.WriteMessage(websocket.BinaryMessage, b); err != nil {
|
if err := conn.WriteMessage(websocket.TextMessage, envelopeJSON); err != nil {
|
||||||
|
g.logger.ComponentWarn("gateway", "pubsub ws: failed to write to websocket",
|
||||||
|
zap.String("topic", topic),
|
||||||
|
zap.Error(err))
|
||||||
close(done)
|
close(done)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g.logger.ComponentInfo("gateway", "pubsub ws: message sent successfully",
|
||||||
|
zap.String("topic", topic))
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
// Ping keepalive
|
// Ping keepalive
|
||||||
_ = conn.WriteControl(websocket.PingMessage, []byte("ping"), time.Now().Add(5*time.Second))
|
_ = conn.WriteControl(websocket.PingMessage, []byte("ping"), time.Now().Add(5*time.Second))
|
||||||
@ -115,6 +167,42 @@ func (g *Gateway) pubsubWebsocketHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Subscribe to libp2p for cross-node messages (in background, non-blocking)
|
||||||
|
go func() {
|
||||||
|
h := func(_ string, data []byte) error {
|
||||||
|
g.logger.ComponentInfo("gateway", "pubsub ws: received message from libp2p",
|
||||||
|
zap.String("topic", topic),
|
||||||
|
zap.Int("data_len", len(data)))
|
||||||
|
|
||||||
|
select {
|
||||||
|
case msgs <- data:
|
||||||
|
g.logger.ComponentInfo("gateway", "pubsub ws: forwarded to client",
|
||||||
|
zap.String("topic", topic),
|
||||||
|
zap.String("source", "libp2p"))
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
// Drop if client is slow to avoid blocking network
|
||||||
|
g.logger.ComponentWarn("gateway", "pubsub ws: client slow, dropping message",
|
||||||
|
zap.String("topic", topic))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := g.client.PubSub().Subscribe(ctx, topic, h); err != nil {
|
||||||
|
g.logger.ComponentWarn("gateway", "pubsub ws: libp2p subscribe failed (will use local-only)",
|
||||||
|
zap.String("topic", topic),
|
||||||
|
zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
g.logger.ComponentInfo("gateway", "pubsub ws: libp2p subscription established",
|
||||||
|
zap.String("topic", topic))
|
||||||
|
|
||||||
|
// Keep subscription alive until done
|
||||||
|
<-done
|
||||||
|
_ = g.client.PubSub().Unsubscribe(ctx, topic)
|
||||||
|
g.logger.ComponentInfo("gateway", "pubsub ws: libp2p subscription closed",
|
||||||
|
zap.String("topic", topic))
|
||||||
|
}()
|
||||||
|
|
||||||
// Reader loop: treat any client message as publish to the same topic
|
// Reader loop: treat any client message as publish to the same topic
|
||||||
for {
|
for {
|
||||||
mt, data, err := conn.ReadMessage()
|
mt, data, err := conn.ReadMessage()
|
||||||
@ -124,6 +212,17 @@ func (g *Gateway) pubsubWebsocketHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
if mt != websocket.TextMessage && mt != websocket.BinaryMessage {
|
if mt != websocket.TextMessage && mt != websocket.BinaryMessage {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter out WebSocket heartbeat messages
|
||||||
|
// Don't publish them to the topic
|
||||||
|
var msg map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &msg); err == nil {
|
||||||
|
if msgType, ok := msg["type"].(string); ok && msgType == "ping" {
|
||||||
|
g.logger.ComponentInfo("gateway", "pubsub ws: filtering out heartbeat ping")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := g.client.PubSub().Publish(ctx, topic, data); err != nil {
|
if err := g.client.PubSub().Publish(ctx, topic, data); err != nil {
|
||||||
// Best-effort notify client
|
// Best-effort notify client
|
||||||
_ = conn.WriteMessage(websocket.TextMessage, []byte("publish_error"))
|
_ = conn.WriteMessage(websocket.TextMessage, []byte("publish_error"))
|
||||||
@ -160,11 +259,54 @@ func (g *Gateway) pubsubPublishHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, "invalid base64 data")
|
writeError(w, http.StatusBadRequest, "invalid base64 data")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := g.client.PubSub().Publish(client.WithInternalAuth(r.Context()), body.Topic, data); err != nil {
|
|
||||||
writeError(w, http.StatusInternalServerError, err.Error())
|
// NEW: Check for local websocket subscribers FIRST and deliver directly
|
||||||
return
|
g.mu.RLock()
|
||||||
|
localSubs := g.getLocalSubscribers(body.Topic, ns)
|
||||||
|
g.mu.RUnlock()
|
||||||
|
|
||||||
|
localDeliveryCount := 0
|
||||||
|
if len(localSubs) > 0 {
|
||||||
|
for _, sub := range localSubs {
|
||||||
|
select {
|
||||||
|
case sub.msgChan <- data:
|
||||||
|
localDeliveryCount++
|
||||||
|
g.logger.ComponentDebug("gateway", "delivered to local subscriber",
|
||||||
|
zap.String("topic", body.Topic))
|
||||||
|
default:
|
||||||
|
// Drop if buffer full
|
||||||
|
g.logger.ComponentWarn("gateway", "local subscriber buffer full, dropping message",
|
||||||
|
zap.String("topic", body.Topic))
|
||||||
}
|
}
|
||||||
// rely on libp2p to deliver to WS subscribers
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g.logger.ComponentInfo("gateway", "pubsub publish: processing message",
|
||||||
|
zap.String("topic", body.Topic),
|
||||||
|
zap.String("namespace", ns),
|
||||||
|
zap.Int("data_len", len(data)),
|
||||||
|
zap.Int("local_subscribers", len(localSubs)),
|
||||||
|
zap.Int("local_delivered", localDeliveryCount))
|
||||||
|
|
||||||
|
// Publish to libp2p asynchronously for cross-node delivery
|
||||||
|
// This prevents blocking the HTTP response if libp2p network is slow
|
||||||
|
go func() {
|
||||||
|
publishCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ctx := pubsub.WithNamespace(client.WithInternalAuth(publishCtx), ns)
|
||||||
|
if err := g.client.PubSub().Publish(ctx, body.Topic, data); err != nil {
|
||||||
|
g.logger.ComponentWarn("gateway", "async libp2p publish failed",
|
||||||
|
zap.String("topic", body.Topic),
|
||||||
|
zap.Error(err))
|
||||||
|
} else {
|
||||||
|
g.logger.ComponentDebug("gateway", "async libp2p publish succeeded",
|
||||||
|
zap.String("topic", body.Topic))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Return immediately after local delivery
|
||||||
|
// Local WebSocket subscribers already received the message
|
||||||
writeJSON(w, http.StatusOK, map[string]any{"status": "ok"})
|
writeJSON(w, http.StatusOK, map[string]any{"status": "ok"})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,7 +321,9 @@ func (g *Gateway) pubsubTopicsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusForbidden, "namespace not resolved")
|
writeError(w, http.StatusForbidden, "namespace not resolved")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
all, err := g.client.PubSub().ListTopics(client.WithInternalAuth(r.Context()))
|
// Apply namespace isolation
|
||||||
|
ctx := pubsub.WithNamespace(client.WithInternalAuth(r.Context()), ns)
|
||||||
|
all, err := g.client.PubSub().ListTopics(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, err.Error())
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
|
|||||||
@ -306,6 +306,8 @@ func (n *Node) startLibP2P() error {
|
|||||||
// Initialize pubsub
|
// Initialize pubsub
|
||||||
ps, err := libp2ppubsub.NewGossipSub(context.Background(), h,
|
ps, err := libp2ppubsub.NewGossipSub(context.Background(), h,
|
||||||
libp2ppubsub.WithPeerExchange(true),
|
libp2ppubsub.WithPeerExchange(true),
|
||||||
|
libp2ppubsub.WithFloodPublish(true), // Ensure messages reach all peers, not just mesh
|
||||||
|
libp2ppubsub.WithDirectPeers(nil), // Enable direct peer connections
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create pubsub: %w", err)
|
return fmt.Errorf("failed to create pubsub: %w", err)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package pubsub
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
||||||
@ -31,23 +32,50 @@ func (m *Manager) announceTopicInterest(topicName string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// forceTopicPeerDiscovery uses a simple strategy to announce presence on the topic.
|
// forceTopicPeerDiscovery uses a simple strategy to announce presence on the topic.
|
||||||
// It publishes a lightweight discovery ping and returns quickly.
|
// It publishes lightweight discovery pings continuously to maintain mesh health.
|
||||||
func (m *Manager) forceTopicPeerDiscovery(topicName string, topic *pubsub.Topic) {
|
func (m *Manager) forceTopicPeerDiscovery(topicName string, topic *pubsub.Topic) {
|
||||||
// If pubsub already reports peers for this topic, do nothing.
|
log.Printf("[PUBSUB] Starting continuous peer discovery for topic: %s", topicName)
|
||||||
|
|
||||||
|
// Initial aggressive discovery phase (10 attempts)
|
||||||
|
for attempt := 0; attempt < 10; attempt++ {
|
||||||
peers := topic.ListPeers()
|
peers := topic.ListPeers()
|
||||||
if len(peers) > 0 {
|
if len(peers) > 0 {
|
||||||
return
|
log.Printf("[PUBSUB] Topic %s: Found %d peers in initial discovery", topicName, len(peers))
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send a short-lived discovery ping to the topic to announce presence.
|
log.Printf("[PUBSUB] Topic %s: Initial attempt %d, sending discovery ping", topicName, attempt+1)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
discoveryMsg := []byte("PEER_DISCOVERY_PING")
|
discoveryMsg := []byte("PEER_DISCOVERY_PING")
|
||||||
_ = topic.Publish(ctx, discoveryMsg)
|
_ = topic.Publish(ctx, discoveryMsg)
|
||||||
|
cancel()
|
||||||
|
|
||||||
// Wait briefly to allow peers to respond via pubsub peer exchange
|
delay := time.Duration(100*(attempt+1)) * time.Millisecond
|
||||||
time.Sleep(300 * time.Millisecond)
|
if delay > 2*time.Second {
|
||||||
|
delay = 2 * time.Second
|
||||||
|
}
|
||||||
|
time.Sleep(delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continuous maintenance phase - keep pinging every 15 seconds
|
||||||
|
ticker := time.NewTicker(15 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for i := 0; i < 20; i++ { // Run for ~5 minutes total
|
||||||
|
<-ticker.C
|
||||||
|
peers := topic.ListPeers()
|
||||||
|
|
||||||
|
if len(peers) == 0 {
|
||||||
|
log.Printf("[PUBSUB] Topic %s: No peers, sending maintenance ping", topicName)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
|
discoveryMsg := []byte("PEER_DISCOVERY_PING")
|
||||||
|
_ = topic.Publish(ctx, discoveryMsg)
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[PUBSUB] Topic %s: Peer discovery maintenance completed", topicName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// monitorTopicPeers periodically checks topic peer connectivity and stops once peers are found.
|
// monitorTopicPeers periodically checks topic peer connectivity and stops once peers are found.
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package pubsub
|
package pubsub
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
||||||
@ -10,15 +12,18 @@ import (
|
|||||||
type Manager struct {
|
type Manager struct {
|
||||||
pubsub *pubsub.PubSub
|
pubsub *pubsub.PubSub
|
||||||
topics map[string]*pubsub.Topic
|
topics map[string]*pubsub.Topic
|
||||||
subscriptions map[string]*subscription
|
subscriptions map[string]*topicSubscription
|
||||||
namespace string
|
namespace string
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// subscription holds subscription data
|
// topicSubscription holds multiple handlers for a single topic
|
||||||
type subscription struct {
|
type topicSubscription struct {
|
||||||
sub *pubsub.Subscription
|
sub *pubsub.Subscription
|
||||||
cancel func()
|
cancel func()
|
||||||
|
handlers map[HandlerID]MessageHandler
|
||||||
|
refCount int // Number of active subscriptions
|
||||||
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManager creates a new pubsub manager
|
// NewManager creates a new pubsub manager
|
||||||
@ -26,7 +31,14 @@ func NewManager(ps *pubsub.PubSub, namespace string) *Manager {
|
|||||||
return &Manager {
|
return &Manager {
|
||||||
pubsub: ps,
|
pubsub: ps,
|
||||||
topics: make(map[string]*pubsub.Topic),
|
topics: make(map[string]*pubsub.Topic),
|
||||||
subscriptions: make(map[string]*subscription),
|
subscriptions: make(map[string]*topicSubscription),
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generateHandlerID creates a unique handler ID
|
||||||
|
func generateHandlerID() HandlerID {
|
||||||
|
b := make([]byte, 8)
|
||||||
|
rand.Read(b)
|
||||||
|
return HandlerID(hex.EncodeToString(b))
|
||||||
|
}
|
||||||
|
|||||||
@ -7,7 +7,9 @@ import (
|
|||||||
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Subscribe subscribes to a topic
|
// Subscribe subscribes to a topic with a handler.
|
||||||
|
// Returns a HandlerID that can be used to unsubscribe this specific handler.
|
||||||
|
// Multiple handlers can subscribe to the same topic.
|
||||||
func (m *Manager) Subscribe(ctx context.Context, topic string, handler MessageHandler) error {
|
func (m *Manager) Subscribe(ctx context.Context, topic string, handler MessageHandler) error {
|
||||||
if m.pubsub == nil {
|
if m.pubsub == nil {
|
||||||
return fmt.Errorf("pubsub not initialized")
|
return fmt.Errorf("pubsub not initialized")
|
||||||
@ -22,15 +24,23 @@ func (m *Manager) Subscribe(ctx context.Context, topic string, handler MessageHa
|
|||||||
}
|
}
|
||||||
namespacedTopic := fmt.Sprintf("%s.%s", ns, topic)
|
namespacedTopic := fmt.Sprintf("%s.%s", ns, topic)
|
||||||
|
|
||||||
// Check if already subscribed
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
if _, exists := m.subscriptions[namespacedTopic]; exists {
|
defer m.mu.Unlock()
|
||||||
m.mu.Unlock()
|
|
||||||
// Already subscribed - this is normal for LibP2P pubsub
|
// Check if we already have a subscription for this topic
|
||||||
|
topicSub, exists := m.subscriptions[namespacedTopic]
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
// Add handler to existing subscription
|
||||||
|
handlerID := generateHandlerID()
|
||||||
|
topicSub.mu.Lock()
|
||||||
|
topicSub.handlers[handlerID] = handler
|
||||||
|
topicSub.refCount++
|
||||||
|
topicSub.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
m.mu.Unlock()
|
|
||||||
|
|
||||||
|
// Create new subscription
|
||||||
// Get or create topic
|
// Get or create topic
|
||||||
libp2pTopic, err := m.getOrCreateTopic(namespacedTopic)
|
libp2pTopic, err := m.getOrCreateTopic(namespacedTopic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -46,15 +56,17 @@ func (m *Manager) Subscribe(ctx context.Context, topic string, handler MessageHa
|
|||||||
// Create cancellable context for this subscription
|
// Create cancellable context for this subscription
|
||||||
subCtx, cancel := context.WithCancel(context.Background())
|
subCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
// Store subscription
|
// Create topic subscription with initial handler
|
||||||
m.mu.Lock()
|
handlerID := generateHandlerID()
|
||||||
m.subscriptions[namespacedTopic] = &subscription{
|
topicSub = &topicSubscription{
|
||||||
sub: sub,
|
sub: sub,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
|
handlers: map[HandlerID]MessageHandler{handlerID: handler},
|
||||||
|
refCount: 1,
|
||||||
}
|
}
|
||||||
m.mu.Unlock()
|
m.subscriptions[namespacedTopic] = topicSub
|
||||||
|
|
||||||
// Start message handler goroutine
|
// Start message handler goroutine (fan-out to all handlers)
|
||||||
go func() {
|
go func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
sub.Cancel()
|
sub.Cancel()
|
||||||
@ -73,22 +85,31 @@ func (m *Manager) Subscribe(ctx context.Context, topic string, handler MessageHa
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the handler
|
// Broadcast to all handlers
|
||||||
if err := handler(topic, msg.Data); err != nil {
|
topicSub.mu.RLock()
|
||||||
// Log error but continue processing
|
handlers := make([]MessageHandler, 0, len(topicSub.handlers))
|
||||||
|
for _, h := range topicSub.handlers {
|
||||||
|
handlers = append(handlers, h)
|
||||||
|
}
|
||||||
|
topicSub.mu.RUnlock()
|
||||||
|
|
||||||
|
// Call each handler (don't block on individual handler errors)
|
||||||
|
for _, h := range handlers {
|
||||||
|
if err := h(topic, msg.Data); err != nil {
|
||||||
|
// Log error but continue processing other handlers
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Force peer discovery for this topic (application-agnostic)
|
|
||||||
go m.announceTopicInterest(namespacedTopic)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unsubscribe unsubscribes from a topic
|
// Unsubscribe decrements the subscription refcount for a topic.
|
||||||
|
// The subscription is only truly cancelled when refcount reaches zero.
|
||||||
|
// This allows multiple subscribers to the same topic.
|
||||||
func (m *Manager) Unsubscribe(ctx context.Context, topic string) error {
|
func (m *Manager) Unsubscribe(ctx context.Context, topic string) error {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
@ -102,9 +123,20 @@ func (m *Manager) Unsubscribe(ctx context.Context, topic string) error {
|
|||||||
}
|
}
|
||||||
namespacedTopic := fmt.Sprintf("%s.%s", ns, topic)
|
namespacedTopic := fmt.Sprintf("%s.%s", ns, topic)
|
||||||
|
|
||||||
if subscription, exists := m.subscriptions[namespacedTopic]; exists {
|
topicSub, exists := m.subscriptions[namespacedTopic]
|
||||||
// Cancel the subscription context to stop the message handler goroutine
|
if !exists {
|
||||||
subscription.cancel()
|
return nil // Already unsubscribed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrement ref count
|
||||||
|
topicSub.mu.Lock()
|
||||||
|
topicSub.refCount--
|
||||||
|
shouldCancel := topicSub.refCount <= 0
|
||||||
|
topicSub.mu.Unlock()
|
||||||
|
|
||||||
|
// Only cancel and remove if no more subscribers
|
||||||
|
if shouldCancel {
|
||||||
|
topicSub.cancel()
|
||||||
delete(m.subscriptions, namespacedTopic)
|
delete(m.subscriptions, namespacedTopic)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,7 +177,7 @@ func (m *Manager) Close() error {
|
|||||||
for _, sub := range m.subscriptions {
|
for _, sub := range m.subscriptions {
|
||||||
sub.cancel()
|
sub.cancel()
|
||||||
}
|
}
|
||||||
m.subscriptions = make(map[string]*subscription)
|
m.subscriptions = make(map[string]*topicSubscription)
|
||||||
|
|
||||||
// Close all topics
|
// Close all topics
|
||||||
for _, topic := range m.topics {
|
for _, topic := range m.topics {
|
||||||
|
|||||||
@ -1,5 +1,15 @@
|
|||||||
package pubsub
|
package pubsub
|
||||||
|
|
||||||
// MessageHandler represents a message handler function signature
|
// MessageHandler represents a message handler function signature.
|
||||||
// This matches the client.MessageHandler type to avoid circular imports
|
// Each handler is called when a message arrives on a subscribed topic.
|
||||||
|
// Multiple handlers can be registered for the same topic, and each will
|
||||||
|
// receive a copy of the message. Handlers should return an error only for
|
||||||
|
// critical failures; the error is logged but does not stop other handlers.
|
||||||
|
// This matches the client.MessageHandler type to avoid circular imports.
|
||||||
type MessageHandler func(topic string, data []byte) error
|
type MessageHandler func(topic string, data []byte) error
|
||||||
|
|
||||||
|
// HandlerID uniquely identifies a handler registration.
|
||||||
|
// Each call to Subscribe generates a new HandlerID, allowing
|
||||||
|
// multiple subscribers to the same topic with independent lifecycles.
|
||||||
|
// Unsubscribe operations are ref-counted per topic.
|
||||||
|
type HandlerID string
|
||||||
@ -137,8 +137,8 @@ func (r *RQLiteManager) Start(ctx context.Context) error {
|
|||||||
r.cmd = exec.Command("rqlited", args...)
|
r.cmd = exec.Command("rqlited", args...)
|
||||||
|
|
||||||
// Enable debug logging of RQLite process to help diagnose issues
|
// Enable debug logging of RQLite process to help diagnose issues
|
||||||
// r.cmd.Stdout = os.Stdout
|
r.cmd.Stdout = os.Stdout
|
||||||
// r.cmd.Stderr = os.Stderr
|
r.cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
if err := r.cmd.Start(); err != nil {
|
if err := r.cmd.Start(); err != nil {
|
||||||
return fmt.Errorf("failed to start RQLite: %w", err)
|
return fmt.Errorf("failed to start RQLite: %w", err)
|
||||||
|
|||||||
@ -1,8 +1,14 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# DeBros Network Production Installation Script
|
# DeBros Network Installation Script
|
||||||
# Installs and configures a complete DeBros network node (bootstrap) with gateway.
|
# Downloads network-cli from GitHub releases and runs interactive setup
|
||||||
# Supports idempotent updates and secure systemd service management.
|
#
|
||||||
|
# Supported: Ubuntu 18.04+, Debian 10+
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# curl -fsSL https://install.debros.network | bash
|
||||||
|
# OR
|
||||||
|
# bash scripts/install-debros-network.sh
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
trap 'echo -e "${RED}An error occurred. Installation aborted.${NOCOLOR}"; exit 1' ERR
|
trap 'echo -e "${RED}An error occurred. Installation aborted.${NOCOLOR}"; exit 1' ERR
|
||||||
@ -15,481 +21,40 @@ BLUE='\033[38;2;2;128;175m'
|
|||||||
YELLOW='\033[1;33m'
|
YELLOW='\033[1;33m'
|
||||||
NOCOLOR='\033[0m'
|
NOCOLOR='\033[0m'
|
||||||
|
|
||||||
# Defaults
|
# Configuration
|
||||||
INSTALL_DIR="/opt/debros"
|
GITHUB_REPO="DeBrosOfficial/network"
|
||||||
REPO_URL="https://github.com/DeBrosOfficial/network.git"
|
GITHUB_API="https://api.github.com/repos/$GITHUB_REPO"
|
||||||
MIN_GO_VERSION="1.21"
|
INSTALL_DIR="/usr/local/bin"
|
||||||
NODE_PORT="4001"
|
|
||||||
RQLITE_PORT="5001"
|
|
||||||
GATEWAY_PORT="6001"
|
|
||||||
RAFT_PORT="7001"
|
|
||||||
UPDATE_MODE=false
|
|
||||||
NON_INTERACTIVE=false
|
|
||||||
DEBROS_USER="debros"
|
|
||||||
|
|
||||||
log() { echo -e "${CYAN}[$(date '+%Y-%m-%d %H:%M:%S')]${NOCOLOR} $1"; }
|
log() { echo -e "${CYAN}[$(date '+%Y-%m-%d %H:%M:%S')]${NOCOLOR} $1"; }
|
||||||
error() { echo -e "${RED}[ERROR]${NOCOLOR} $1"; }
|
error() { echo -e "${RED}[ERROR]${NOCOLOR} $1"; }
|
||||||
success() { echo -e "${GREEN}[SUCCESS]${NOCOLOR} $1"; }
|
success() { echo -e "${GREEN}[SUCCESS]${NOCOLOR} $1"; }
|
||||||
warning() { echo -e "${YELLOW}[WARNING]${NOCOLOR} $1"; }
|
warning() { echo -e "${YELLOW}[WARNING]${NOCOLOR} $1"; }
|
||||||
|
|
||||||
# Detect non-interactive mode
|
# REQUIRE INTERACTIVE MODE
|
||||||
if [ ! -t 0 ]; then
|
if [ ! -t 0 ]; then
|
||||||
NON_INTERACTIVE=true
|
error "This script requires an interactive terminal."
|
||||||
log "Running in non-interactive mode"
|
echo -e ""
|
||||||
|
echo -e "${YELLOW}Please run this script directly:${NOCOLOR}"
|
||||||
|
echo -e "${CYAN} bash <(curl -fsSL https://install.debros.network)${NOCOLOR}"
|
||||||
|
echo -e ""
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Root/sudo checks
|
# Check if running as root
|
||||||
if [[ $EUID -eq 0 ]]; then
|
if [[ $EUID -eq 0 ]]; then
|
||||||
warning "Running as root is not recommended for security reasons."
|
error "This script should NOT be run as root"
|
||||||
if [ "$NON_INTERACTIVE" != true ]; then
|
echo -e "${YELLOW}Run as a regular user with sudo privileges:${NOCOLOR}"
|
||||||
echo -n "Are you sure you want to continue? (yes/no): "
|
echo -e "${CYAN} bash $0${NOCOLOR}"
|
||||||
read ROOT_CONFIRM
|
|
||||||
if [[ "$ROOT_CONFIRM" != "yes" ]]; then
|
|
||||||
error "Installation cancelled for security reasons."
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
log "Non-interactive mode: proceeding with root (use at your own risk)"
|
# Check for sudo
|
||||||
fi
|
if ! command -v sudo &>/dev/null; then
|
||||||
alias sudo=''
|
|
||||||
else
|
|
||||||
if ! command -v sudo &>/dev/null; then
|
|
||||||
error "sudo command not found. Please ensure you have sudo privileges."
|
error "sudo command not found. Please ensure you have sudo privileges."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Detect OS and package manager
|
|
||||||
detect_os() {
|
|
||||||
if [ -f /etc/os-release ]; then
|
|
||||||
. /etc/os-release
|
|
||||||
OS=$ID
|
|
||||||
VERSION=$VERSION_ID
|
|
||||||
else
|
|
||||||
error "Cannot detect operating system"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
case $OS in
|
|
||||||
ubuntu|debian) PACKAGE_MANAGER="apt" ;;
|
|
||||||
centos|rhel|fedora)
|
|
||||||
PACKAGE_MANAGER="yum"
|
|
||||||
if command -v dnf &> /dev/null; then PACKAGE_MANAGER="dnf"; fi
|
|
||||||
;;
|
|
||||||
*) error "Unsupported operating system: $OS"; exit 1 ;;
|
|
||||||
esac
|
|
||||||
log "Detected OS: $OS $VERSION"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check for existing install
|
|
||||||
check_existing_installation() {
|
|
||||||
if [ -d "$INSTALL_DIR" ] && [ -f "$INSTALL_DIR/bin/node" ]; then
|
|
||||||
log "Found existing DeBros Network installation at $INSTALL_DIR"
|
|
||||||
NODE_RUNNING=false
|
|
||||||
if systemctl is-active --quiet debros-node.service 2>/dev/null; then
|
|
||||||
NODE_RUNNING=true
|
|
||||||
log "Node service is currently running"
|
|
||||||
fi
|
|
||||||
if [ "$NON_INTERACTIVE" = true ]; then
|
|
||||||
log "Non-interactive mode: updating existing installation"
|
|
||||||
UPDATE_MODE=true
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
echo -e "${YELLOW}Existing installation detected!${NOCOLOR}"
|
|
||||||
echo -e "${CYAN}Options:${NOCOLOR}"
|
|
||||||
echo -e "${CYAN}1) Update existing installation${NOCOLOR}"
|
|
||||||
echo -e "${CYAN}2) Remove and reinstall${NOCOLOR}"
|
|
||||||
echo -e "${CYAN}3) Exit installer${NOCOLOR}"
|
|
||||||
while true; do
|
|
||||||
read -rp "Enter your choice (1, 2, or 3): " EXISTING_CHOICE
|
|
||||||
case $EXISTING_CHOICE in
|
|
||||||
1) UPDATE_MODE=true; log "Will update existing installation"; return 0 ;;
|
|
||||||
2) log "Will remove and reinstall"; remove_existing_installation; UPDATE_MODE=false; return 0 ;;
|
|
||||||
3) log "Installation cancelled by user"; exit 0 ;;
|
|
||||||
*) error "Invalid choice. Please enter 1, 2, or 3." ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
else
|
|
||||||
UPDATE_MODE=false
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
remove_existing_installation() {
|
|
||||||
log "Removing existing installation..."
|
|
||||||
for service in debros-node debros-gateway; do
|
|
||||||
if systemctl list-unit-files | grep -q "$service.service"; then
|
|
||||||
log "Stopping $service service..."
|
|
||||||
sudo systemctl stop $service.service 2>/dev/null || true
|
|
||||||
sudo systemctl disable $service.service 2>/dev/null || true
|
|
||||||
sudo rm -f /etc/systemd/system/$service.service
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
if [ -d "$INSTALL_DIR" ]; then
|
|
||||||
sudo rm -rf "$INSTALL_DIR"
|
|
||||||
log "Removed installation directory"
|
|
||||||
fi
|
|
||||||
if id "$DEBROS_USER" &>/dev/null; then
|
|
||||||
sudo userdel "$DEBROS_USER" 2>/dev/null || true
|
|
||||||
log "Removed debros user"
|
|
||||||
fi
|
|
||||||
success "Existing installation removed"
|
|
||||||
}
|
|
||||||
|
|
||||||
check_go_installation() {
|
|
||||||
if command -v go &> /dev/null; then
|
|
||||||
GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//')
|
|
||||||
log "Found Go version: $GO_VERSION"
|
|
||||||
if [ "$(printf '%s\n' "$MIN_GO_VERSION" "$GO_VERSION" | sort -V | head -n1)" = "$MIN_GO_VERSION" ]; then
|
|
||||||
success "Go version is sufficient"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
warning "Go version $GO_VERSION is too old. Minimum required: $MIN_GO_VERSION"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log "Go not found on system"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
install_go() {
|
|
||||||
log "Installing Go..."
|
|
||||||
case $PACKAGE_MANAGER in
|
|
||||||
apt) sudo apt update; sudo apt install -y wget ;;
|
|
||||||
yum|dnf) sudo $PACKAGE_MANAGER install -y wget ;;
|
|
||||||
esac
|
|
||||||
GO_TARBALL="go1.21.6.linux-amd64.tar.gz"
|
|
||||||
ARCH=$(uname -m)
|
|
||||||
if [ "$ARCH" = "aarch64" ]; then GO_TARBALL="go1.21.6.linux-arm64.tar.gz"; fi
|
|
||||||
cd /tmp
|
|
||||||
wget -q "https://go.dev/dl/$GO_TARBALL"
|
|
||||||
sudo rm -rf /usr/local/go
|
|
||||||
sudo tar -C /usr/local -xzf "$GO_TARBALL"
|
|
||||||
if ! grep -q "/usr/local/go/bin" /etc/environment 2>/dev/null; then
|
|
||||||
echo 'PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/local/go/bin"' | sudo tee /etc/environment > /dev/null
|
|
||||||
fi
|
|
||||||
if ! grep -q "/usr/local/go/bin" ~/.bashrc; then
|
|
||||||
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
|
|
||||||
fi
|
|
||||||
export PATH=$PATH:/usr/local/go/bin
|
|
||||||
success "Go installed successfully"
|
|
||||||
}
|
|
||||||
|
|
||||||
install_dependencies() {
|
|
||||||
log "Checking system dependencies..."
|
|
||||||
MISSING_DEPS=()
|
|
||||||
case $PACKAGE_MANAGER in
|
|
||||||
apt)
|
|
||||||
for pkg in git make build-essential curl; do
|
|
||||||
if ! dpkg -l | grep -q "^ii $pkg "; then MISSING_DEPS+=($pkg); fi
|
|
||||||
done
|
|
||||||
if [ ${#MISSING_DEPS[@]} -gt 0 ]; then
|
|
||||||
log "Installing missing dependencies: ${MISSING_DEPS[*]}"
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y "${MISSING_DEPS[@]}"
|
|
||||||
else
|
|
||||||
success "All system dependencies already installed"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
yum|dnf)
|
|
||||||
for pkg in git make curl; do
|
|
||||||
if ! rpm -q $pkg &>/dev/null; then MISSING_DEPS+=($pkg); fi
|
|
||||||
done
|
|
||||||
if ! rpm -q gcc &>/dev/null; then MISSING_DEPS+=("Development Tools"); fi
|
|
||||||
if [ ${#MISSING_DEPS[@]} -gt 0 ]; then
|
|
||||||
log "Installing missing dependencies: ${MISSING_DEPS[*]}"
|
|
||||||
if [[ " ${MISSING_DEPS[*]} " =~ " Development Tools " ]]; then
|
|
||||||
sudo $PACKAGE_MANAGER groupinstall -y "Development Tools"
|
|
||||||
fi
|
|
||||||
MISSING_DEPS=($(printf '%s\n' "${MISSING_DEPS[@]}" | grep -v "Development Tools"))
|
|
||||||
if [ ${#MISSING_DEPS[@]} -gt 0 ]; then
|
|
||||||
sudo $PACKAGE_MANAGER install -y "${MISSING_DEPS[@]}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
success "All system dependencies already installed"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
success "System dependencies ready"
|
|
||||||
}
|
|
||||||
|
|
||||||
install_rqlite() {
|
|
||||||
if command -v rqlited &> /dev/null; then
|
|
||||||
RQLITE_VERSION=$(rqlited -version | head -n1 | awk '{print $2}')
|
|
||||||
log "Found RQLite version: $RQLITE_VERSION"
|
|
||||||
success "RQLite already installed"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
log "Installing RQLite..."
|
|
||||||
ARCH=$(uname -m)
|
|
||||||
case $ARCH in
|
|
||||||
x86_64) RQLITE_ARCH="amd64" ;;
|
|
||||||
aarch64|arm64) RQLITE_ARCH="arm64" ;;
|
|
||||||
armv7l) RQLITE_ARCH="arm" ;;
|
|
||||||
*) error "Unsupported architecture: $ARCH"; exit 1 ;;
|
|
||||||
esac
|
|
||||||
RQLITE_VERSION="8.43.0"
|
|
||||||
RQLITE_TARBALL="rqlite-v${RQLITE_VERSION}-linux-${RQLITE_ARCH}.tar.gz"
|
|
||||||
RQLITE_URL="https://github.com/rqlite/rqlite/releases/download/v${RQLITE_VERSION}/${RQLITE_TARBALL}"
|
|
||||||
cd /tmp
|
|
||||||
if ! wget -q "$RQLITE_URL"; then error "Failed to download RQLite from $RQLITE_URL"; exit 1; fi
|
|
||||||
tar -xzf "$RQLITE_TARBALL"
|
|
||||||
RQLITE_DIR="rqlite-v${RQLITE_VERSION}-linux-${RQLITE_ARCH}"
|
|
||||||
sudo cp "$RQLITE_DIR/rqlited" /usr/local/bin/
|
|
||||||
sudo cp "$RQLITE_DIR/rqlite" /usr/local/bin/
|
|
||||||
sudo chmod +x /usr/local/bin/rqlited
|
|
||||||
sudo chmod +x /usr/local/bin/rqlite
|
|
||||||
rm -rf "$RQLITE_TARBALL" "$RQLITE_DIR"
|
|
||||||
if command -v rqlited &> /dev/null; then
|
|
||||||
INSTALLED_VERSION=$(rqlited -version | head -n1 | awk '{print $2}')
|
|
||||||
success "RQLite v$INSTALLED_VERSION installed successfully"
|
|
||||||
else
|
|
||||||
error "RQLite installation failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
check_ports() {
|
|
||||||
local ports=($NODE_PORT $RQLITE_PORT $RAFT_PORT $GATEWAY_PORT)
|
|
||||||
for port in "${ports[@]}"; do
|
|
||||||
if sudo netstat -tuln 2>/dev/null | grep -q ":$port " || ss -tuln 2>/dev/null | grep -q ":$port "; then
|
|
||||||
error "Port $port is already in use. Please free it up and try again."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
success "All required ports are available"
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_directories() {
|
|
||||||
log "Setting up directories and permissions..."
|
|
||||||
if ! id "$DEBROS_USER" &>/dev/null; then
|
|
||||||
sudo useradd -r -s /usr/sbin/nologin -d "$INSTALL_DIR" "$DEBROS_USER"
|
|
||||||
log "Created debros user"
|
|
||||||
else
|
|
||||||
log "User 'debros' already exists"
|
|
||||||
fi
|
|
||||||
sudo mkdir -p "$INSTALL_DIR"/{bin,src}
|
|
||||||
sudo chown -R "$DEBROS_USER:$DEBROS_USER" "$INSTALL_DIR"
|
|
||||||
sudo chmod 755 "$INSTALL_DIR"
|
|
||||||
sudo chmod 755 "$INSTALL_DIR/bin"
|
|
||||||
|
|
||||||
# Create ~/.debros for the debros user
|
|
||||||
DEBROS_HOME=$(sudo -u "$DEBROS_USER" sh -c 'echo ~')
|
|
||||||
sudo -u "$DEBROS_USER" mkdir -p "$DEBROS_HOME/.debros"
|
|
||||||
sudo chmod 0700 "$DEBROS_HOME/.debros"
|
|
||||||
|
|
||||||
success "Directory structure ready"
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_source_code() {
|
|
||||||
log "Setting up source code..."
|
|
||||||
if [ -d "$INSTALL_DIR/src/.git" ]; then
|
|
||||||
log "Updating existing repository..."
|
|
||||||
cd "$INSTALL_DIR/src"
|
|
||||||
sudo -u "$DEBROS_USER" git pull
|
|
||||||
else
|
|
||||||
log "Cloning repository..."
|
|
||||||
sudo -u "$DEBROS_USER" git clone "$REPO_URL" "$INSTALL_DIR/src"
|
|
||||||
cd "$INSTALL_DIR/src"
|
|
||||||
fi
|
|
||||||
success "Source code ready"
|
|
||||||
}
|
|
||||||
|
|
||||||
build_binaries() {
|
|
||||||
log "Building DeBros Network binaries..."
|
|
||||||
cd "$INSTALL_DIR/src"
|
|
||||||
export PATH=$PATH:/usr/local/go/bin
|
|
||||||
|
|
||||||
local services_were_running=()
|
|
||||||
if [ "$UPDATE_MODE" = true ]; then
|
|
||||||
log "Update mode: checking for running services before binary update..."
|
|
||||||
if systemctl is-active --quiet debros-node.service 2>/dev/null; then
|
|
||||||
log "Stopping debros-node service to update binaries..."
|
|
||||||
sudo systemctl stop debros-node.service
|
|
||||||
services_were_running+=("debros-node")
|
|
||||||
fi
|
|
||||||
if systemctl is-active --quiet debros-gateway.service 2>/dev/null; then
|
|
||||||
log "Stopping debros-gateway service to update binaries..."
|
|
||||||
sudo systemctl stop debros-gateway.service
|
|
||||||
services_were_running+=("debros-gateway")
|
|
||||||
fi
|
|
||||||
if [ ${#services_were_running[@]} -gt 0 ]; then
|
|
||||||
log "Waiting for services to stop completely..."
|
|
||||||
sleep 3
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
sudo -u "$DEBROS_USER" env "PATH=$PATH:/usr/local/go/bin" make build
|
|
||||||
sudo cp bin/* "$INSTALL_DIR/bin/"
|
|
||||||
sudo chown "$DEBROS_USER:$DEBROS_USER" "$INSTALL_DIR/bin/"*
|
|
||||||
sudo chmod 755 "$INSTALL_DIR/bin/"*
|
|
||||||
|
|
||||||
if [ "$UPDATE_MODE" = true ] && [ ${#services_were_running[@]} -gt 0 ]; then
|
|
||||||
log "Restarting previously running services..."
|
|
||||||
for service in "${services_were_running[@]}"; do
|
|
||||||
log "Starting $service service..."
|
|
||||||
sudo systemctl start $service.service
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
success "Binaries built and installed"
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_configs() {
|
|
||||||
log "Generating configuration files via network-cli..."
|
|
||||||
DEBROS_HOME=$(sudo -u "$DEBROS_USER" sh -c 'echo ~')
|
|
||||||
|
|
||||||
# Generate bootstrap config
|
|
||||||
log "Generating bootstrap.yaml..."
|
|
||||||
sudo -u "$DEBROS_USER" "$INSTALL_DIR/bin/network-cli" config init --type bootstrap --force
|
|
||||||
|
|
||||||
# Generate gateway config
|
|
||||||
log "Generating gateway.yaml..."
|
|
||||||
sudo -u "$DEBROS_USER" "$INSTALL_DIR/bin/network-cli" config init --type gateway --force
|
|
||||||
|
|
||||||
success "Configuration files generated"
|
|
||||||
}
|
|
||||||
|
|
||||||
configure_firewall() {
|
|
||||||
log "Configuring firewall rules..."
|
|
||||||
if command -v ufw &> /dev/null; then
|
|
||||||
log "Adding UFW rules for DeBros Network ports..."
|
|
||||||
for port in $NODE_PORT $RQLITE_PORT $RAFT_PORT $GATEWAY_PORT; do
|
|
||||||
if ! sudo ufw allow $port 2>/dev/null; then
|
|
||||||
error "Failed to allow port $port"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
log "Added UFW rule: allow port $port"
|
|
||||||
done
|
|
||||||
UFW_STATUS=$(sudo ufw status | grep -o "Status: [a-z]\+" | awk '{print $2}' || echo "inactive")
|
|
||||||
if [[ "$UFW_STATUS" == "active" ]]; then
|
|
||||||
success "Firewall rules added and active"
|
|
||||||
else
|
|
||||||
success "Firewall rules added (UFW is inactive - rules will take effect when UFW is enabled)"
|
|
||||||
log "To enable UFW with current rules: sudo ufw enable"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
warning "UFW not found. Please configure firewall manually."
|
|
||||||
log "Required ports to allow:"
|
|
||||||
log " - Port $NODE_PORT (Node P2P)"
|
|
||||||
log " - Port $RQLITE_PORT (RQLite HTTP)"
|
|
||||||
log " - Port $RAFT_PORT (RQLite Raft)"
|
|
||||||
log " - Port $GATEWAY_PORT (Gateway)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
create_systemd_services() {
|
|
||||||
log "Creating systemd service units..."
|
|
||||||
|
|
||||||
# Node service
|
|
||||||
local node_service_file="/etc/systemd/system/debros-node.service"
|
|
||||||
if [ -f "$node_service_file" ]; then
|
|
||||||
log "Cleaning up existing node service..."
|
|
||||||
sudo systemctl stop debros-node.service 2>/dev/null || true
|
|
||||||
sudo systemctl disable debros-node.service 2>/dev/null || true
|
|
||||||
sudo rm -f "$node_service_file"
|
|
||||||
fi
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
log "Creating new systemd service..."
|
|
||||||
local exec_start="$INSTALL_DIR/bin/node --config $INSTALL_DIR/configs/node.yaml"
|
|
||||||
cat > /tmp/debros-node.service << EOF
|
|
||||||
[Unit]
|
|
||||||
Description=DeBros Network Node (Bootstrap)
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=debros
|
|
||||||
Group=debros
|
|
||||||
WorkingDirectory=/opt/debros/src
|
|
||||||
ExecStart=/opt/debros/bin/node --config bootstrap.yaml
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
StandardOutput=journal
|
|
||||||
StandardError=journal
|
|
||||||
SyslogIdentifier=debros-node
|
|
||||||
|
|
||||||
NoNewPrivileges=yes
|
|
||||||
PrivateTmp=yes
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=yes
|
|
||||||
ReadWritePaths=/opt/debros
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
sudo mv /tmp/debros-node.service "$node_service_file"
|
|
||||||
|
|
||||||
# Gateway service
|
|
||||||
local gateway_service_file="/etc/systemd/system/debros-gateway.service"
|
|
||||||
if [ -f "$gateway_service_file" ]; then
|
|
||||||
log "Cleaning up existing gateway service..."
|
|
||||||
sudo systemctl stop debros-gateway.service 2>/dev/null || true
|
|
||||||
sudo systemctl disable debros-gateway.service 2>/dev/null || true
|
|
||||||
sudo rm -f "$gateway_service_file"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Creating debros-gateway.service..."
|
|
||||||
cat > /tmp/debros-gateway.service << 'EOF'
|
|
||||||
[Unit]
|
|
||||||
Description=DeBros Gateway (HTTP/WebSocket)
|
|
||||||
After=debros-node.service
|
|
||||||
Wants=debros-node.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=debros
|
|
||||||
Group=debros
|
|
||||||
WorkingDirectory=/opt/debros/src
|
|
||||||
ExecStart=/opt/debros/bin/gateway
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
StandardOutput=journal
|
|
||||||
StandardError=journal
|
|
||||||
SyslogIdentifier=debros-gateway
|
|
||||||
|
|
||||||
NoNewPrivileges=yes
|
|
||||||
PrivateTmp=yes
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=yes
|
|
||||||
ReadWritePaths=/opt/debros
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
sudo mv /tmp/debros-gateway.service "$gateway_service_file"
|
|
||||||
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable debros-node.service
|
|
||||||
sudo systemctl enable debros-gateway.service
|
|
||||||
success "Systemd services ready"
|
|
||||||
}
|
|
||||||
|
|
||||||
start_services() {
|
|
||||||
log "Starting DeBros Network services..."
|
|
||||||
sudo systemctl start debros-node.service
|
|
||||||
sleep 3
|
|
||||||
if systemctl is-active --quiet debros-node.service; then
|
|
||||||
success "DeBros Node service started successfully"
|
|
||||||
else
|
|
||||||
error "Failed to start DeBros Node service"
|
|
||||||
log "Check logs with: sudo journalctl -u debros-node.service -f"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
sleep 2
|
|
||||||
sudo systemctl start debros-gateway.service
|
|
||||||
sleep 2
|
|
||||||
if systemctl is-active --quiet debros-gateway.service; then
|
|
||||||
success "DeBros Gateway service started successfully"
|
|
||||||
else
|
|
||||||
error "Failed to start DeBros Gateway service"
|
|
||||||
log "Check logs with: sudo journalctl -u debros-gateway.service -f"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
display_banner() {
|
display_banner() {
|
||||||
echo -e "${BLUE}========================================================================${NOCOLOR}"
|
echo -e "${BLUE}========================================================================${NOCOLOR}"
|
||||||
echo -e "${CYAN}
|
echo -e "${CYAN}
|
||||||
@ -500,69 +65,216 @@ display_banner() {
|
|||||||
|____/ \\___|____/|_| \\___/|___/ |_| \\_|\\___|\\__| \\_/\\_/ \\___/|_| |_|\\_\\
|
|____/ \\___|____/|_| \\___/|___/ |_| \\_|\\___|\\__| \\_/\\_/ \\___/|_| |_|\\_\\
|
||||||
${NOCOLOR}"
|
${NOCOLOR}"
|
||||||
echo -e "${BLUE}========================================================================${NOCOLOR}"
|
echo -e "${BLUE}========================================================================${NOCOLOR}"
|
||||||
|
echo -e "${GREEN} Quick Install Script ${NOCOLOR}"
|
||||||
|
echo -e "${BLUE}========================================================================${NOCOLOR}"
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_os() {
|
||||||
|
if [ ! -f /etc/os-release ]; then
|
||||||
|
error "Cannot detect operating system"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
. /etc/os-release
|
||||||
|
OS=$ID
|
||||||
|
VERSION=$VERSION_ID
|
||||||
|
|
||||||
|
# Only support Debian and Ubuntu
|
||||||
|
case $OS in
|
||||||
|
ubuntu|debian)
|
||||||
|
log "Detected OS: $OS ${VERSION:-unknown}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
error "Unsupported operating system: $OS"
|
||||||
|
echo -e "${YELLOW}This script only supports Ubuntu 18.04+ and Debian 10+${NOCOLOR}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
check_architecture() {
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
case $ARCH in
|
||||||
|
x86_64)
|
||||||
|
GITHUB_ARCH="amd64"
|
||||||
|
;;
|
||||||
|
aarch64|arm64)
|
||||||
|
GITHUB_ARCH="arm64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
error "Unsupported architecture: $ARCH"
|
||||||
|
echo -e "${YELLOW}Supported: x86_64, aarch64/arm64${NOCOLOR}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
log "Architecture: $ARCH (using $GITHUB_ARCH)"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_dependencies() {
|
||||||
|
log "Checking required tools..."
|
||||||
|
|
||||||
|
local missing_deps=()
|
||||||
|
|
||||||
|
for cmd in curl tar; do
|
||||||
|
if ! command -v $cmd &>/dev/null; then
|
||||||
|
missing_deps+=("$cmd")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#missing_deps[@]} -gt 0 ]; then
|
||||||
|
log "Installing missing dependencies: ${missing_deps[*]}"
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y "${missing_deps[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
success "All required tools available"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_latest_release() {
|
||||||
|
log "Fetching latest release information..."
|
||||||
|
|
||||||
|
# Get latest release (exclude pre-releases and nightly)
|
||||||
|
LATEST_RELEASE=$(curl -fsSL "$GITHUB_API/releases" | \
|
||||||
|
grep -v "prerelease.*true" | \
|
||||||
|
grep -v "draft.*true" | \
|
||||||
|
grep '"tag_name"' | \
|
||||||
|
head -1 | \
|
||||||
|
cut -d'"' -f4)
|
||||||
|
|
||||||
|
if [ -z "$LATEST_RELEASE" ]; then
|
||||||
|
error "Could not determine latest release"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Latest release: $LATEST_RELEASE"
|
||||||
|
}
|
||||||
|
|
||||||
|
download_and_install() {
|
||||||
|
log "Downloading network-cli..."
|
||||||
|
|
||||||
|
# Construct download URL
|
||||||
|
DOWNLOAD_URL="https://github.com/$GITHUB_REPO/releases/download/$LATEST_RELEASE/debros-network_${LATEST_RELEASE#v}_linux_${GITHUB_ARCH}.tar.gz"
|
||||||
|
|
||||||
|
# Create temporary directory
|
||||||
|
TEMP_DIR=$(mktemp -d)
|
||||||
|
trap "rm -rf $TEMP_DIR" EXIT
|
||||||
|
|
||||||
|
# Download
|
||||||
|
log "Downloading from: $DOWNLOAD_URL"
|
||||||
|
if ! curl -fsSL -o "$TEMP_DIR/network-cli.tar.gz" "$DOWNLOAD_URL"; then
|
||||||
|
error "Failed to download network-cli"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract
|
||||||
|
log "Extracting network-cli..."
|
||||||
|
cd "$TEMP_DIR"
|
||||||
|
tar xzf network-cli.tar.gz
|
||||||
|
|
||||||
|
# Install
|
||||||
|
log "Installing to $INSTALL_DIR..."
|
||||||
|
sudo cp network-cli "$INSTALL_DIR/"
|
||||||
|
sudo chmod +x "$INSTALL_DIR/network-cli"
|
||||||
|
|
||||||
|
success "network-cli installed successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_installation() {
|
||||||
|
if command -v network-cli &>/dev/null; then
|
||||||
|
INSTALLED_VERSION=$(network-cli version 2>/dev/null || echo "unknown")
|
||||||
|
success "network-cli is ready: $INSTALLED_VERSION"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
error "network-cli not found in PATH"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_setup() {
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${BLUE}========================================${NOCOLOR}"
|
||||||
|
echo -e "${GREEN}Step 2: Run Interactive Setup${NOCOLOR}"
|
||||||
|
echo -e "${BLUE}========================================${NOCOLOR}"
|
||||||
|
echo -e ""
|
||||||
|
|
||||||
|
log "The setup command will:"
|
||||||
|
log " • Create system user and directories"
|
||||||
|
log " • Install dependencies (RQLite, etc.)"
|
||||||
|
log " • Build DeBros binaries"
|
||||||
|
log " • Configure network settings"
|
||||||
|
log " • Create and start systemd services"
|
||||||
|
echo -e ""
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Ready to run setup? This will prompt for configuration details.${NOCOLOR}"
|
||||||
|
echo -n "Continue? (yes/no): "
|
||||||
|
read -r CONTINUE_SETUP
|
||||||
|
|
||||||
|
if [[ "$CONTINUE_SETUP" != "yes" && "$CONTINUE_SETUP" != "y" ]]; then
|
||||||
|
echo -e ""
|
||||||
|
success "network-cli installed successfully!"
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${CYAN}To complete setup later, run:${NOCOLOR}"
|
||||||
|
echo -e "${GREEN} sudo network-cli setup${NOCOLOR}"
|
||||||
|
echo -e ""
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e ""
|
||||||
|
log "Running setup (requires sudo)..."
|
||||||
|
sudo network-cli setup
|
||||||
|
}
|
||||||
|
|
||||||
|
show_completion() {
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${BLUE}========================================================================${NOCOLOR}"
|
||||||
|
success "DeBros Network installation complete!"
|
||||||
|
echo -e "${BLUE}========================================================================${NOCOLOR}"
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${GREEN}Next Steps:${NOCOLOR}"
|
||||||
|
echo -e " • Verify installation: ${CYAN}curl http://localhost:6001/health${NOCOLOR}"
|
||||||
|
echo -e " • Check services: ${CYAN}sudo network-cli service status all${NOCOLOR}"
|
||||||
|
echo -e " • View logs: ${CYAN}sudo network-cli service logs node --follow${NOCOLOR}"
|
||||||
|
echo -e " • Authenticate: ${CYAN}network-cli auth login${NOCOLOR}"
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${CYAN}Environment Management:${NOCOLOR}"
|
||||||
|
echo -e " • Switch to devnet: ${CYAN}network-cli devnet enable${NOCOLOR}"
|
||||||
|
echo -e " • Switch to testnet: ${CYAN}network-cli testnet enable${NOCOLOR}"
|
||||||
|
echo -e " • Show environment: ${CYAN}network-cli env current${NOCOLOR}"
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${CYAN}Documentation: https://docs.debros.io${NOCOLOR}"
|
||||||
|
echo -e ""
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
display_banner
|
display_banner
|
||||||
log "${BLUE}==================================================${NOCOLOR}"
|
|
||||||
log "${GREEN} Starting DeBros Network Installation ${NOCOLOR}"
|
echo -e ""
|
||||||
log "${BLUE}==================================================${NOCOLOR}"
|
log "Starting DeBros Network installation..."
|
||||||
|
echo -e ""
|
||||||
|
|
||||||
detect_os
|
detect_os
|
||||||
check_existing_installation
|
check_architecture
|
||||||
if [ "$UPDATE_MODE" != true ]; then check_ports; else log "Update mode: skipping port availability check"; fi
|
check_dependencies
|
||||||
if ! check_go_installation; then install_go; fi
|
|
||||||
install_dependencies
|
|
||||||
install_rqlite
|
|
||||||
setup_directories
|
|
||||||
setup_source_code
|
|
||||||
build_binaries
|
|
||||||
if [ "$UPDATE_MODE" != true ]; then
|
|
||||||
generate_configs
|
|
||||||
configure_firewall
|
|
||||||
else
|
|
||||||
log "Update mode: keeping existing configuration"
|
|
||||||
fi
|
|
||||||
create_systemd_services
|
|
||||||
start_services
|
|
||||||
|
|
||||||
DEBROS_HOME=$(sudo -u "$DEBROS_USER" sh -c 'echo ~')
|
echo -e ""
|
||||||
|
echo -e "${BLUE}========================================${NOCOLOR}"
|
||||||
|
echo -e "${GREEN}Step 1: Install network-cli${NOCOLOR}"
|
||||||
|
echo -e "${BLUE}========================================${NOCOLOR}"
|
||||||
|
echo -e ""
|
||||||
|
|
||||||
log "${BLUE}==================================================${NOCOLOR}"
|
get_latest_release
|
||||||
if [ "$UPDATE_MODE" = true ]; then
|
download_and_install
|
||||||
log "${GREEN} Update Complete! ${NOCOLOR}"
|
|
||||||
else
|
|
||||||
log "${GREEN} Installation Complete! ${NOCOLOR}"
|
|
||||||
fi
|
|
||||||
log "${BLUE}==================================================${NOCOLOR}"
|
|
||||||
log "${GREEN}Installation Directory:${NOCOLOR} ${CYAN}$INSTALL_DIR${NOCOLOR}"
|
|
||||||
log "${GREEN}Config Directory:${NOCOLOR} ${CYAN}$DEBROS_HOME/.debros${NOCOLOR}"
|
|
||||||
log "${GREEN}LibP2P Port:${NOCOLOR} ${CYAN}$NODE_PORT${NOCOLOR}"
|
|
||||||
log "${GREEN}RQLite Port:${NOCOLOR} ${CYAN}$RQLITE_PORT${NOCOLOR}"
|
|
||||||
log "${GREEN}Gateway Port:${NOCOLOR} ${CYAN}$GATEWAY_PORT${NOCOLOR}"
|
|
||||||
log "${GREEN}Raft Port:${NOCOLOR} ${CYAN}$RAFT_PORT${NOCOLOR}"
|
|
||||||
log "${BLUE}==================================================${NOCOLOR}"
|
|
||||||
log "${GREEN}Service Management:${NOCOLOR}"
|
|
||||||
log "${CYAN} - sudo systemctl status debros-node${NOCOLOR} (Check node status)"
|
|
||||||
log "${CYAN} - sudo systemctl status debros-gateway${NOCOLOR} (Check gateway status)"
|
|
||||||
log "${CYAN} - sudo systemctl restart debros-node${NOCOLOR} (Restart node)"
|
|
||||||
log "${CYAN} - sudo systemctl restart debros-gateway${NOCOLOR} (Restart gateway)"
|
|
||||||
log "${CYAN} - sudo systemctl stop debros-node${NOCOLOR} (Stop node)"
|
|
||||||
log "${CYAN} - sudo systemctl stop debros-gateway${NOCOLOR} (Stop gateway)"
|
|
||||||
log "${CYAN} - sudo journalctl -u debros-node.service -f${NOCOLOR} (View node logs)"
|
|
||||||
log "${CYAN} - sudo journalctl -u debros-gateway.service -f${NOCOLOR} (View gateway logs)"
|
|
||||||
log "${BLUE}==================================================${NOCOLOR}"
|
|
||||||
log "${GREEN}Verify Installation:${NOCOLOR}"
|
|
||||||
log "${CYAN} - Node health: curl http://127.0.0.1:5001/status${NOCOLOR}"
|
|
||||||
log "${CYAN} - Gateway health: curl http://127.0.0.1:6001/health${NOCOLOR}"
|
|
||||||
log "${CYAN} - Show bootstrap peer: cat $DEBROS_HOME/.debros/bootstrap/peer.info${NOCOLOR}"
|
|
||||||
log "${BLUE}==================================================${NOCOLOR}"
|
|
||||||
|
|
||||||
if [ "$UPDATE_MODE" = true ]; then
|
# Verify installation
|
||||||
success "DeBros Network has been updated and is running!"
|
if ! verify_installation; then
|
||||||
else
|
exit 1
|
||||||
success "DeBros Network is now running!"
|
|
||||||
fi
|
fi
|
||||||
log "${CYAN}For documentation visit: https://docs.debros.io${NOCOLOR}"
|
|
||||||
|
# Run setup
|
||||||
|
run_setup
|
||||||
|
|
||||||
|
# Show completion message
|
||||||
|
show_completion
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
289
scripts/release.sh
Executable file
289
scripts/release.sh
Executable file
@ -0,0 +1,289 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# DeBros Network Interactive Release Script
|
||||||
|
# Handles the complete release workflow for both stable and nightly releases
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Color codes
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
BLUE='\033[38;2;2;128;175m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NOCOLOR='\033[0m'
|
||||||
|
|
||||||
|
log() { echo -e "${CYAN}[$(date '+%Y-%m-%d %H:%M:%S')]${NOCOLOR} $1"; }
|
||||||
|
error() { echo -e "${RED}[ERROR]${NOCOLOR} $1"; }
|
||||||
|
success() { echo -e "${GREEN}[SUCCESS]${NOCOLOR} $1"; }
|
||||||
|
warning() { echo -e "${YELLOW}[WARNING]${NOCOLOR} $1"; }
|
||||||
|
info() { echo -e "${BLUE}ℹ${NOCOLOR} $1"; }
|
||||||
|
|
||||||
|
display_banner() {
|
||||||
|
echo -e "${BLUE}========================================================================${NOCOLOR}"
|
||||||
|
echo -e "${CYAN}
|
||||||
|
____ ____ _ _ _ _
|
||||||
|
| _ \\ ___| __ ) _ __ ___ ___ | \\ | | ___| |___ _____ _ __| | __
|
||||||
|
| | | |/ _ \\ _ \\| __/ _ \\/ __| | \\| |/ _ \\ __\\ \\ /\\ / / _ \\| __| |/ /
|
||||||
|
| |_| | __/ |_) | | | (_) \\__ \\ | |\\ | __/ |_ \\ V V / (_) | | | <
|
||||||
|
|____/ \\___|____/|_| \\___/|___/ |_| \\_|\\___|\\__| \\_/\\_/ \\___/|_| |_|\\_\\
|
||||||
|
${NOCOLOR}"
|
||||||
|
echo -e "${BLUE}========================================================================${NOCOLOR}"
|
||||||
|
echo -e "${GREEN} Release Management Tool${NOCOLOR}"
|
||||||
|
echo -e "${BLUE}========================================================================${NOCOLOR}"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_git_clean() {
|
||||||
|
if ! git diff-index --quiet HEAD --; then
|
||||||
|
error "Working directory has uncommitted changes. Please commit or stash them first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_current_branch() {
|
||||||
|
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
echo "$CURRENT_BRANCH"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_latest_version() {
|
||||||
|
git tag --list 'v*' --sort=-version:refname | head -1 | sed 's/^v//' || echo "0.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
increment_version() {
|
||||||
|
local version=$1
|
||||||
|
local bump=$2
|
||||||
|
|
||||||
|
IFS='.' read -r major minor patch <<< "$version"
|
||||||
|
|
||||||
|
case $bump in
|
||||||
|
major)
|
||||||
|
major=$((major + 1))
|
||||||
|
minor=0
|
||||||
|
patch=0
|
||||||
|
;;
|
||||||
|
minor)
|
||||||
|
minor=$((minor + 1))
|
||||||
|
patch=0
|
||||||
|
;;
|
||||||
|
patch)
|
||||||
|
patch=$((patch + 1))
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "$major.$minor.$patch"
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt_release_type() {
|
||||||
|
echo "" >&2
|
||||||
|
echo -e "${CYAN}=== Release Type ===${NOCOLOR}" >&2
|
||||||
|
echo "1) Stable Release (merge nightly → main, tag on main)" >&2
|
||||||
|
echo "2) Nightly Release (tag directly on nightly)" >&2
|
||||||
|
echo "3) Exit" >&2
|
||||||
|
echo "" >&2
|
||||||
|
read -p "Choose release type (1-3): " release_type >&2
|
||||||
|
echo "$release_type"
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt_version_strategy() {
|
||||||
|
echo "" >&2
|
||||||
|
echo -e "${CYAN}=== Version Strategy ===${NOCOLOR}" >&2
|
||||||
|
local latest=$(get_latest_version)
|
||||||
|
echo "Latest version: $latest" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "1) Major bump ($latest → $(increment_version $latest major))" >&2
|
||||||
|
echo "2) Minor bump ($latest → $(increment_version $latest minor))" >&2
|
||||||
|
echo "3) Patch bump ($latest → $(increment_version $latest patch))" >&2
|
||||||
|
echo "4) Custom version" >&2
|
||||||
|
echo "" >&2
|
||||||
|
read -p "Choose version strategy (1-4): " version_strategy >&2
|
||||||
|
echo "$version_strategy"
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt_custom_version() {
|
||||||
|
read -p "Enter custom version (e.g., 0.52.1): " custom_version >&2
|
||||||
|
if [[ ! $custom_version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
error "Invalid version format. Must be X.Y.Z"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "$custom_version"
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm_release() {
|
||||||
|
local version=$1
|
||||||
|
local target=$2
|
||||||
|
local msg=$3
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}=== Release Summary ===${NOCOLOR}"
|
||||||
|
echo "Version: $version"
|
||||||
|
echo "Target: $target"
|
||||||
|
echo "Message: $msg"
|
||||||
|
echo ""
|
||||||
|
read -p "Is this correct? (yes/no): " confirm
|
||||||
|
|
||||||
|
if [[ "$confirm" != "yes" ]]; then
|
||||||
|
error "Release cancelled."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_stable_release() {
|
||||||
|
local version=$1
|
||||||
|
|
||||||
|
log "Stable Release: v$version"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if on nightly
|
||||||
|
if [[ "$CURRENT_BRANCH" != "nightly" ]]; then
|
||||||
|
warning "Not on nightly branch. Checking out nightly..."
|
||||||
|
git checkout nightly
|
||||||
|
git pull origin nightly
|
||||||
|
else
|
||||||
|
git pull origin nightly
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Current branch: nightly"
|
||||||
|
info "Next step: Create PR from nightly → main in GitHub"
|
||||||
|
info "Once PR is merged, this script will create the release tag"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}Have you already merged the PR to main? (yes/no)${NOCOLOR}"
|
||||||
|
read -p "> " pr_merged
|
||||||
|
|
||||||
|
if [[ "$pr_merged" != "yes" ]]; then
|
||||||
|
warning "Please create and merge the PR first, then run this script again."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify main is updated
|
||||||
|
log "Switching to main and pulling latest..."
|
||||||
|
git checkout main
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# Create tag
|
||||||
|
log "Creating tag v$version on main..."
|
||||||
|
git tag -a "v$version" -m "Release v$version"
|
||||||
|
|
||||||
|
success "Tag created: v$version"
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_nightly_release() {
|
||||||
|
local version=$1
|
||||||
|
|
||||||
|
log "Nightly Release: v$version-nightly"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if on nightly
|
||||||
|
if [[ "$CURRENT_BRANCH" != "nightly" ]]; then
|
||||||
|
warning "Not on nightly branch. Checking out nightly..."
|
||||||
|
git checkout nightly
|
||||||
|
git pull origin nightly
|
||||||
|
else
|
||||||
|
git pull origin nightly
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Current branch: nightly"
|
||||||
|
|
||||||
|
# Create tag
|
||||||
|
log "Creating tag v$version-nightly on nightly..."
|
||||||
|
git tag -a "v$version-nightly" -m "Nightly Release v$version"
|
||||||
|
|
||||||
|
success "Tag created: v$version-nightly"
|
||||||
|
}
|
||||||
|
|
||||||
|
push_release() {
|
||||||
|
local tag=$1
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}=== Pushing Release ===${NOCOLOR}"
|
||||||
|
log "Pushing tag $tag to origin..."
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}This will trigger GitHub Actions to build and publish the release.${NOCOLOR}"
|
||||||
|
read -p "Continue? (yes/no): " confirm_push
|
||||||
|
|
||||||
|
if [[ "$confirm_push" != "yes" ]]; then
|
||||||
|
error "Push cancelled. Tag created but not pushed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git push origin "$tag"
|
||||||
|
success "Tag pushed successfully!"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}========================================================================${NOCOLOR}"
|
||||||
|
echo -e "${GREEN}✅ Release Started!${NOCOLOR}"
|
||||||
|
echo -e "${GREEN}========================================================================${NOCOLOR}"
|
||||||
|
echo ""
|
||||||
|
echo -e "📊 Monitor your release:"
|
||||||
|
echo -e " • GitHub Actions: https://github.com/DeBrosOfficial/network/actions"
|
||||||
|
echo -e " • Releases: https://github.com/DeBrosOfficial/network/releases"
|
||||||
|
echo ""
|
||||||
|
echo -e "⏱️ The build usually takes 2-5 minutes."
|
||||||
|
echo -e "📦 Your release will appear on the Releases page once complete."
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
display_banner
|
||||||
|
|
||||||
|
# Check git status
|
||||||
|
log "Checking git status..."
|
||||||
|
check_git_clean
|
||||||
|
|
||||||
|
CURRENT_BRANCH=$(check_current_branch)
|
||||||
|
log "Current branch: $CURRENT_BRANCH"
|
||||||
|
|
||||||
|
# Get release type
|
||||||
|
release_type=$(prompt_release_type)
|
||||||
|
|
||||||
|
if [[ "$release_type" == "3" ]]; then
|
||||||
|
info "Release cancelled."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get version strategy
|
||||||
|
version_strategy=$(prompt_version_strategy)
|
||||||
|
latest_version=$(get_latest_version)
|
||||||
|
|
||||||
|
case $version_strategy in
|
||||||
|
1)
|
||||||
|
new_version=$(increment_version "$latest_version" major)
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
new_version=$(increment_version "$latest_version" minor)
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
new_version=$(increment_version "$latest_version" patch)
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
new_version=$(prompt_custom_version)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
error "Invalid choice"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Handle release based on type
|
||||||
|
case $release_type in
|
||||||
|
1)
|
||||||
|
# Stable release
|
||||||
|
confirm_release "$new_version" "main (stable)" "Release v$new_version to stable main branch"
|
||||||
|
handle_stable_release "$new_version"
|
||||||
|
push_release "v$new_version"
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
# Nightly release
|
||||||
|
confirm_release "$new_version" "nightly (development)" "Release v$new_version-nightly to nightly branch"
|
||||||
|
handle_nightly_release "$new_version"
|
||||||
|
push_release "v$new_version-nightly"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
error "Invalid choice"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo -e "${GREEN}Done! 🎉${NOCOLOR}"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Loading…
x
Reference in New Issue
Block a user