Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ docs/plans/
*.test
.serena/

devnet-builder
/devnet-builder
40 changes: 32 additions & 8 deletions cmd/devnet-builder/commands/core/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/altuslabsxyz/devnet-builder/internal/application"
"github.com/altuslabsxyz/devnet-builder/internal/application/dto"
cmdvalidation "github.com/altuslabsxyz/devnet-builder/internal/cmd/validation"
"github.com/altuslabsxyz/devnet-builder/internal/config"
"github.com/altuslabsxyz/devnet-builder/internal/infrastructure/network"
"github.com/altuslabsxyz/devnet-builder/internal/output"
Expand Down Expand Up @@ -70,7 +71,8 @@ Examples:

# After initializing, modify config then run:
devnet-builder start`,
RunE: runInit,
PreRunE: preRunInit,
RunE: runInit,
}

cmd.Flags().StringVarP(&initNetwork, "network", "n", "mainnet",
Expand All @@ -91,6 +93,28 @@ Examples:
return cmd
}

func preRunInit(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("network") {
if err := cmdvalidation.ValidateNetworkSource(initNetwork); err != nil {
return err
}
}

if cmd.Flags().Changed("mode") {
if err := cmdvalidation.ValidateMode(initMode); err != nil {
return err
}
}

if cmd.Flags().Changed("validators") {
if err := cmdvalidation.ValidateValidatorsRange(initValidators, 1, 4); err != nil {
return err
}
}

return nil
}

func runInit(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
cfg := ctxconfig.FromContext(ctx)
Expand Down Expand Up @@ -160,15 +184,15 @@ func runInit(cmd *cobra.Command, args []string) error {
initAccounts = *effectiveCfg.Accounts
}

// Validate inputs
if !types.NetworkSource(initNetwork).IsValid() {
return outputInitError(fmt.Errorf("invalid network: %s (must be 'mainnet' or 'testnet')", initNetwork), jsonMode)
// Validate inputs after config resolution.
if err := cmdvalidation.ValidateNetworkSource(initNetwork); err != nil {
return outputInitError(err, jsonMode)
}
if initValidators < 1 || initValidators > 4 {
return outputInitError(fmt.Errorf("invalid validators: %d (must be 1-4)", initValidators), jsonMode)
if err := cmdvalidation.ValidateValidatorsRange(initValidators, 1, 4); err != nil {
return outputInitError(err, jsonMode)
}
if !types.ExecutionMode(initMode).IsValid() {
return outputInitError(fmt.Errorf("invalid mode: %s (must be 'docker' or 'local')", initMode), jsonMode)
if err := cmdvalidation.ValidateMode(initMode); err != nil {
return outputInitError(err, jsonMode)
}
// Validate blockchain network module exists
if !network.Has(initBlockchainNetwork) {
Expand Down
62 changes: 47 additions & 15 deletions cmd/devnet-builder/commands/manage/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/altuslabsxyz/devnet-builder/internal/application"
"github.com/altuslabsxyz/devnet-builder/internal/application/dto"
cmdvalidation "github.com/altuslabsxyz/devnet-builder/internal/cmd/validation"
"github.com/altuslabsxyz/devnet-builder/internal/config"
"github.com/altuslabsxyz/devnet-builder/internal/di"
"github.com/altuslabsxyz/devnet-builder/internal/infrastructure/binary"
Expand Down Expand Up @@ -104,7 +105,8 @@ Examples:
# Deploy with different binary versions for export and start
# (useful when export requires a specific version for state compatibility)
devnet-builder deploy --mode local --start-version v1.2.3 --export-version v1.1.0`,
RunE: runDeploy,
PreRunE: preRunDeploy,
RunE: runDeploy,
}

// Command flags
Expand Down Expand Up @@ -147,6 +149,45 @@ Examples:
return cmd
}

func preRunDeploy(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("network") {
if err := cmdvalidation.ValidateNetworkSource(deployNetwork); err != nil {
return err
}
}

if cmd.Flags().Changed("mode") {
if err := cmdvalidation.ValidateMode(deployMode); err != nil {
return err
}
}

if cmd.Flags().Changed("validators") {
mode := resolveDeployModeForValidation(cmd)
if err := cmdvalidation.ValidateValidatorsForMode(mode, deployValidators); err != nil {
return err
}
}

return nil
}

func resolveDeployModeForValidation(cmd *cobra.Command) string {
mode := deployMode

cfg := ctxconfig.FromContext(cmd.Context())
fileCfg := cfg.FileConfig()
if fileCfg != nil && fileCfg.ExecutionMode != nil && !cmd.Flags().Changed("mode") {
mode = string(*fileCfg.ExecutionMode)
}

if envMode := os.Getenv("DEVNET_MODE"); envMode != "" && !cmd.Flags().Changed("mode") {
mode = envMode
}

return mode
}

