diff --git a/CHANGELOG.md b/CHANGELOG.md index f8fd6b4..846d701 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,23 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant ### Deprecated ### Fixed +## [0.67.5] - 2025-11-11 + +### Added +- Added `--restart` option to `dbn prod upgrade` to automatically restart services after upgrade. +- The gateway now supports an optional `--config` flag to specify the configuration file path. + +### Changed +- Improved `dbn prod upgrade` process to better handle existing installations, including detecting node type and ensuring configurations are updated to the latest format. +- Configuration loading logic for `node` and `gateway` commands now correctly handles absolute paths passed via command line or systemd. + +### Deprecated + +### Removed + +### Fixed +- Fixed an issue during production upgrades where IPFS repositories in private swarms might fail to start due to `AutoConf` not being disabled. + ## [0.67.4] - 2025-11-11 ### Added diff --git a/Makefile b/Makefile index 224f9ab..2fdbb53 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.4 +VERSION := 0.67.5 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/cmd/gateway/config.go b/cmd/gateway/config.go index 8973963..74f6f13 100644 --- a/cmd/gateway/config.go +++ b/cmd/gateway/config.go @@ -1,6 +1,7 @@ package main import ( + "flag" "fmt" "os" "path/filepath" @@ -40,13 +41,35 @@ func getEnvBoolDefault(key string, def bool) bool { } // parseGatewayConfig loads gateway.yaml from ~/.debros exclusively. +// It accepts an optional --config flag for absolute paths (used by systemd services). func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config { + // Parse --config flag (optional, for systemd services that pass absolute paths) + configFlag := flag.String("config", "", "Config file path (absolute path or filename in ~/.debros)") + flag.Parse() + // Determine config path - configPath, err := config.DefaultPath("gateway.yaml") - if err != nil { - logger.ComponentError(logging.ComponentGeneral, "Failed to determine config path", zap.Error(err)) - fmt.Fprintf(os.Stderr, "Configuration error: %v\n", err) - os.Exit(1) + var configPath string + var err error + if *configFlag != "" { + // If --config flag is provided, use it (handles both absolute and relative paths) + if filepath.IsAbs(*configFlag) { + configPath = *configFlag + } else { + configPath, err = config.DefaultPath(*configFlag) + if err != nil { + logger.ComponentError(logging.ComponentGeneral, "Failed to determine config path", zap.Error(err)) + fmt.Fprintf(os.Stderr, "Configuration error: %v\n", err) + os.Exit(1) + } + } + } else { + // Default behavior: look for gateway.yaml in ~/.debros/configs/ or ~/.debros/ + configPath, err = config.DefaultPath("gateway.yaml") + if err != nil { + logger.ComponentError(logging.ComponentGeneral, "Failed to determine config path", zap.Error(err)) + fmt.Fprintf(os.Stderr, "Configuration error: %v\n", err) + os.Exit(1) + } } // Load YAML diff --git a/cmd/node/main.go b/cmd/node/main.go index b665101..2e1bd5a 100644 --- a/cmd/node/main.go +++ b/cmd/node/main.go @@ -245,11 +245,14 @@ func main() { select_data_dir_check(configName) // Determine config path (handle both absolute and relative paths) + // Note: select_data_dir_check already validated the path exists, so we can safely determine it here var configPath string var err error if filepath.IsAbs(*configName) { + // Absolute path passed directly (e.g., from systemd service) configPath = *configName } else { + // Relative path - use DefaultPath which checks both ~/.debros/configs/ and ~/.debros/ configPath, err = config.DefaultPath(*configName) if err != nil { logger.Error("Failed to determine config path", zap.Error(err)) diff --git a/pkg/cli/prod_commands.go b/pkg/cli/prod_commands.go index e082396..8c5c125 100644 --- a/pkg/cli/prod_commands.go +++ b/pkg/cli/prod_commands.go @@ -54,6 +54,8 @@ func showProdHelp() { fmt.Printf(" --bootstrap-join ADDR - Bootstrap raft join address (for secondary bootstrap)\n") fmt.Printf(" --domain DOMAIN - Domain for HTTPS (optional)\n") fmt.Printf(" upgrade - Upgrade existing installation (requires root/sudo)\n") + fmt.Printf(" Options:\n") + fmt.Printf(" --restart - Automatically restart services after upgrade\n") fmt.Printf(" status - Show status of production services\n") fmt.Printf(" logs - View production service logs\n") fmt.Printf(" Options:\n") @@ -188,10 +190,14 @@ func handleProdInstall(args []string) { func handleProdUpgrade(args []string) { // Parse arguments force := false + restartServices := false for _, arg := range args { if arg == "--force" { force = true } + if arg == "--restart" { + restartServices = true + } } if os.Geteuid() != 0 { @@ -201,24 +207,108 @@ func handleProdUpgrade(args []string) { debrosHome := "/home/debros" fmt.Printf("šŸ”„ Upgrading production installation...\n") - fmt.Printf(" This will preserve existing configurations and data\n\n") + fmt.Printf(" This will preserve existing configurations and data\n") + fmt.Printf(" Configurations will be updated to latest format\n\n") - // For now, just re-run the install with force flag setup := production.NewProductionSetup(debrosHome, os.Stdout, force) + // Phase 1: Check prerequisites + fmt.Printf("\nšŸ“‹ Phase 1: Checking prerequisites...\n") if err := setup.Phase1CheckPrerequisites(); err != nil { fmt.Fprintf(os.Stderr, "āŒ Prerequisites check failed: %v\n", err) os.Exit(1) } + // Phase 2: Provision environment (ensures directories exist) + fmt.Printf("\nšŸ› ļø Phase 2: Provisioning environment...\n") if err := setup.Phase2ProvisionEnvironment(); err != nil { fmt.Fprintf(os.Stderr, "āŒ Environment provisioning failed: %v\n", err) os.Exit(1) } - fmt.Printf("āœ… Upgrade complete!\n") - fmt.Printf(" Services will use existing configurations\n") - fmt.Printf(" To restart services: sudo systemctl restart debros-*\n\n") + // Phase 2b: Install/update binaries + fmt.Printf("\nPhase 2b: Installing/updating binaries...\n") + if err := setup.Phase2bInstallBinaries(); err != nil { + fmt.Fprintf(os.Stderr, "āŒ Binary installation failed: %v\n", err) + os.Exit(1) + } + + // Detect node type from existing installation + nodeType := "node" + if setup.IsUpdate() { + // Check if bootstrap config exists + bootstrapConfig := filepath.Join("/home/debros/.debros", "configs", "bootstrap.yaml") + if _, err := os.Stat(bootstrapConfig); err == nil { + nodeType = "bootstrap" + } else { + // Check data directory structure + bootstrapDataPath := filepath.Join("/home/debros/.debros", "data", "bootstrap") + if _, err := os.Stat(bootstrapDataPath); err == nil { + nodeType = "bootstrap" + } + } + fmt.Printf(" Detected node type: %s\n", nodeType) + } else { + fmt.Printf(" āš ļø No existing installation detected, treating as fresh install\n") + fmt.Printf(" Use 'dbn prod install --bootstrap' for fresh bootstrap installation\n") + nodeType = "bootstrap" // Default for upgrade if nothing exists + } + + // Phase 2c: Ensure services are properly initialized (fixes existing repos) + fmt.Printf("\nPhase 2c: Ensuring services are properly initialized...\n") + if err := setup.Phase2cInitializeServices(nodeType); err != nil { + fmt.Fprintf(os.Stderr, "āŒ Service initialization failed: %v\n", err) + os.Exit(1) + } + + // Phase 3: Ensure secrets exist (preserves existing secrets) + fmt.Printf("\nšŸ” Phase 3: Ensuring secrets...\n") + if err := setup.Phase3GenerateSecrets(nodeType == "bootstrap"); err != nil { + fmt.Fprintf(os.Stderr, "āŒ Secret generation failed: %v\n", err) + os.Exit(1) + } + + // Phase 4: Regenerate configs (updates to latest format) + // Note: This will overwrite existing configs, but preserves secrets + bootstrapPeers := []string{} // Could be read from existing config if needed + enableHTTPS := false + domain := "" + bootstrapJoin := "" + if err := setup.Phase4GenerateConfigs(nodeType == "bootstrap", bootstrapPeers, "", enableHTTPS, domain, bootstrapJoin); err != nil { + fmt.Fprintf(os.Stderr, "āš ļø Config generation warning: %v\n", err) + fmt.Fprintf(os.Stderr, " Existing configs preserved\n") + } + + // Phase 5: Update systemd services + fmt.Printf("\nšŸ”§ Phase 5: Updating systemd services...\n") + if err := setup.Phase5CreateSystemdServices(nodeType, ""); err != nil { + fmt.Fprintf(os.Stderr, "āš ļø Service update warning: %v\n", err) + } + + fmt.Printf("\nāœ… Upgrade complete!\n") + if restartServices { + fmt.Printf(" Restarting services...\n") + // Reload systemd daemon + exec.Command("systemctl", "daemon-reload").Run() + // Restart services to apply changes + services := []string{ + "debros-ipfs-bootstrap", + "debros-ipfs-cluster-bootstrap", + "debros-rqlite-bootstrap", + "debros-olric", + "debros-node-bootstrap", + "debros-gateway", + } + for _, svc := range services { + exec.Command("systemctl", "restart", svc).Run() + } + fmt.Printf(" āœ“ Services restarted\n") + } else { + fmt.Printf(" To apply changes, restart services:\n") + fmt.Printf(" sudo systemctl daemon-reload\n") + fmt.Printf(" sudo systemctl restart debros-*\n") + } + fmt.Printf("\n") } func handleProdStatus() { diff --git a/pkg/config/paths.go b/pkg/config/paths.go index ca3fd5a..249a389 100644 --- a/pkg/config/paths.go +++ b/pkg/config/paths.go @@ -30,7 +30,13 @@ func EnsureConfigDir() (string, error) { // DefaultPath returns the path to the config file for the given component name. // component should be e.g., "node.yaml", "bootstrap.yaml", "gateway.yaml" // It checks both ~/.debros/ and ~/.debros/configs/ for backward compatibility. +// If component is already an absolute path, it returns it as-is. func DefaultPath(component string) (string, error) { + // If component is already an absolute path, return it directly + if filepath.IsAbs(component) { + return component, nil + } + dir, err := ConfigDir() if err != nil { return "", err diff --git a/pkg/environments/production/installers.go b/pkg/environments/production/installers.go index d9a2b44..d4683fa 100644 --- a/pkg/environments/production/installers.go +++ b/pkg/environments/production/installers.go @@ -340,13 +340,14 @@ func (bi *BinaryInstaller) InstallSystemDependencies() error { // InitializeIPFSRepo initializes an IPFS repository for a node func (bi *BinaryInstaller) InitializeIPFSRepo(nodeType, ipfsRepoPath string, swarmKeyPath string) error { configPath := filepath.Join(ipfsRepoPath, "config") + repoExists := false if _, err := os.Stat(configPath); err == nil { - // Already initialized - return nil + repoExists = true + fmt.Fprintf(bi.logWriter.(interface{ Write([]byte) (int, error) }), " IPFS repo for %s already exists, ensuring configuration...\n", nodeType) + } else { + fmt.Fprintf(bi.logWriter.(interface{ Write([]byte) (int, error) }), " Initializing IPFS repo for %s...\n", nodeType) } - fmt.Fprintf(bi.logWriter.(interface{ Write([]byte) (int, error) }), " Initializing IPFS repo for %s...\n", nodeType) - if err := os.MkdirAll(ipfsRepoPath, 0755); err != nil { return fmt.Errorf("failed to create IPFS repo directory: %w", err) } @@ -357,25 +358,30 @@ func (bi *BinaryInstaller) InitializeIPFSRepo(nodeType, ipfsRepoPath string, swa return err } - // Initialize IPFS with the correct repo path - 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)) + // Initialize IPFS if repo doesn't exist + if !repoExists { + 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)) + } } // Copy swarm key if present swarmKeyExists := false if data, err := os.ReadFile(swarmKeyPath); err == nil { - if err := os.WriteFile(filepath.Join(ipfsRepoPath, "swarm.key"), data, 0600); err != nil { + swarmKeyDest := filepath.Join(ipfsRepoPath, "swarm.key") + if err := os.WriteFile(swarmKeyDest, data, 0600); err != nil { return fmt.Errorf("failed to copy swarm key: %w", err) } swarmKeyExists = true } - // Disable AutoConf for private swarm (required when swarm.key is present) - // This prevents IPFS from trying to use the public mainnet AutoConf service + // Always disable AutoConf for private swarm when swarm.key is present + // This is critical - IPFS will fail to start if AutoConf is enabled on a private network + // We do this even for existing repos to fix repos initialized before this fix was applied if swarmKeyExists { - cmd = exec.Command(ipfsBinary, "config", "--json", "AutoConf.Enabled", "false") + fmt.Fprintf(bi.logWriter.(interface{ Write([]byte) (int, error) }), " Disabling AutoConf for private swarm...\n") + cmd := exec.Command(ipfsBinary, "config", "--json", "AutoConf.Enabled", "false") cmd.Env = append(os.Environ(), "IPFS_PATH="+ipfsRepoPath) if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("failed to disable AutoConf: %v\n%s", err, string(output)) @@ -390,15 +396,17 @@ func (bi *BinaryInstaller) InitializeIPFSRepo(nodeType, ipfsRepoPath string, swa // InitializeIPFSClusterConfig initializes IPFS Cluster configuration // This runs `ipfs-cluster-service init` to create the service.json configuration file. +// For existing installations, it ensures the cluster secret is up to date. func (bi *BinaryInstaller) InitializeIPFSClusterConfig(nodeType, clusterPath, clusterSecret string, ipfsAPIPort int) error { serviceJSONPath := filepath.Join(clusterPath, "service.json") + configExists := false if _, err := os.Stat(serviceJSONPath); err == nil { - // Already initialized - return nil + configExists = true + fmt.Fprintf(bi.logWriter.(interface{ Write([]byte) (int, error) }), " IPFS Cluster config for %s already exists, ensuring it's up to date...\n", nodeType) + } else { + fmt.Fprintf(bi.logWriter.(interface{ Write([]byte) (int, error) }), " Preparing IPFS Cluster path for %s...\n", nodeType) } - fmt.Fprintf(bi.logWriter.(interface{ Write([]byte) (int, error) }), " Preparing IPFS Cluster path for %s...\n", nodeType) - if err := os.MkdirAll(clusterPath, 0755); err != nil { return fmt.Errorf("failed to create IPFS Cluster directory: %w", err) } @@ -412,23 +420,28 @@ func (bi *BinaryInstaller) InitializeIPFSClusterConfig(nodeType, clusterPath, cl return fmt.Errorf("ipfs-cluster-service binary not found: %w", err) } - // Initialize cluster config with ipfs-cluster-service init - // This creates the service.json file with all required sections - fmt.Fprintf(bi.logWriter.(interface{ Write([]byte) (int, error) }), " Initializing IPFS Cluster config...\n") - cmd := exec.Command(clusterBinary, "init", "--force") - cmd.Env = append(os.Environ(), "IPFS_CLUSTER_PATH="+clusterPath) - if output, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("failed to initialize IPFS Cluster config: %v\n%s", err, string(output)) + // Initialize cluster config if it doesn't exist + if !configExists { + // Initialize cluster config with ipfs-cluster-service init + // This creates the service.json file with all required sections + fmt.Fprintf(bi.logWriter.(interface{ Write([]byte) (int, error) }), " Initializing IPFS Cluster config...\n") + cmd := exec.Command(clusterBinary, "init", "--force") + cmd.Env = append(os.Environ(), "IPFS_CLUSTER_PATH="+clusterPath) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to initialize IPFS Cluster config: %v\n%s", err, string(output)) + } } - // Update the cluster secret in service.json if provided + // Always update the cluster secret (for both new and existing configs) + // This ensures existing installations get the secret synchronized if clusterSecret != "" { + fmt.Fprintf(bi.logWriter.(interface{ Write([]byte) (int, error) }), " Updating cluster secret...\n") if err := bi.updateClusterSecret(clusterPath, clusterSecret); err != nil { return fmt.Errorf("failed to update cluster secret: %w", err) } } - // Fix ownership again after init + // Fix ownership again after updates exec.Command("chown", "-R", "debros:debros", clusterPath).Run() return nil diff --git a/pkg/environments/production/orchestrator.go b/pkg/environments/production/orchestrator.go index e28b7f6..91108e9 100644 --- a/pkg/environments/production/orchestrator.go +++ b/pkg/environments/production/orchestrator.go @@ -68,6 +68,11 @@ func (ps *ProductionSetup) logf(format string, args ...interface{}) { } } +// IsUpdate detects if this is an update to an existing installation +func (ps *ProductionSetup) IsUpdate() bool { + return ps.stateDetector.IsConfigured() || ps.stateDetector.HasIPFSData() +} + // Phase1CheckPrerequisites performs initial environment validation func (ps *ProductionSetup) Phase1CheckPrerequisites() error { ps.logf("Phase 1: Checking prerequisites...") @@ -286,7 +291,12 @@ func (ps *ProductionSetup) Phase3GenerateSecrets(isBootstrap bool) error { // Phase4GenerateConfigs generates node, gateway, and service configs func (ps *ProductionSetup) Phase4GenerateConfigs(isBootstrap bool, bootstrapPeers []string, vpsIP string, enableHTTPS bool, domain string, bootstrapJoin string) error { - ps.logf("Phase 4: Generating configurations...") + if ps.IsUpdate() { + ps.logf("Phase 4: Updating configurations...") + ps.logf(" (Existing configs will be updated to latest format)") + } else { + ps.logf("Phase 4: Generating configurations...") + } // Node config nodeConfig, err := ps.configGenerator.GenerateNodeConfig(isBootstrap, bootstrapPeers, vpsIP, bootstrapJoin)