diff --git a/CHANGELOG.md b/CHANGELOG.md index 2010557..1cf1fdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,22 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant ### Deprecated ### Fixed +## [0.67.2] - 2025-11-11 + +### Added +- Added a new utility function to reliably resolve the full path of required external binaries (like ipfs, rqlited, etc.). + +### Changed +- Improved service initialization by validating the availability and path of all required external binaries before creating systemd service units. +- Updated systemd service generation logic to use the resolved, fully-qualified paths for external binaries instead of relying on hardcoded paths. + +### Deprecated + +### Removed + +### Fixed +- Changed IPFS initialization from a warning to a fatal error if the repo fails to initialize, ensuring setup stops on critical failures. + ## [0.67.1] - 2025-11-11 ### Added diff --git a/Makefile b/Makefile index 8faf0fc..d813f42 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.67.1 +VERSION := 0.67.2 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)' diff --git a/pkg/environments/production/installers.go b/pkg/environments/production/installers.go index 33416ae..d1de87e 100644 --- a/pkg/environments/production/installers.go +++ b/pkg/environments/production/installers.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" ) // BinaryInstaller handles downloading and installing external binaries @@ -153,6 +154,44 @@ func (bi *BinaryInstaller) InstallGo() error { return nil } +// ResolveBinaryPath finds the fully-qualified path to a required executable +func (bi *BinaryInstaller) ResolveBinaryPath(binary string, extraPaths ...string) (string, error) { + // First try to find in PATH + if path, err := exec.LookPath(binary); err == nil { + if abs, err := filepath.Abs(path); err == nil { + return abs, nil + } + return path, nil + } + + // Then try extra candidate paths + for _, candidate := range extraPaths { + if candidate == "" { + continue + } + if info, err := os.Stat(candidate); err == nil && !info.IsDir() && info.Mode()&0111 != 0 { + if abs, err := filepath.Abs(candidate); err == nil { + return abs, nil + } + return candidate, nil + } + } + + // Not found - generate error message + checked := make([]string, 0, len(extraPaths)) + for _, candidate := range extraPaths { + if candidate != "" { + checked = append(checked, candidate) + } + } + + if len(checked) == 0 { + return "", fmt.Errorf("required binary %q not found in path", binary) + } + + return "", fmt.Errorf("required binary %q not found in path (also checked %s)", binary, strings.Join(checked, ", ")) +} + // InstallDeBrosBinaries clones and builds DeBros binaries func (bi *BinaryInstaller) InstallDeBrosBinaries(branch string, debrosHome string) error { fmt.Fprintf(bi.logWriter.(interface{ Write([]byte) (int, error) }), " Building DeBros binaries...\n") @@ -230,8 +269,14 @@ func (bi *BinaryInstaller) InitializeIPFSRepo(nodeType, ipfsRepoPath string, swa return fmt.Errorf("failed to create IPFS repo directory: %w", err) } + // Resolve IPFS binary path + ipfsBinary, err := bi.ResolveBinaryPath("ipfs", "/usr/local/bin/ipfs", "/usr/bin/ipfs") + if err != nil { + return err + } + // Initialize IPFS with the correct repo path - cmd := exec.Command("ipfs", "init", "--profile=server", "--repo-dir="+ipfsRepoPath) + cmd := exec.Command(ipfsBinary, "init", "--profile=server", "--repo-dir="+ipfsRepoPath) if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("failed to initialize IPFS: %v\n%s", err, string(output)) } diff --git a/pkg/environments/production/orchestrator.go b/pkg/environments/production/orchestrator.go index 3d105de..06ba571 100644 --- a/pkg/environments/production/orchestrator.go +++ b/pkg/environments/production/orchestrator.go @@ -228,7 +228,7 @@ func (ps *ProductionSetup) Phase2cInitializeServices(nodeType string) error { // Initialize IPFS repo with correct path structure ipfsRepoPath := filepath.Join(dataDir, "ipfs", "repo") if err := ps.binaryInstaller.InitializeIPFSRepo(nodeType, ipfsRepoPath, filepath.Join(ps.debrosDir, "secrets", "swarm.key")); err != nil { - ps.logf(" ⚠️ IPFS initialization warning: %v", err) + return fmt.Errorf("failed to initialize IPFS repo: %w", err) } // Initialize IPFS Cluster path (just ensure directory exists, actual init happens in daemon startup) @@ -340,8 +340,26 @@ func (ps *ProductionSetup) Phase4GenerateConfigs(isBootstrap bool, bootstrapPeer func (ps *ProductionSetup) Phase5CreateSystemdServices(nodeType string, vpsIP string) error { ps.logf("Phase 5: Creating systemd services...") + // Validate all required binaries are available before creating services + ipfsBinary, err := ps.binaryInstaller.ResolveBinaryPath("ipfs", "/usr/local/bin/ipfs", "/usr/bin/ipfs") + if err != nil { + return fmt.Errorf("ipfs binary not available: %w", err) + } + clusterBinary, err := ps.binaryInstaller.ResolveBinaryPath("ipfs-cluster-service", "/usr/local/bin/ipfs-cluster-service", "/usr/bin/ipfs-cluster-service") + if err != nil { + return fmt.Errorf("ipfs-cluster-service binary not available: %w", err) + } + rqliteBinary, err := ps.binaryInstaller.ResolveBinaryPath("rqlited", "/usr/local/bin/rqlited", "/usr/bin/rqlited") + if err != nil { + return fmt.Errorf("rqlited binary not available: %w", err) + } + olricBinary, err := ps.binaryInstaller.ResolveBinaryPath("olric-server", "/usr/local/bin/olric-server", "/usr/bin/olric-server") + if err != nil { + return fmt.Errorf("olric-server binary not available: %w", err) + } + // IPFS service - ipfsUnit := ps.serviceGenerator.GenerateIPFSService(nodeType) + ipfsUnit := ps.serviceGenerator.GenerateIPFSService(nodeType, ipfsBinary) unitName := fmt.Sprintf("debros-ipfs-%s.service", nodeType) if err := ps.serviceController.WriteServiceUnit(unitName, ipfsUnit); err != nil { return fmt.Errorf("failed to write IPFS service: %w", err) @@ -349,7 +367,7 @@ func (ps *ProductionSetup) Phase5CreateSystemdServices(nodeType string, vpsIP st ps.logf(" ✓ IPFS service created: %s", unitName) // IPFS Cluster service - clusterUnit := ps.serviceGenerator.GenerateIPFSClusterService(nodeType) + clusterUnit := ps.serviceGenerator.GenerateIPFSClusterService(nodeType, clusterBinary) clusterUnitName := fmt.Sprintf("debros-ipfs-cluster-%s.service", nodeType) if err := ps.serviceController.WriteServiceUnit(clusterUnitName, clusterUnit); err != nil { return fmt.Errorf("failed to write IPFS Cluster service: %w", err) @@ -369,7 +387,7 @@ func (ps *ProductionSetup) Phase5CreateSystemdServices(nodeType string, vpsIP st } ps.logf(" RQLite will advertise: %s (advertise IP: %s)", rqliteJoinAddr, advertiseIP) - rqliteUnit := ps.serviceGenerator.GenerateRQLiteService(nodeType, 5001, 7001, rqliteJoinAddr, advertiseIP) + rqliteUnit := ps.serviceGenerator.GenerateRQLiteService(nodeType, rqliteBinary, 5001, 7001, rqliteJoinAddr, advertiseIP) rqliteUnitName := fmt.Sprintf("debros-rqlite-%s.service", nodeType) if err := ps.serviceController.WriteServiceUnit(rqliteUnitName, rqliteUnit); err != nil { return fmt.Errorf("failed to write RQLite service: %w", err) @@ -377,7 +395,7 @@ func (ps *ProductionSetup) Phase5CreateSystemdServices(nodeType string, vpsIP st ps.logf(" ✓ RQLite service created: %s", rqliteUnitName) // Olric service - olricUnit := ps.serviceGenerator.GenerateOlricService() + olricUnit := ps.serviceGenerator.GenerateOlricService(olricBinary) if err := ps.serviceController.WriteServiceUnit("debros-olric.service", olricUnit); err != nil { return fmt.Errorf("failed to write Olric service: %w", err) } diff --git a/pkg/environments/production/services.go b/pkg/environments/production/services.go index 79be077..deacb6f 100644 --- a/pkg/environments/production/services.go +++ b/pkg/environments/production/services.go @@ -23,7 +23,7 @@ func NewSystemdServiceGenerator(debrosHome, debrosDir string) *SystemdServiceGen } // GenerateIPFSService generates the IPFS daemon systemd unit -func (ssg *SystemdServiceGenerator) GenerateIPFSService(nodeType string) string { +func (ssg *SystemdServiceGenerator) GenerateIPFSService(nodeType string, ipfsBinary string) string { var ipfsRepoPath string if nodeType == "bootstrap" { ipfsRepoPath = filepath.Join(ssg.debrosDir, "data", "bootstrap", "ipfs", "repo") @@ -34,7 +34,7 @@ func (ssg *SystemdServiceGenerator) GenerateIPFSService(nodeType string) string logFile := filepath.Join(ssg.debrosDir, "logs", fmt.Sprintf("ipfs-%s.log", nodeType)) return fmt.Sprintf(`[Unit] -Description=IPFS Daemon (%s) +Description=IPFS Daemon (%[1]s) After=network-online.target Wants=network-online.target @@ -42,28 +42,28 @@ Wants=network-online.target Type=simple User=debros Group=debros -Environment=HOME=%s -Environment=IPFS_PATH=%s -ExecStartPre=/bin/bash -c 'if [ -f %s/secrets/swarm.key ] && [ ! -f %s/swarm.key ]; then cp %s/secrets/swarm.key %s/swarm.key && chmod 600 %s/swarm.key; fi' -ExecStart=/usr/bin/ipfs daemon --enable-pubsub-experiment --repo-dir=%s +Environment=HOME=%[2]s +Environment=IPFS_PATH=%[3]s +ExecStartPre=/bin/bash -c 'if [ -f %[4]s/secrets/swarm.key ] && [ ! -f %[3]s/swarm.key ]; then cp %[4]s/secrets/swarm.key %[3]s/swarm.key && chmod 600 %[3]s/swarm.key; fi' +ExecStart=%[6]s daemon --enable-pubsub-experiment --repo-dir=%[3]s Restart=always RestartSec=5 -StandardOutput=file:%s -StandardError=file:%s -SyslogIdentifier=ipfs-%s +StandardOutput=file:%[5]s +StandardError=file:%[5]s +SyslogIdentifier=ipfs-%[1]s NoNewPrivileges=yes PrivateTmp=yes ProtectSystem=strict -ReadWritePaths=%s +ReadWritePaths=%[4]s [Install] WantedBy=multi-user.target -`, nodeType, ssg.debrosHome, ipfsRepoPath, ssg.debrosDir, ipfsRepoPath, ssg.debrosDir, ipfsRepoPath, ipfsRepoPath, ipfsRepoPath, logFile, logFile, nodeType, ssg.debrosDir) +`, nodeType, ssg.debrosHome, ipfsRepoPath, ssg.debrosDir, logFile, ipfsBinary) } // GenerateIPFSClusterService generates the IPFS Cluster systemd unit -func (ssg *SystemdServiceGenerator) GenerateIPFSClusterService(nodeType string) string { +func (ssg *SystemdServiceGenerator) GenerateIPFSClusterService(nodeType string, clusterBinary string) string { var clusterPath string if nodeType == "bootstrap" { clusterPath = filepath.Join(ssg.debrosDir, "data", "bootstrap", "ipfs-cluster") @@ -74,37 +74,37 @@ func (ssg *SystemdServiceGenerator) GenerateIPFSClusterService(nodeType string) logFile := filepath.Join(ssg.debrosDir, "logs", fmt.Sprintf("ipfs-cluster-%s.log", nodeType)) return fmt.Sprintf(`[Unit] -Description=IPFS Cluster Service (%s) -After=debros-ipfs-%s.service -Wants=debros-ipfs-%s.service -Requires=debros-ipfs-%s.service +Description=IPFS Cluster Service (%[1]s) +After=debros-ipfs-%[1]s.service +Wants=debros-ipfs-%[1]s.service +Requires=debros-ipfs-%[1]s.service [Service] Type=simple User=debros Group=debros -WorkingDirectory=%s -Environment=HOME=%s -Environment=IPFS_CLUSTER_PATH=%s -ExecStart=/usr/local/bin/ipfs-cluster-service daemon +WorkingDirectory=%[2]s +Environment=HOME=%[2]s +Environment=IPFS_CLUSTER_PATH=%[3]s +ExecStart=%[6]s daemon Restart=always RestartSec=5 -StandardOutput=file:%s -StandardError=file:%s -SyslogIdentifier=ipfs-cluster-%s +StandardOutput=file:%[4]s +StandardError=file:%[4]s +SyslogIdentifier=ipfs-cluster-%[1]s NoNewPrivileges=yes PrivateTmp=yes ProtectSystem=strict -ReadWritePaths=%s +ReadWritePaths=%[5]s [Install] WantedBy=multi-user.target -`, nodeType, nodeType, nodeType, nodeType, ssg.debrosHome, ssg.debrosHome, clusterPath, logFile, logFile, nodeType, ssg.debrosDir) +`, nodeType, ssg.debrosHome, clusterPath, logFile, ssg.debrosDir, clusterBinary) } // GenerateRQLiteService generates the RQLite systemd unit -func (ssg *SystemdServiceGenerator) GenerateRQLiteService(nodeType string, httpPort, raftPort int, joinAddr string, advertiseIP string) string { +func (ssg *SystemdServiceGenerator) GenerateRQLiteService(nodeType string, rqliteBinary string, httpPort, raftPort int, joinAddr string, advertiseIP string) string { var dataDir string if nodeType == "bootstrap" { dataDir = filepath.Join(ssg.debrosDir, "data", "bootstrap", "rqlite") @@ -131,7 +131,7 @@ func (ssg *SystemdServiceGenerator) GenerateRQLiteService(nodeType string, httpP logFile := filepath.Join(ssg.debrosDir, "logs", fmt.Sprintf("rqlite-%s.log", nodeType)) return fmt.Sprintf(`[Unit] -Description=RQLite Database (%s) +Description=RQLite Database (%[1]s) After=network-online.target Wants=network-online.target @@ -139,26 +139,26 @@ Wants=network-online.target Type=simple User=debros Group=debros -Environment=HOME=%s -ExecStart=/usr/local/bin/rqlited %s +Environment=HOME=%[2]s +ExecStart=%[6]s %[3]s Restart=always RestartSec=5 -StandardOutput=file:%s -StandardError=file:%s -SyslogIdentifier=rqlite-%s +StandardOutput=file:%[4]s +StandardError=file:%[4]s +SyslogIdentifier=rqlite-%[1]s NoNewPrivileges=yes PrivateTmp=yes ProtectSystem=strict -ReadWritePaths=%s +ReadWritePaths=%[5]s [Install] WantedBy=multi-user.target -`, nodeType, ssg.debrosHome, args, logFile, logFile, nodeType, ssg.debrosDir) +`, nodeType, ssg.debrosHome, args, logFile, ssg.debrosDir, rqliteBinary) } // GenerateOlricService generates the Olric systemd unit -func (ssg *SystemdServiceGenerator) GenerateOlricService() string { +func (ssg *SystemdServiceGenerator) GenerateOlricService(olricBinary string) string { olricConfigPath := filepath.Join(ssg.debrosDir, "configs", "olric", "config.yaml") logFile := filepath.Join(ssg.debrosDir, "logs", "olric.log") @@ -171,23 +171,23 @@ Wants=network-online.target Type=simple User=debros Group=debros -Environment=HOME=%s -Environment=OLRIC_SERVER_CONFIG=%s -ExecStart=/usr/local/bin/olric-server +Environment=HOME=%[1]s +Environment=OLRIC_SERVER_CONFIG=%[2]s +ExecStart=%[5]s Restart=always RestartSec=5 -StandardOutput=file:%s -StandardError=file:%s +StandardOutput=file:%[3]s +StandardError=file:%[3]s SyslogIdentifier=olric NoNewPrivileges=yes PrivateTmp=yes ProtectSystem=strict -ReadWritePaths=%s +ReadWritePaths=%[4]s [Install] WantedBy=multi-user.target -`, ssg.debrosHome, olricConfigPath, logFile, logFile, ssg.debrosDir) +`, ssg.debrosHome, olricConfigPath, logFile, ssg.debrosDir, olricBinary) } // GenerateNodeService generates the DeBros Node systemd unit