func runDeploy(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
cfg := ctxconfig.FromContext(ctx)
Expand Down Expand Up @@ -289,21 +330,12 @@ func runDeploy(cmd *cobra.Command, args []string) error {
}
}

// Validate inputs
if !types.NetworkSource(deployNetwork).IsValid() {
return fmt.Errorf("invalid network: %s (must be 'mainnet' or 'testnet')", deployNetwork)
// Validate inputs after config resolution.
if err := cmdvalidation.ValidateNetworkSource(deployNetwork); err != nil {
return err
}
// Validate validator count based on mode
if deployMode == string(types.ExecutionModeDocker) {
if deployValidators < 1 || deployValidators > 100 {
return fmt.Errorf("invalid validators: %d (must be 1-100 for docker mode)", deployValidators)
}
} else if deployMode == string(types.ExecutionModeLocal) {
if deployValidators < 1 || deployValidators > 4 {
return fmt.Errorf("invalid validators: %d (must be 1-4 for local mode)", deployValidators)
}
} else {
return fmt.Errorf("invalid mode: %s (must be 'docker' or 'local')", deployMode)
if err := cmdvalidation.ValidateValidatorsForMode(deployMode, deployValidators); err != nil {
return err
}

// Validate port availability for local mode before proceeding
Expand Down
75 changes: 75 additions & 0 deletions cmd/devnet-builder/commands/manage/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package manage
import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/spf13/cobra"
)

func TestValidateBinaryPath_ValidExecutable(t *testing.T) {
Expand Down Expand Up @@ -130,3 +133,75 @@ func TestValidateBinaryPath_PathWithSpaces(t *testing.T) {
t.Errorf("validateBinaryPath() should return absolute path, got: %s", result)
}
}

func TestDeployPreRunInvalidModeStopsRunE(t *testing.T) {
cmd := NewDeployCmd()
cmd.SilenceErrors = true
cmd.SilenceUsage = true
cmd.SetArgs([]string{"--mode", "invalid"})

runCalled := false
cmd.RunE = func(cmd *cobra.Command, args []string) error {
runCalled = true
return nil
}

err := cmd.Execute()
if err == nil {
t.Fatalf("expected error for invalid mode")
}
if !strings.Contains(err.Error(), "invalid mode") {
t.Fatalf("unexpected error: %v", err)
}
if runCalled {
t.Fatalf("RunE should not be called when PreRunE validation fails")
}
}

func TestStartPreRunInvalidModeStopsRunE(t *testing.T) {
cmd := NewStartCmd()
cmd.SilenceErrors = true
cmd.SilenceUsage = true
cmd.SetArgs([]string{"--mode", "invalid"})

runCalled := false
cmd.RunE = func(cmd *cobra.Command, args []string) error {
runCalled = true
return nil
}

err := cmd.Execute()
if err == nil {
t.Fatalf("expected error for invalid mode")
}
if !strings.Contains(err.Error(), "invalid mode") {
t.Fatalf("unexpected error: %v", err)
}
if runCalled {
t.Fatalf("RunE should not be called when PreRunE validation fails")
}
}

func TestUpgradePreRunInvalidVotingPeriodStopsRunE(t *testing.T) {
cmd := NewUpgradeCmd()
cmd.SilenceErrors = true
cmd.SilenceUsage = true
cmd.SetArgs([]string{"--voting-period", "not-a-duration"})

runCalled := false
cmd.RunE = func(cmd *cobra.Command, args []string) error {
runCalled = true
return nil
}

err := cmd.Execute()
if err == nil {
t.Fatalf("expected error for invalid voting period")
}
if !strings.Contains(err.Error(), "invalid voting period") {
t.Fatalf("unexpected error: %v", err)
}
if runCalled {
t.Fatalf("RunE should not be called when PreRunE validation fails")
}
}
16 changes: 10 additions & 6 deletions cmd/devnet-builder/commands/manage/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/altuslabsxyz/devnet-builder/internal/application"
"github.com/altuslabsxyz/devnet-builder/internal/application/dto"
cmdvalidation "github.com/altuslabsxyz/devnet-builder/internal/cmd/validation"
"github.com/altuslabsxyz/devnet-builder/internal/output"
"github.com/altuslabsxyz/devnet-builder/types"
"github.com/altuslabsxyz/devnet-builder/types/ctxconfig"
Expand Down Expand Up @@ -60,7 +61,8 @@ Examples:

# Start with custom health timeout
devnet-builder start --health-timeout 10m`,
RunE: runStart,
PreRunE: preRunStart,
RunE: runStart,
}

cmd.Flags().StringVarP(&upMode, "mode", "m", "",
Expand All @@ -75,6 +77,13 @@ Examples:
return cmd
}

func preRunStart(cmd *cobra.Command, args []string) error {
if upMode == "" {
return nil
}
return cmdvalidation.ValidateMode(upMode)
}

func runStart(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
cfg := ctxconfig.FromContext(ctx)
Expand All @@ -94,11 +103,6 @@ func runStart(cmd *cobra.Command, args []string) error {
upStableVersion = version
}

// Validate mode if specified
if upMode != "" && !types.ExecutionMode(upMode).IsValid() {
return outputStartErrorWithMode(fmt.Errorf("invalid mode: %s (must be 'docker' or 'local')", upMode), jsonMode)
}

// Initialize service
svc, err := application.GetService(homeDir)
if err != nil {
Expand Down
34 changes: 26 additions & 8 deletions cmd/devnet-builder/commands/manage/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/altuslabsxyz/devnet-builder/internal/application"
"github.com/altuslabsxyz/devnet-builder/internal/application/dto"
"github.com/altuslabsxyz/devnet-builder/internal/application/ports"
cmdvalidation "github.com/altuslabsxyz/devnet-builder/internal/cmd/validation"
"github.com/altuslabsxyz/devnet-builder/internal/di"
"github.com/altuslabsxyz/devnet-builder/internal/infrastructure/binary"
"github.com/altuslabsxyz/devnet-builder/internal/infrastructure/cache"
Expand Down Expand Up @@ -133,7 +134,8 @@ Resume options (for interrupted upgrades):

# Resume from a specific stage (advanced)
devnet-builder upgrade --resume --resume-from SwitchingBinary`,
RunE: runUpgrade,
PreRunE: preRunUpgrade,
RunE: runUpgrade,
}

