From 9193f088a37650ecc6c2f0f3d76ce49d8ffcad65 Mon Sep 17 00:00:00 2001 From: anonpenguin23 Date: Fri, 28 Nov 2025 22:27:27 +0200 Subject: [PATCH] feat: update node and gateway commands to use Orama naming convention - Renamed the node executable from `node` to `orama-node` in the Makefile and various scripts to reflect the new naming convention. - Updated the gateway command to `orama-gateway` for consistency. - Modified service configurations and systemd templates to ensure proper execution of the renamed binaries. - Enhanced the interactive installer to prompt for the gateway URL, allowing users to select between local and remote nodes. - Added functionality to extract domain information for TLS configuration, improving security for remote connections. --- CHANGELOG.md | 22 +++ Makefile | 12 +- pkg/auth/simple_auth.go | 30 ++- pkg/cli/auth_commands.go | 53 +++++- pkg/cli/prod_commands.go | 89 ++++++--- pkg/client/implementations.go | 42 ++++ pkg/client/interface.go | 21 +- pkg/discovery/discovery.go | 2 +- pkg/environments/development/runner.go | 2 +- pkg/environments/production/installers.go | 42 +++- pkg/environments/production/orchestrator.go | 26 ++- pkg/environments/production/services.go | 37 ++-- pkg/environments/templates/node.yaml | 2 +- .../templates/systemd_node.service | 2 +- pkg/installer/installer.go | 83 +++++--- pkg/ipfs/cluster.go | 179 +++++++++++++++++- scripts/dev-kill-all.sh | 2 +- 17 files changed, 540 insertions(+), 106 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 364ab11..5e04e6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,28 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant ### Deprecated ### Fixed +## [0.72.0] - 2025-11-28 + +### Added +- Interactive prompt for selecting local or remote gateway URL during CLI login. +- Support for discovering and configuring IPFS Cluster peers during installation and runtime via the gateway status endpoint. +- New CLI flags (`--ipfs-cluster-peer`, `--ipfs-cluster-addrs`) added to the `prod install` command for cluster discovery. + +### Changed +- Renamed the main network node executable from `node` to `orama-node` and the gateway executable to `orama-gateway`. +- Improved the `auth login` flow to use a TLS-aware HTTP client, supporting Let's Encrypt staging certificates for remote gateways. +- Updated the production installer to set `CAP_NET_BIND_SERVICE` on `orama-node` to allow binding to privileged ports (80/443) without root. +- Updated the production installer to configure IPFS Cluster to listen on port 9098 for consistent multi-node communication. +- Refactored the `prod install` process to generate configurations before initializing services, ensuring configuration files are present. + +### Deprecated + +### Removed + +### Fixed +- Corrected the IPFS Cluster API port used in `node.yaml` template from 9096 to 9098 to match the cluster's LibP2P port. +- Fixed the `anyone-client` systemd service configuration to use the correct binary name and allow writing to the home directory. + ## [0.71.0] - 2025-11-27 ### Added diff --git a/Makefile b/Makefile index 1cb885e..64841ca 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ test-e2e: .PHONY: build clean test run-node run-node2 run-node3 run-example deps tidy fmt vet lint clear-ports install-hooks kill -VERSION := 0.71.0 +VERSION := 0.72.0 COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ) LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)' -X 'main.date=$(DATE)' @@ -29,7 +29,7 @@ build: deps @echo "Building network executables (version=$(VERSION))..." @mkdir -p bin go build -ldflags "$(LDFLAGS)" -o bin/identity ./cmd/identity - go build -ldflags "$(LDFLAGS)" -o bin/node ./cmd/node + go build -ldflags "$(LDFLAGS)" -o bin/orama-node ./cmd/node go build -ldflags "$(LDFLAGS)" -o bin/orama cmd/cli/main.go # Inject gateway build metadata via pkg path variables go build -ldflags "$(LDFLAGS) -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildVersion=$(VERSION)' -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildCommit=$(COMMIT)' -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildTime=$(DATE)'" -o bin/gateway ./cmd/gateway @@ -51,25 +51,25 @@ clean: run-node: @echo "Starting node..." @echo "Config: ~/.orama/node.yaml" - go run ./cmd/node --config node.yaml + go run ./cmd/orama-node --config node.yaml # Run second node - requires join address run-node2: @echo "Starting second node..." @echo "Config: ~/.orama/node2.yaml" - go run ./cmd/node --config node2.yaml + go run ./cmd/orama-node --config node2.yaml # Run third node - requires join address run-node3: @echo "Starting third node..." @echo "Config: ~/.orama/node3.yaml" - go run ./cmd/node --config node3.yaml + go run ./cmd/orama-node --config node3.yaml # Run gateway HTTP server run-gateway: @echo "Starting gateway HTTP server..." @echo "Note: Config must be in ~/.orama/data/gateway.yaml" - go run ./cmd/gateway + go run ./cmd/orama-gateway # Setup local domain names for development setup-domains: diff --git a/pkg/auth/simple_auth.go b/pkg/auth/simple_auth.go index 246ed80..af11953 100644 --- a/pkg/auth/simple_auth.go +++ b/pkg/auth/simple_auth.go @@ -10,6 +10,8 @@ import ( "os" "strings" "time" + + "github.com/DeBrosOfficial/network/pkg/tlsutil" ) // PerformSimpleAuthentication performs a simple authentication flow where the user @@ -91,7 +93,13 @@ func requestAPIKeyFromGateway(gatewayURL, wallet, namespace string) (string, err } endpoint := gatewayURL + "/v1/auth/simple-key" - resp, err := http.Post(endpoint, "application/json", bytes.NewReader(payload)) + + // Extract domain from URL for TLS configuration + // This uses tlsutil which handles Let's Encrypt staging certificates for *.debros.network + domain := extractDomainFromURL(gatewayURL) + client := tlsutil.NewHTTPClientForDomain(30*time.Second, domain) + + resp, err := client.Post(endpoint, "application/json", bytes.NewReader(payload)) if err != nil { return "", fmt.Errorf("failed to call gateway: %w", err) } @@ -114,3 +122,23 @@ func requestAPIKeyFromGateway(gatewayURL, wallet, namespace string) (string, err return apiKey, nil } + +// extractDomainFromURL extracts the domain from a URL +// Removes protocol (https://, http://), path, and port components +func extractDomainFromURL(url string) string { + // Remove protocol prefixes + url = strings.TrimPrefix(url, "https://") + url = strings.TrimPrefix(url, "http://") + + // Remove path component + if idx := strings.Index(url, "/"); idx != -1 { + url = url[:idx] + } + + // Remove port component + if idx := strings.Index(url, ":"); idx != -1 { + url = url[:idx] + } + + return url +} diff --git a/pkg/cli/auth_commands.go b/pkg/cli/auth_commands.go index 6a159c1..36f8594 100644 --- a/pkg/cli/auth_commands.go +++ b/pkg/cli/auth_commands.go @@ -1,8 +1,10 @@ package cli import ( + "bufio" "fmt" "os" + "strings" "github.com/DeBrosOfficial/network/pkg/auth" ) @@ -56,7 +58,8 @@ func showAuthHelp() { } func handleAuthLogin() { - gatewayURL := getGatewayURL() + // Prompt for node selection + gatewayURL := promptForGatewayURL() fmt.Printf("🔐 Authenticating with gateway at: %s\n", gatewayURL) // Use the simple authentication flow @@ -161,7 +164,55 @@ func handleAuthStatus() { } } +// promptForGatewayURL interactively prompts for the gateway URL +// Allows user to choose between local node or remote node by domain +func promptForGatewayURL() string { + // Check environment variable first (allows override without prompting) + if url := os.Getenv("DEBROS_GATEWAY_URL"); url != "" { + return url + } + + reader := bufio.NewReader(os.Stdin) + + fmt.Println("\n🌐 Node Connection") + fmt.Println("==================") + fmt.Println("1. Local node (localhost:6001)") + fmt.Println("2. Remote node (enter domain)") + fmt.Print("\nSelect option [1/2]: ") + + choice, _ := reader.ReadString('\n') + choice = strings.TrimSpace(choice) + + if choice == "1" || choice == "" { + return "http://localhost:6001" + } + + if choice != "2" { + fmt.Println("âš ī¸ Invalid option, using localhost") + return "http://localhost:6001" + } + + fmt.Print("Enter node domain (e.g., node-hk19de.debros.network): ") + domain, _ := reader.ReadString('\n') + domain = strings.TrimSpace(domain) + + if domain == "" { + fmt.Println("âš ī¸ No domain entered, using localhost") + return "http://localhost:6001" + } + + // Remove any protocol prefix if user included it + domain = strings.TrimPrefix(domain, "https://") + domain = strings.TrimPrefix(domain, "http://") + // Remove trailing slash + domain = strings.TrimSuffix(domain, "/") + + // Use HTTPS for remote domains + return fmt.Sprintf("https://%s", domain) +} + // getGatewayURL returns the gateway URL based on environment or env var +// Used by other commands that don't need interactive node selection func getGatewayURL() string { // Check environment variable first (for backwards compatibility) if url := os.Getenv("DEBROS_GATEWAY_URL"); url != "" { diff --git a/pkg/cli/prod_commands.go b/pkg/cli/prod_commands.go index fce681f..ce7d9ab 100644 --- a/pkg/cli/prod_commands.go +++ b/pkg/cli/prod_commands.go @@ -26,6 +26,12 @@ type IPFSPeerInfo struct { Addrs []string } +// IPFSClusterPeerInfo contains IPFS Cluster peer information for cluster discovery +type IPFSClusterPeerInfo struct { + PeerID string + Addrs []string +} + // validateSwarmKey validates that a swarm key is 64 hex characters func validateSwarmKey(key string) error { key = strings.TrimSpace(key) @@ -76,6 +82,13 @@ func runInteractiveInstaller() { if len(config.IPFSSwarmAddrs) > 0 { args = append(args, "--ipfs-addrs", strings.Join(config.IPFSSwarmAddrs, ",")) } + // Pass IPFS Cluster peer info for cluster peer_addresses configuration + if config.IPFSClusterPeerID != "" { + args = append(args, "--ipfs-cluster-peer", config.IPFSClusterPeerID) + } + if len(config.IPFSClusterAddrs) > 0 { + args = append(args, "--ipfs-cluster-addrs", strings.Join(config.IPFSClusterAddrs, ",")) + } } // Re-run with collected args @@ -315,6 +328,8 @@ func showProdHelp() { fmt.Printf(" --swarm-key HEX - 64-hex IPFS swarm key (required when joining)\n") fmt.Printf(" --ipfs-peer ID - IPFS peer ID to connect to (auto-discovered)\n") fmt.Printf(" --ipfs-addrs ADDRS - IPFS swarm addresses (auto-discovered)\n") + fmt.Printf(" --ipfs-cluster-peer ID - IPFS Cluster peer ID (auto-discovered)\n") + fmt.Printf(" --ipfs-cluster-addrs ADDRS - IPFS Cluster addresses (auto-discovered)\n") fmt.Printf(" --branch BRANCH - Git branch to use (main or nightly, default: main)\n") fmt.Printf(" --no-pull - Skip git clone/pull, use existing /home/debros/src\n") fmt.Printf(" --ignore-resource-checks - Skip disk/RAM/CPU prerequisite validation\n") @@ -369,6 +384,8 @@ func handleProdInstall(args []string) { swarmKey := fs.String("swarm-key", "", "64-hex IPFS swarm key (for joining existing private network)") ipfsPeerID := fs.String("ipfs-peer", "", "IPFS peer ID to connect to (auto-discovered from peer domain)") ipfsAddrs := fs.String("ipfs-addrs", "", "Comma-separated IPFS swarm addresses (auto-discovered from peer domain)") + ipfsClusterPeerID := fs.String("ipfs-cluster-peer", "", "IPFS Cluster peer ID to connect to (auto-discovered from peer domain)") + ipfsClusterAddrs := fs.String("ipfs-cluster-addrs", "", "Comma-separated IPFS Cluster addresses (auto-discovered from peer domain)") interactive := fs.Bool("interactive", false, "Run interactive TUI installer") dryRun := fs.Bool("dry-run", false, "Show what would be done without making changes") noPull := fs.Bool("no-pull", false, "Skip git clone/pull, use existing /home/debros/src") @@ -488,6 +505,19 @@ func handleProdInstall(args []string) { } } + // Store IPFS Cluster peer info for cluster peer discovery + var ipfsClusterPeerInfo *IPFSClusterPeerInfo + if *ipfsClusterPeerID != "" { + var addrs []string + if *ipfsClusterAddrs != "" { + addrs = strings.Split(*ipfsClusterAddrs, ",") + } + ipfsClusterPeerInfo = &IPFSClusterPeerInfo{ + PeerID: *ipfsClusterPeerID, + Addrs: addrs, + } + } + setup := production.NewProductionSetup(oramaHome, os.Stdout, *force, *branch, *noPull, *skipResourceChecks) // Inform user if skipping git pull @@ -548,21 +578,8 @@ func handleProdInstall(args []string) { os.Exit(1) } - // Phase 2c: Initialize services (after secrets are in place) - fmt.Printf("\nPhase 2c: Initializing services...\n") - var prodIPFSPeer *production.IPFSPeerInfo - if ipfsPeerInfo != nil { - prodIPFSPeer = &production.IPFSPeerInfo{ - PeerID: ipfsPeerInfo.PeerID, - Addrs: ipfsPeerInfo.Addrs, - } - } - if err := setup.Phase2cInitializeServices(peers, *vpsIP, prodIPFSPeer); err != nil { - fmt.Fprintf(os.Stderr, "❌ Service initialization failed: %v\n", err) - os.Exit(1) - } - - // Phase 4: Generate configs + // Phase 4: Generate configs (BEFORE service initialization) + // This ensures node.yaml exists before services try to access it fmt.Printf("\nâš™ī¸ Phase 4: Generating configurations...\n") enableHTTPS := *domain != "" if err := setup.Phase4GenerateConfigs(peers, *vpsIP, enableHTTPS, *domain, *joinAddress); err != nil { @@ -578,6 +595,27 @@ func handleProdInstall(args []string) { } fmt.Printf(" ✓ Configuration validated\n") + // Phase 2c: Initialize services (after config is in place) + fmt.Printf("\nPhase 2c: Initializing services...\n") + var prodIPFSPeer *production.IPFSPeerInfo + if ipfsPeerInfo != nil { + prodIPFSPeer = &production.IPFSPeerInfo{ + PeerID: ipfsPeerInfo.PeerID, + Addrs: ipfsPeerInfo.Addrs, + } + } + var prodIPFSClusterPeer *production.IPFSClusterPeerInfo + if ipfsClusterPeerInfo != nil { + prodIPFSClusterPeer = &production.IPFSClusterPeerInfo{ + PeerID: ipfsClusterPeerInfo.PeerID, + Addrs: ipfsClusterPeerInfo.Addrs, + } + } + if err := setup.Phase2cInitializeServices(peers, *vpsIP, prodIPFSPeer, prodIPFSClusterPeer); err != nil { + fmt.Fprintf(os.Stderr, "❌ Service initialization failed: %v\n", err) + os.Exit(1) + } + // Phase 5: Create systemd services fmt.Printf("\n🔧 Phase 5: Creating systemd services...\n") if err := setup.Phase5CreateSystemdServices(enableHTTPS); err != nil { @@ -876,20 +914,23 @@ func handleProdUpgrade(args []string) { fmt.Printf(" - Join address: %s\n", joinAddress) } - // Phase 2c: Ensure services are properly initialized (fixes existing repos) - // Now that we have peers and VPS IP, we can properly configure IPFS Cluster - // Note: IPFS peer info is nil for upgrades - peering is only configured during initial install - fmt.Printf("\nPhase 2c: Ensuring services are properly initialized...\n") - if err := setup.Phase2cInitializeServices(peers, vpsIP, nil); err != nil { - fmt.Fprintf(os.Stderr, "❌ Service initialization failed: %v\n", err) - os.Exit(1) - } - + // Phase 4: Generate configs (BEFORE service initialization) + // This ensures node.yaml exists before services try to access it if err := setup.Phase4GenerateConfigs(peers, vpsIP, enableHTTPS, domain, joinAddress); err != nil { fmt.Fprintf(os.Stderr, "âš ī¸ Config generation warning: %v\n", err) fmt.Fprintf(os.Stderr, " Existing configs preserved\n") } + // Phase 2c: Ensure services are properly initialized (fixes existing repos) + // Now that we have peers and VPS IP, we can properly configure IPFS Cluster + // Note: IPFS peer info is nil for upgrades - peering is only configured during initial install + // Note: IPFS Cluster peer info is also nil for upgrades - peer_addresses is only configured during initial install + fmt.Printf("\nPhase 2c: Ensuring services are properly initialized...\n") + if err := setup.Phase2cInitializeServices(peers, vpsIP, nil, nil); err != nil { + fmt.Fprintf(os.Stderr, "❌ Service initialization failed: %v\n", err) + os.Exit(1) + } + // Phase 5: Update systemd services fmt.Printf("\n🔧 Phase 5: Updating systemd services...\n") if err := setup.Phase5CreateSystemdServices(enableHTTPS); err != nil { diff --git a/pkg/client/implementations.go b/pkg/client/implementations.go index 1eccec5..46392f6 100644 --- a/pkg/client/implementations.go +++ b/pkg/client/implementations.go @@ -509,6 +509,9 @@ func (n *NetworkInfoImpl) GetStatus(ctx context.Context) (*NetworkStatus, error) // Try to get IPFS peer info (optional - don't fail if unavailable) ipfsInfo := queryIPFSPeerInfo() + // Try to get IPFS Cluster peer info (optional - don't fail if unavailable) + ipfsClusterInfo := queryIPFSClusterPeerInfo() + return &NetworkStatus{ NodeID: host.ID().String(), PeerID: host.ID().String(), @@ -517,6 +520,7 @@ func (n *NetworkInfoImpl) GetStatus(ctx context.Context) (*NetworkStatus, error) DatabaseSize: dbSize, Uptime: time.Since(n.client.startTime), IPFS: ipfsInfo, + IPFSCluster: ipfsClusterInfo, }, nil } @@ -558,6 +562,44 @@ func queryIPFSPeerInfo() *IPFSPeerInfo { } } +// queryIPFSClusterPeerInfo queries the local IPFS Cluster API for peer information +// Returns nil if IPFS Cluster is not running or unavailable +func queryIPFSClusterPeerInfo() *IPFSClusterPeerInfo { + // IPFS Cluster API typically runs on port 9094 in our setup + client := &http.Client{Timeout: 2 * time.Second} + resp, err := client.Get("http://localhost:9094/id") + if err != nil { + return nil // IPFS Cluster not available + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil + } + + var result struct { + ID string `json:"id"` + Addresses []string `json:"addresses"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil + } + + // Filter addresses to only include public/routable ones for cluster discovery + var clusterAddrs []string + for _, addr := range result.Addresses { + // Skip loopback addresses - only keep routable addresses + if !strings.Contains(addr, "127.0.0.1") && !strings.Contains(addr, "/ip6/::1") { + clusterAddrs = append(clusterAddrs, addr) + } + } + + return &IPFSClusterPeerInfo{ + PeerID: result.ID, + Addresses: clusterAddrs, + } +} + // ConnectToPeer connects to a specific peer func (n *NetworkInfoImpl) ConnectToPeer(ctx context.Context, peerAddr string) error { if !n.client.isConnected() { diff --git a/pkg/client/interface.go b/pkg/client/interface.go index b4de258..8eaf377 100644 --- a/pkg/client/interface.go +++ b/pkg/client/interface.go @@ -114,13 +114,14 @@ type PeerInfo struct { // NetworkStatus contains overall network status type NetworkStatus struct { - NodeID string `json:"node_id"` - PeerID string `json:"peer_id"` - Connected bool `json:"connected"` - PeerCount int `json:"peer_count"` - DatabaseSize int64 `json:"database_size"` - Uptime time.Duration `json:"uptime"` - IPFS *IPFSPeerInfo `json:"ipfs,omitempty"` + NodeID string `json:"node_id"` + PeerID string `json:"peer_id"` + Connected bool `json:"connected"` + PeerCount int `json:"peer_count"` + DatabaseSize int64 `json:"database_size"` + Uptime time.Duration `json:"uptime"` + IPFS *IPFSPeerInfo `json:"ipfs,omitempty"` + IPFSCluster *IPFSClusterPeerInfo `json:"ipfs_cluster,omitempty"` } // IPFSPeerInfo contains IPFS peer information for discovery @@ -129,6 +130,12 @@ type IPFSPeerInfo struct { SwarmAddresses []string `json:"swarm_addresses"` } +// IPFSClusterPeerInfo contains IPFS Cluster peer information for cluster discovery +type IPFSClusterPeerInfo struct { + PeerID string `json:"peer_id"` // Cluster peer ID (different from IPFS peer ID) + Addresses []string `json:"addresses"` // Cluster multiaddresses (e.g., /ip4/x.x.x.x/tcp/9098) +} + // HealthStatus contains health check information type HealthStatus struct { Status string `json:"status"` // "healthy", "degraded", "unhealthy" diff --git a/pkg/discovery/discovery.go b/pkg/discovery/discovery.go index 0d6be27..1d1ec60 100644 --- a/pkg/discovery/discovery.go +++ b/pkg/discovery/discovery.go @@ -271,7 +271,7 @@ func (d *Manager) discoverViaPeerstore(ctx context.Context, maxConnections int) } // Filter peers to only include those with addresses on our port (4001) - // This prevents attempting to connect to IPFS (port 4101) or IPFS Cluster (port 9096) + // This prevents attempting to connect to IPFS (port 4101) or IPFS Cluster (port 9096/9098) peerInfo := d.host.Peerstore().PeerInfo(pid) hasValidPort := false for _, addr := range peerInfo.Addrs { diff --git a/pkg/environments/development/runner.go b/pkg/environments/development/runner.go index 35cb6bf..9564ee7 100644 --- a/pkg/environments/development/runner.go +++ b/pkg/environments/development/runner.go @@ -1024,7 +1024,7 @@ func (pm *ProcessManager) startAnon(ctx context.Context) error { func (pm *ProcessManager) startNode(name, configFile, logPath string) error { pidPath := filepath.Join(pm.pidsDir, fmt.Sprintf("%s.pid", name)) - cmd := exec.Command("./bin/node", "--config", configFile) + cmd := exec.Command("./bin/orama-node", "--config", configFile) logFile, _ := os.Create(logPath) cmd.Stdout = logFile cmd.Stderr = logFile diff --git a/pkg/environments/production/installers.go b/pkg/environments/production/installers.go index 1c34294..40bab11 100644 --- a/pkg/environments/production/installers.go +++ b/pkg/environments/production/installers.go @@ -388,6 +388,17 @@ func (bi *BinaryInstaller) InstallDeBrosBinaries(branch string, oramaHome string fmt.Fprintf(bi.logWriter, " âš ī¸ Warning: failed to chown bin directory: %v\n", err) } + // Grant CAP_NET_BIND_SERVICE to orama-node to allow binding to ports 80/443 without root + nodeBinary := filepath.Join(binDir, "orama-node") + if _, err := os.Stat(nodeBinary); err == nil { + if err := exec.Command("setcap", "cap_net_bind_service=+ep", nodeBinary).Run(); err != nil { + fmt.Fprintf(bi.logWriter, " âš ī¸ Warning: failed to setcap on orama-node: %v\n", err) + fmt.Fprintf(bi.logWriter, " âš ī¸ Gateway may not be able to bind to port 80/443\n") + } else { + fmt.Fprintf(bi.logWriter, " ✓ Set CAP_NET_BIND_SERVICE on orama-node\n") + } + } + fmt.Fprintf(bi.logWriter, " ✓ DeBros binaries installed\n") return nil } @@ -418,6 +429,12 @@ type IPFSPeerInfo struct { Addrs []string } +// IPFSClusterPeerInfo contains IPFS Cluster peer information for cluster peer discovery +type IPFSClusterPeerInfo struct { + PeerID string // Cluster peer ID (different from IPFS peer ID) + Addrs []string // Cluster multiaddresses (e.g., /ip4/x.x.x.x/tcp/9098) +} + // InitializeIPFSRepo initializes an IPFS repository for a node (unified - no bootstrap/node distinction) // If ipfsPeer is provided, configures Peering.Peers for peer discovery in private networks func (bi *BinaryInstaller) InitializeIPFSRepo(ipfsRepoPath string, swarmKeyPath string, apiPort, gatewayPort, swarmPort int, ipfsPeer *IPFSPeerInfo) error { @@ -702,9 +719,12 @@ func (bi *BinaryInstaller) updateClusterConfig(clusterPath, secret string, ipfsA return fmt.Errorf("failed to parse service.json: %w", err) } - // Update cluster secret and peer addresses + // Update cluster secret, listen_multiaddress, and peer addresses if cluster, ok := config["cluster"].(map[string]interface{}); ok { cluster["secret"] = secret + // Set consistent listen_multiaddress - port 9098 for cluster LibP2P communication + // This MUST match the port used in GetClusterPeerMultiaddr() and peer_addresses + cluster["listen_multiaddress"] = []interface{}{"/ip4/0.0.0.0/tcp/9098"} // Configure peer addresses for cluster discovery // This allows nodes to find and connect to each other if len(bootstrapClusterPeers) > 0 { @@ -712,7 +732,8 @@ func (bi *BinaryInstaller) updateClusterConfig(clusterPath, secret string, ipfsA } } else { clusterConfig := map[string]interface{}{ - "secret": secret, + "secret": secret, + "listen_multiaddress": []interface{}{"/ip4/0.0.0.0/tcp/9098"}, } if len(bootstrapClusterPeers) > 0 { clusterConfig["peer_addresses"] = bootstrapClusterPeers @@ -821,7 +842,8 @@ func (bi *BinaryInstaller) InitializeRQLiteDataDir(dataDir string) error { // InstallAnyoneClient installs the anyone-client npm package globally func (bi *BinaryInstaller) InstallAnyoneClient() error { // Check if anyone-client is already available via npx (more reliable for scoped packages) - if cmd := exec.Command("npx", "--yes", "@anyone-protocol/anyone-client", "--version"); cmd.Run() == nil { + // Note: the CLI binary is "anyone-client", not the full scoped package name + if cmd := exec.Command("npx", "anyone-client", "--help"); cmd.Run() == nil { fmt.Fprintf(bi.logWriter, " ✓ anyone-client already installed\n") return nil } @@ -873,8 +895,18 @@ func (bi *BinaryInstaller) InstallAnyoneClient() error { return fmt.Errorf("failed to install anyone-client: %w\n%s", err, string(output)) } - // Verify installation - try npx first (most reliable for scoped packages) - verifyCmd := exec.Command("npx", "--yes", "@anyone-protocol/anyone-client", "--version") + // Create terms-agreement file to bypass interactive prompt when running as a service + termsFile := filepath.Join(debrosHome, "terms-agreement") + if err := os.WriteFile(termsFile, []byte("agreed"), 0644); err != nil { + fmt.Fprintf(bi.logWriter, " âš ī¸ Warning: failed to create terms-agreement: %v\n", err) + } else { + if err := exec.Command("chown", "debros:debros", termsFile).Run(); err != nil { + fmt.Fprintf(bi.logWriter, " âš ī¸ Warning: failed to chown terms-agreement: %v\n", err) + } + } + + // Verify installation - try npx with the correct CLI name (anyone-client, not full scoped package name) + verifyCmd := exec.Command("npx", "anyone-client", "--help") if err := verifyCmd.Run(); err != nil { // Fallback: check if binary exists in common locations possiblePaths := []string{ diff --git a/pkg/environments/production/orchestrator.go b/pkg/environments/production/orchestrator.go index 91bf9c6..f7f5554 100644 --- a/pkg/environments/production/orchestrator.go +++ b/pkg/environments/production/orchestrator.go @@ -273,7 +273,8 @@ func (ps *ProductionSetup) Phase2bInstallBinaries() error { // Phase2cInitializeServices initializes service repositories and configurations // ipfsPeer can be nil for the first node, or contain peer info for joining nodes -func (ps *ProductionSetup) Phase2cInitializeServices(peerAddresses []string, vpsIP string, ipfsPeer *IPFSPeerInfo) error { +// ipfsClusterPeer can be nil for the first node, or contain IPFS Cluster peer info for joining nodes +func (ps *ProductionSetup) Phase2cInitializeServices(peerAddresses []string, vpsIP string, ipfsPeer *IPFSPeerInfo, ipfsClusterPeer *IPFSClusterPeerInfo) error { ps.logf("Phase 2c: Initializing services...") // Ensure directories exist (unified structure) @@ -298,13 +299,28 @@ func (ps *ProductionSetup) Phase2cInitializeServices(peerAddresses []string, vps return fmt.Errorf("failed to get cluster secret: %w", err) } - // Get cluster peer addresses from peers if available + // Get cluster peer addresses from IPFS Cluster peer info if available var clusterPeers []string - if len(peerAddresses) > 0 { - // Infer IP from peers + if ipfsClusterPeer != nil && ipfsClusterPeer.PeerID != "" { + // Construct cluster peer multiaddress using the discovered peer ID + // Format: /ip4//tcp/9098/p2p/ peerIP := inferPeerIP(peerAddresses, vpsIP) if peerIP != "" { - ps.logf(" â„šī¸ Will attempt to connect to cluster peers at %s", peerIP) + // Construct the bootstrap multiaddress for IPFS Cluster + // Note: IPFS Cluster listens on port 9098 for cluster communication + clusterBootstrapAddr := fmt.Sprintf("/ip4/%s/tcp/9098/p2p/%s", peerIP, ipfsClusterPeer.PeerID) + clusterPeers = []string{clusterBootstrapAddr} + ps.logf(" â„šī¸ IPFS Cluster will connect to peer: %s", clusterBootstrapAddr) + } else if len(ipfsClusterPeer.Addrs) > 0 { + // Fallback: use the addresses from discovery (if they include peer ID) + for _, addr := range ipfsClusterPeer.Addrs { + if strings.Contains(addr, ipfsClusterPeer.PeerID) { + clusterPeers = append(clusterPeers, addr) + } + } + if len(clusterPeers) > 0 { + ps.logf(" â„šī¸ IPFS Cluster will connect to discovered peers: %v", clusterPeers) + } } } diff --git a/pkg/environments/production/services.go b/pkg/environments/production/services.go index a9c819b..7ae6eba 100644 --- a/pkg/environments/production/services.go +++ b/pkg/environments/production/services.go @@ -42,8 +42,8 @@ ExecStartPre=/bin/bash -c 'if [ -f %[3]s/secrets/swarm.key ] && [ ! -f %[2]s/swa ExecStart=%[5]s daemon --enable-pubsub-experiment --repo-dir=%[2]s Restart=always RestartSec=5 -StandardOutput=file:%[4]s -StandardError=file:%[4]s +StandardOutput=append:%[4]s +StandardError=append:%[4]s SyslogIdentifier=debros-ipfs NoNewPrivileges=yes @@ -92,8 +92,8 @@ ExecStartPre=/bin/bash -c 'mkdir -p %[2]s && chmod 700 %[2]s' ExecStart=%[4]s daemon Restart=always RestartSec=5 -StandardOutput=file:%[3]s -StandardError=file:%[3]s +StandardOutput=append:%[3]s +StandardError=append:%[3]s SyslogIdentifier=debros-ipfs-cluster NoNewPrivileges=yes @@ -147,8 +147,8 @@ Environment=HOME=%[1]s ExecStart=%[5]s %[2]s Restart=always RestartSec=5 -StandardOutput=file:%[3]s -StandardError=file:%[3]s +StandardOutput=append:%[3]s +StandardError=append:%[3]s SyslogIdentifier=debros-rqlite NoNewPrivileges=yes @@ -186,8 +186,8 @@ Environment=OLRIC_SERVER_CONFIG=%[2]s ExecStart=%[5]s Restart=always RestartSec=5 -StandardOutput=file:%[3]s -StandardError=file:%[3]s +StandardOutput=append:%[3]s +StandardError=append:%[3]s SyslogIdentifier=olric NoNewPrivileges=yes @@ -210,6 +210,8 @@ WantedBy=multi-user.target func (ssg *SystemdServiceGenerator) GenerateNodeService() string { configFile := "node.yaml" logFile := filepath.Join(ssg.oramaDir, "logs", "node.log") + // Note: systemd StandardOutput/StandardError paths should not contain substitution variables + // Use absolute paths directly as they will be resolved by systemd at runtime return fmt.Sprintf(`[Unit] Description=DeBros Network Node @@ -222,11 +224,11 @@ User=debros Group=debros WorkingDirectory=%[1]s Environment=HOME=%[1]s -ExecStart=%[1]s/bin/node --config %[2]s/configs/%[3]s +ExecStart=%[1]s/bin/orama-node --config %[2]s/configs/%[3]s Restart=always RestartSec=5 -StandardOutput=file:%[4]s -StandardError=file:%[4]s +StandardOutput=append:%[4]s +StandardError=append:%[4]s SyslogIdentifier=debros-node AmbientCapabilities=CAP_NET_BIND_SERVICE @@ -264,8 +266,8 @@ Environment=HOME=%[1]s ExecStart=%[1]s/bin/gateway --config %[2]s/data/gateway.yaml Restart=always RestartSec=5 -StandardOutput=file:%[3]s -StandardError=file:%[3]s +StandardOutput=append:%[3]s +StandardError=append:%[3]s SyslogIdentifier=debros-gateway AmbientCapabilities=CAP_NET_BIND_SERVICE @@ -303,17 +305,18 @@ User=debros Group=debros Environment=HOME=%[1]s Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/lib/node_modules/.bin -ExecStart=/usr/bin/npx --yes @anyone-protocol/anyone-client +WorkingDirectory=%[1]s +ExecStart=/usr/bin/npx anyone-client Restart=always RestartSec=5 -StandardOutput=file:%[2]s -StandardError=file:%[2]s +StandardOutput=append:%[2]s +StandardError=append:%[2]s SyslogIdentifier=anyone-client NoNewPrivileges=yes PrivateTmp=yes ProtectSystem=strict -ProtectHome=read-only +ProtectHome=no ProtectKernelTunables=yes ProtectKernelModules=yes ProtectControlGroups=yes diff --git a/pkg/environments/templates/node.yaml b/pkg/environments/templates/node.yaml index df8cc10..2024f5c 100644 --- a/pkg/environments/templates/node.yaml +++ b/pkg/environments/templates/node.yaml @@ -70,7 +70,7 @@ http_gateway: routes: # Note: Raft traffic bypasses SNI gateway - RQLite uses native TLS on port 7002 ipfs.{{.Domain}}: "localhost:4101" - ipfs-cluster.{{.Domain}}: "localhost:9096" + ipfs-cluster.{{.Domain}}: "localhost:9098" olric.{{.Domain}}: "localhost:3322" {{end}} diff --git a/pkg/environments/templates/systemd_node.service b/pkg/environments/templates/systemd_node.service index c1cfd16..d30b77e 100644 --- a/pkg/environments/templates/systemd_node.service +++ b/pkg/environments/templates/systemd_node.service @@ -10,7 +10,7 @@ User=debros Group=debros WorkingDirectory={{.HomeDir}} Environment=HOME={{.HomeDir}} -ExecStart={{.HomeDir}}/bin/node --config {{.OramaDir}}/configs/{{.ConfigFile}} +ExecStart={{.HomeDir}}/bin/orama-node --config {{.OramaDir}}/configs/{{.ConfigFile}} Restart=always RestartSec=5 StandardOutput=journal diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index d659e28..a545c90 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -32,9 +32,12 @@ type InstallerConfig struct { SwarmKeyHex string // 64-hex IPFS swarm key (for joining private network) IPFSPeerID string // IPFS peer ID (auto-discovered from peer domain) IPFSSwarmAddrs []string // IPFS swarm addresses (auto-discovered from peer domain) - Branch string - IsFirstNode bool - NoPull bool + // IPFS Cluster peer info for cluster discovery + IPFSClusterPeerID string // IPFS Cluster peer ID (auto-discovered from peer domain) + IPFSClusterAddrs []string // IPFS Cluster addresses (auto-discovered from peer domain) + Branch string + IsFirstNode bool + NoPull bool } // Step represents a step in the installation wizard @@ -69,6 +72,7 @@ type Model struct { discovering bool // Whether domain discovery is in progress discoveryInfo string // Info message during discovery discoveredPeer string // Discovered peer ID from domain + sniWarning string // Warning about missing SNI DNS records (non-blocking) } // Styles @@ -214,14 +218,13 @@ func (m *Model) handleEnter() (tea.Model, tea.Cmd) { return m, nil } - // Check SNI DNS records for this domain + // Check SNI DNS records for this domain (non-blocking warning) m.discovering = true - m.discoveryInfo = "Validating SNI DNS records for " + domain + "..." + m.discoveryInfo = "Checking SNI DNS records for " + domain + "..." - if err := validateSNIDNSRecords(domain); err != nil { - m.discovering = false - m.err = fmt.Errorf("SNI DNS validation failed: %w", err) - return m, nil + if warning := validateSNIDNSRecords(domain); warning != "" { + // Log warning but continue - SNI DNS is optional for single-node setups + m.sniWarning = warning } m.discovering = false @@ -255,14 +258,13 @@ func (m *Model) handleEnter() (tea.Model, tea.Cmd) { return m, nil } - // Validate SNI DNS records for peer domain + // Check SNI DNS records for peer domain (non-blocking warning) m.discovering = true - m.discoveryInfo = "Validating SNI DNS records for " + peerDomain + "..." + m.discoveryInfo = "Checking SNI DNS records for " + peerDomain + "..." - if err := validateSNIDNSRecords(peerDomain); err != nil { - m.discovering = false - m.err = fmt.Errorf("SNI DNS validation failed: %w", err) - return m, nil + if warning := validateSNIDNSRecords(peerDomain); warning != "" { + // Log warning but continue - peer might have different DNS setup + m.sniWarning = warning } // Discover peer info from domain (try HTTPS first, then HTTP) @@ -313,6 +315,12 @@ func (m *Model) handleEnter() (tea.Model, tea.Cmd) { m.config.IPFSSwarmAddrs = discovery.IPFSSwarmAddrs } + // Store IPFS Cluster peer info for cluster peer_addresses configuration + if discovery.IPFSClusterPeerID != "" { + m.config.IPFSClusterPeerID = discovery.IPFSClusterPeerID + m.config.IPFSClusterAddrs = discovery.IPFSClusterAddrs + } + m.err = nil m.step = StepClusterSecret m.setupStepInput() @@ -651,6 +659,13 @@ func (m Model) viewConfirm() string { } s.WriteString(boxStyle.Render(config)) + + // Show SNI DNS warning if present + if m.sniWarning != "" { + s.WriteString("\n\n") + s.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500")).Render(m.sniWarning)) + } + s.WriteString("\n\n") s.WriteString(helpStyle.Render("Press Enter to install â€ĸ Esc to go back")) return s.String() @@ -713,6 +728,9 @@ type DiscoveryResult struct { PeerID string // LibP2P peer ID IPFSPeerID string // IPFS peer ID IPFSSwarmAddrs []string // IPFS swarm addresses + // IPFS Cluster info for cluster peer discovery + IPFSClusterPeerID string // IPFS Cluster peer ID + IPFSClusterAddrs []string // IPFS Cluster multiaddresses } // discoverPeerFromDomain queries an existing node to get its peer ID and IPFS info @@ -741,7 +759,7 @@ func discoverPeerFromDomain(domain string) (*DiscoveryResult, error) { return nil, fmt.Errorf("unexpected status from %s: %s", domain, resp.Status) } - // Parse response including IPFS info + // Parse response including IPFS and IPFS Cluster info var status struct { PeerID string `json:"peer_id"` NodeID string `json:"node_id"` // fallback for backward compatibility @@ -749,6 +767,10 @@ func discoverPeerFromDomain(domain string) (*DiscoveryResult, error) { PeerID string `json:"peer_id"` SwarmAddresses []string `json:"swarm_addresses"` } `json:"ipfs,omitempty"` + IPFSCluster *struct { + PeerID string `json:"peer_id"` + Addresses []string `json:"addresses"` + } `json:"ipfs_cluster,omitempty"` } if err := json.NewDecoder(resp.Body).Decode(&status); err != nil { return nil, fmt.Errorf("failed to parse response from %s: %w", domain, err) @@ -774,6 +796,12 @@ func discoverPeerFromDomain(domain string) (*DiscoveryResult, error) { result.IPFSSwarmAddrs = status.IPFS.SwarmAddresses } + // Include IPFS Cluster info if available + if status.IPFSCluster != nil { + result.IPFSClusterPeerID = status.IPFSCluster.PeerID + result.IPFSClusterAddrs = status.IPFSCluster.Addresses + } + return result, nil } @@ -860,7 +888,8 @@ func detectPublicIP() string { // It tries to resolve the key SNI hostnames for IPFS, IPFS Cluster, and Olric // Note: Raft no longer uses SNI - it uses direct RQLite TLS on port 7002 // All should resolve to the same IP (the node's public IP or domain) -func validateSNIDNSRecords(domain string) error { +// Returns a warning string if records are missing (empty string if all OK) +func validateSNIDNSRecords(domain string) string { // List of SNI services that need DNS records // Note: raft.domain is NOT included - RQLite uses direct TLS on port 7002 sniServices := []string{ @@ -872,11 +901,12 @@ func validateSNIDNSRecords(domain string) error { // Try to resolve the main domain first to get baseline mainIPs, err := net.LookupHost(domain) if err != nil { - return fmt.Errorf("could not resolve main domain %s: %w", domain, err) + // Main domain doesn't resolve - this is just a warning now + return fmt.Sprintf("Warning: could not resolve main domain %s: %v", domain, err) } if len(mainIPs) == 0 { - return fmt.Errorf("main domain %s resolved to no IP addresses", domain) + return fmt.Sprintf("Warning: main domain %s resolved to no IP addresses", domain) } // Check each SNI service @@ -890,18 +920,15 @@ func validateSNIDNSRecords(domain string) error { if len(unresolvedServices) > 0 { serviceList := strings.Join(unresolvedServices, ", ") - return fmt.Errorf( - "SNI DNS records not found for: %s\n\n"+ - "You need to add DNS records (A records or wildcard CNAME) for these services:\n"+ - " - They should all resolve to the same IP as %s\n"+ - " - Option 1: Add individual A records pointing to %s\n"+ - " - Option 2: Add wildcard CNAME: *.%s -> %s\n\n"+ - "Without these records, multi-node clustering will fail.", - serviceList, domain, domain, domain, domain, + return fmt.Sprintf( + "âš ī¸ SNI DNS records not found for: %s\n"+ + " For multi-node clustering, add wildcard CNAME: *.%s -> %s\n"+ + " (Continuing anyway - single-node setup will work)", + serviceList, domain, domain, ) } - return nil + return "" } // Run starts the TUI installer and returns the configuration diff --git a/pkg/ipfs/cluster.go b/pkg/ipfs/cluster.go index ebae2f7..a203a58 100644 --- a/pkg/ipfs/cluster.go +++ b/pkg/ipfs/cluster.go @@ -490,21 +490,30 @@ func (cm *ClusterConfigManager) UpdateAllClusterPeers() (bool, error) { } // RepairPeerConfiguration automatically discovers and repairs peer configuration -// Tries multiple methods: config-based discovery, peer multiaddr, or discovery service +// Tries multiple methods: gateway /v1/network/status, config-based discovery, peer multiaddr func (cm *ClusterConfigManager) RepairPeerConfiguration() (bool, error) { if cm.cfg.Database.IPFS.ClusterAPIURL == "" { return false, nil // IPFS not configured } - // Skip if this is the first node (creates the cluster, no join address) + // Method 1: Try to discover cluster peers via /v1/network/status endpoint + // This is the most reliable method as it uses the HTTPS gateway + if len(cm.cfg.Discovery.BootstrapPeers) > 0 { + success, err := cm.DiscoverClusterPeersFromGateway() + if err != nil { + cm.logger.Debug("Gateway discovery failed, trying direct API", zap.Error(err)) + } else if success { + cm.logger.Info("Successfully discovered cluster peers from gateway") + return true, nil + } + } + + // Skip direct API method if this is the first node (creates the cluster, no join address) if cm.cfg.Database.RQLiteJoinAddress == "" { return false, nil } - // Method 1: Try to use peer API URL from config if available - // Check if we have a peer's cluster API URL in discovery metadata - // For now, we'll infer from peers multiaddr - + // Method 2: Try direct cluster API (fallback) var peerAPIURL string // Try to extract from peers multiaddr @@ -530,7 +539,7 @@ func (cm *ClusterConfigManager) RepairPeerConfiguration() (bool, error) { } if success { - cm.logger.Info("Successfully repaired peer configuration") + cm.logger.Info("Successfully repaired peer configuration via direct API") return true, nil } @@ -539,6 +548,162 @@ func (cm *ClusterConfigManager) RepairPeerConfiguration() (bool, error) { return false, nil } +// DiscoverClusterPeersFromGateway queries bootstrap peers' /v1/network/status endpoint +// to discover IPFS Cluster peer information and updates the local service.json +func (cm *ClusterConfigManager) DiscoverClusterPeersFromGateway() (bool, error) { + if len(cm.cfg.Discovery.BootstrapPeers) == 0 { + cm.logger.Debug("No bootstrap peers configured, skipping gateway discovery") + return false, nil + } + + var discoveredPeers []string + seenPeers := make(map[string]bool) + + for _, peerAddr := range cm.cfg.Discovery.BootstrapPeers { + // Extract domain or IP from multiaddr + domain := extractDomainFromMultiaddr(peerAddr) + if domain == "" { + continue + } + + // Query /v1/network/status endpoint + statusURL := fmt.Sprintf("https://%s/v1/network/status", domain) + cm.logger.Debug("Querying peer network status", zap.String("url", statusURL)) + + // Use TLS-aware HTTP client (handles staging certs for *.debros.network) + client := tlsutil.NewHTTPClientForDomain(10*time.Second, domain) + resp, err := client.Get(statusURL) + if err != nil { + // Try HTTP fallback + statusURL = fmt.Sprintf("http://%s/v1/network/status", domain) + resp, err = client.Get(statusURL) + if err != nil { + cm.logger.Debug("Failed to query peer status", zap.String("domain", domain), zap.Error(err)) + continue + } + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + cm.logger.Debug("Peer returned non-OK status", zap.String("domain", domain), zap.Int("status", resp.StatusCode)) + continue + } + + // Parse response + var status struct { + IPFSCluster *struct { + PeerID string `json:"peer_id"` + Addresses []string `json:"addresses"` + } `json:"ipfs_cluster"` + } + if err := json.NewDecoder(resp.Body).Decode(&status); err != nil { + cm.logger.Debug("Failed to decode peer status", zap.String("domain", domain), zap.Error(err)) + continue + } + + if status.IPFSCluster == nil || status.IPFSCluster.PeerID == "" { + cm.logger.Debug("Peer has no IPFS Cluster info", zap.String("domain", domain)) + continue + } + + // Extract IP from domain or addresses + peerIP := extractIPFromMultiaddrForCluster(peerAddr) + if peerIP == "" { + // Try to resolve domain + ips, err := net.LookupIP(domain) + if err == nil && len(ips) > 0 { + for _, ip := range ips { + if ip.To4() != nil { + peerIP = ip.String() + break + } + } + } + } + + if peerIP == "" { + cm.logger.Debug("Could not determine peer IP", zap.String("domain", domain)) + continue + } + + // Construct cluster multiaddr + // IPFS Cluster listens on port 9098 (REST API port 9094 + 4) + clusterAddr := fmt.Sprintf("/ip4/%s/tcp/9098/p2p/%s", peerIP, status.IPFSCluster.PeerID) + if !seenPeers[clusterAddr] { + discoveredPeers = append(discoveredPeers, clusterAddr) + seenPeers[clusterAddr] = true + cm.logger.Info("Discovered cluster peer from gateway", + zap.String("domain", domain), + zap.String("peer_id", status.IPFSCluster.PeerID), + zap.String("cluster_addr", clusterAddr)) + } + } + + if len(discoveredPeers) == 0 { + cm.logger.Debug("No cluster peers discovered from gateway") + return false, nil + } + + // Load current config + serviceJSONPath := filepath.Join(cm.clusterPath, "service.json") + cfg, err := cm.loadOrCreateConfig(serviceJSONPath) + if err != nil { + return false, fmt.Errorf("failed to load config: %w", err) + } + + // Update peerstore file + peerstorePath := filepath.Join(cm.clusterPath, "peerstore") + peerstoreContent := strings.Join(discoveredPeers, "\n") + "\n" + if err := os.WriteFile(peerstorePath, []byte(peerstoreContent), 0644); err != nil { + cm.logger.Warn("Failed to update peerstore file", zap.Error(err)) + } + + // Update peer_addresses in config + cfg.Cluster.PeerAddresses = discoveredPeers + + // Save config + if err := cm.saveConfig(serviceJSONPath, cfg); err != nil { + return false, fmt.Errorf("failed to save config: %w", err) + } + + cm.logger.Info("Updated cluster peer addresses from gateway discovery", + zap.Int("peer_count", len(discoveredPeers)), + zap.Strings("peer_addresses", discoveredPeers)) + + return true, nil +} + +// extractDomainFromMultiaddr extracts domain or IP from a multiaddr string +// Handles formats like /dns4/domain/tcp/port/p2p/id or /ip4/ip/tcp/port/p2p/id +func extractDomainFromMultiaddr(multiaddrStr string) string { + ma, err := multiaddr.NewMultiaddr(multiaddrStr) + if err != nil { + return "" + } + + // Try DNS4 first (domain name) + if domain, err := ma.ValueForProtocol(multiaddr.P_DNS4); err == nil && domain != "" { + return domain + } + + // Try DNS6 + if domain, err := ma.ValueForProtocol(multiaddr.P_DNS6); err == nil && domain != "" { + return domain + } + + // Try IP4 + if ip, err := ma.ValueForProtocol(multiaddr.P_IP4); err == nil && ip != "" { + return ip + } + + // Try IP6 + if ip, err := ma.ValueForProtocol(multiaddr.P_IP6); err == nil && ip != "" { + return ip + } + + return "" +} + // DiscoverClusterPeersFromLibP2P loads IPFS cluster peer addresses from the peerstore file. // If peerstore is empty, it means there are no peers to connect to. // Returns true if peers were loaded and configured, false otherwise (non-fatal) diff --git a/scripts/dev-kill-all.sh b/scripts/dev-kill-all.sh index 4dbc6a4..945696c 100755 --- a/scripts/dev-kill-all.sh +++ b/scripts/dev-kill-all.sh @@ -52,7 +52,7 @@ SPECIFIC_PATTERNS=( "ipfs daemon" "ipfs-cluster-service daemon" "olric-server" - "bin/node" + "bin/orama-node" "bin/gateway" "anyone-client" )