// Version selection flags
Expand Down Expand Up @@ -165,6 +167,27 @@ Resume options (for interrupted upgrades):
return cmd
}

func preRunUpgrade(cmd *cobra.Command, args []string) error {
if upgradeMode != "" {
if err := cmdvalidation.ValidateMode(upgradeMode); err != nil {
return err
}
}

if _, err := time.ParseDuration(votingPeriod); err != nil {
return fmt.Errorf("invalid voting period: %w", err)
}

if upgradeResumeFrom != "" {
targetStage := ports.ResumableStage(upgradeResumeFrom)
if !isValidStage(targetStage) {
return fmt.Errorf("invalid stage: %s", upgradeResumeFrom)
}
}

return nil
}

// UpgradeResultJSON represents the JSON output for the upgrade command.
type UpgradeResultJSON struct {
Status string `json:"status"`
Expand Down Expand Up @@ -256,13 +279,8 @@ func runUpgrade(cmd *cobra.Command, args []string) error {
resolvedMode := UpgradeExecutionMode(cleanMetadata.ExecutionMode)
modeExplicitlySet := false
if upgradeMode != "" {
switch UpgradeExecutionMode(upgradeMode) {
case UpgradeModeDocker, UpgradeModeLocal:
resolvedMode = UpgradeExecutionMode(upgradeMode)
modeExplicitlySet = true
default:
return fmt.Errorf("invalid mode %q: must be 'docker' or 'local'", upgradeMode)
}
resolvedMode = UpgradeExecutionMode(upgradeMode)
modeExplicitlySet = true
}

// Mode validation against --image/--binary flags
Expand Down
2 changes: 1 addition & 1 deletion cmd/dvb/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Examples:
newDaemonStatusCmd(),
newDaemonLogsCmd(),
newDaemonWhoAmICmd(),
newPluginsCmd(),
markDaemonRequired(newPluginsCmd()),
)

return cmd
Expand Down
9 changes: 3 additions & 6 deletions cmd/dvb/errors.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
// cmd/dvb/errors.go
package main

import "fmt"
import cmdvalidation "github.com/altuslabsxyz/devnet-builder/internal/cmd/validation"

// errDaemonNotRunning is the standard error returned when daemon connection is required but unavailable.
var errDaemonNotRunning = fmt.Errorf("daemon not running - start with: devnetd")
var errDaemonNotRunning = cmdvalidation.ErrDaemonNotRunning

// requireDaemon returns errDaemonNotRunning if the daemon client is not connected.
// Usage: if err := requireDaemon(); err != nil { return err }
func requireDaemon() error {
if daemonClient == nil {
return errDaemonNotRunning
}
return nil
return cmdvalidation.RequireDaemonConnected(daemonClient != nil)
}
4 changes: 0 additions & 4 deletions cmd/dvb/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,6 @@ Examples:
dvb get staging/my-devnet`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if err := requireDaemon(); err != nil {
return err
}

var explicitDevnet string
if len(args) > 0 {
explicitDevnet = args[0]
Expand Down
Loading
Loading