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
14 changes: 3 additions & 11 deletions cmd/devnet-builder/commands/manage/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,17 +293,9 @@ func runDeploy(cmd *cobra.Command, args []string) error {
if !types.NetworkSource(deployNetwork).IsValid() {
return fmt.Errorf("invalid network: %s (must be 'mainnet' or 'testnet')", deployNetwork)
}
// 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)
// Validate validator count using shared mode-aware constraints.
if err := config.ValidateValidatorCount(deployMode, deployValidators); err != nil {
return err
}

// Validate port availability for local mode before proceeding
Expand Down
39 changes: 22 additions & 17 deletions cmd/dvb/provision.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,26 @@ func detectProvisionMode(opts *provisionOptions) ProvisionMode {
return InteractiveMode
}

func validateFlagModeOptions(opts *provisionOptions) error {
// Validate required flags
if opts.name == "" {
return fmt.Errorf("--name is required in flag mode")
}
if opts.network == "" {
return fmt.Errorf("--network is required in flag mode")
}

// Validate options
if err := config.ValidateValidatorCount(opts.mode, opts.validators); err != nil {
return err
}
if opts.fullNodes < 0 {
return fmt.Errorf("--full-nodes cannot be negative")
}

return nil
}

// runInteractiveMode handles interactive wizard mode
func runInteractiveMode(ctx context.Context, opts *provisionOptions) error {
// Interactive mode requires a TTY
Expand Down Expand Up @@ -236,23 +256,8 @@ func runFlagMode(ctx context.Context, opts *provisionOptions) error {
}
}

// Validate required flags
if opts.name == "" {
return fmt.Errorf("--name is required in flag mode")
}
if opts.network == "" {
return fmt.Errorf("--network is required in flag mode")
}

// Validate options
if opts.validators < 1 {
return fmt.Errorf("--validators must be at least 1")
}
if opts.fullNodes < 0 {
return fmt.Errorf("--full-nodes cannot be negative")
}
if opts.mode != "docker" && opts.mode != "local" {
return fmt.Errorf("--mode must be 'docker' or 'local'")
if err := validateFlagModeOptions(opts); err != nil {
return err
}

// Build devnet spec
Expand Down
54 changes: 54 additions & 0 deletions cmd/dvb/provision_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,60 @@ func TestDetectProvisionMode(t *testing.T) {
}
}

func TestValidateFlagModeOptions_ValidatorBounds(t *testing.T) {
tests := []struct {
name string
opts *provisionOptions
wantErr bool
errContain string
}{
{
name: "docker allows up to 100",
opts: &provisionOptions{
name: "devnet-a",
network: "stable",
mode: "docker",
validators: 100,
},
wantErr: false,
},
{
name: "local rejects above 4",
opts: &provisionOptions{
name: "devnet-a",
network: "stable",
mode: "local",
validators: 5,
},
wantErr: true,
errContain: "1-4 for local mode",
},
{
name: "docker rejects above 100",
opts: &provisionOptions{
name: "devnet-a",
network: "stable",
mode: "docker",
validators: 101,
},
wantErr: true,
errContain: "1-100 for docker mode",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateFlagModeOptions(tt.opts)
if (err != nil) != tt.wantErr {
t.Fatalf("validateFlagModeOptions() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.errContain != "" && err != nil && !strings.Contains(err.Error(), tt.errContain) {
t.Fatalf("expected error containing %q, got %q", tt.errContain, err.Error())
}
})
}
}

func TestProvisionOptions_NoWaitFlag(t *testing.T) {
tests := []struct {
name string
Expand Down
17 changes: 13 additions & 4 deletions internal/config/interactive.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,26 +200,35 @@ func (s *InteractiveSetup) promptNetworkSource(cfg *FileConfig) (string, error)
return result, nil
}

// promptValidators prompts the user to enter validators count (1-4).
// promptValidators prompts the user to enter validators count using mode-aware bounds.
func (s *InteractiveSetup) promptValidators(cfg *FileConfig) (int, error) {
defaultValue := "4"
if cfg.Validators != nil {
defaultValue = strconv.Itoa(*cfg.Validators)
}

mode := ""
if cfg.ExecutionMode != nil {
mode = string(*cfg.ExecutionMode)
}
min, max, resolvedMode, err := ValidatorCountRangeForMode(mode)
if err != nil {
return 0, err
}

validate := func(input string) error {
val, err := strconv.Atoi(input)
if err != nil {
return fmt.Errorf("please enter a number")
}
if val < 1 || val > 4 {
return fmt.Errorf("validators must be between 1 and 4")
if err := ValidateValidatorCount(mode, val); err != nil {
return err
}
return nil
}

prompt := promptui.Prompt{
Label: "Number of validators (1-4)",
Label: fmt.Sprintf("Number of validators (%d-%d for %s mode)", min, max, resolvedMode),
Default: defaultValue,
Validate: validate,
Templates: &promptui.PromptTemplates{
Expand Down
28 changes: 16 additions & 12 deletions internal/config/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@ func (c *EffectiveConfig) Validate() error {
}
}

// Validate validators
if c.Validators.Value < 1 || c.Validators.Value > 4 {
return fmt.Errorf("invalid validators: %d (must be 1-4)", c.Validators.Value)
}

// Validate mode using canonical type validation
if !types.ExecutionMode(c.Mode.Value).IsValid() {
return fmt.Errorf("invalid mode: %s (must be 'docker' or 'local')", c.Mode.Value)
}

// Validate validators with mode-aware constraints.
if err := ValidateValidatorCount(c.Mode.Value, c.Validators.Value); err != nil {
return err
}

// Validate accounts
if c.Accounts.Value < 0 || c.Accounts.Value > 100 {
return fmt.Errorf("invalid accounts: %d (must be 0-100)", c.Accounts.Value)
Expand All @@ -56,20 +56,24 @@ func ValidateFileConfig(cfg *FileConfig) error {
// Note: BlockchainNetwork validation is deferred until modules are registered.
// This allows config loading to happen before network module init() calls.

// Validate validators if set
if cfg.Validators != nil {
if *cfg.Validators < 1 || *cfg.Validators > 4 {
return fmt.Errorf("invalid validators in config file: %d (must be 1-4)", *cfg.Validators)
}
}

// Validate mode if set using canonical type validation
if cfg.ExecutionMode != nil {
if !types.ExecutionMode(*cfg.ExecutionMode).IsValid() {
return fmt.Errorf("invalid mode in config file: %s (must be 'docker' or 'local')", *cfg.ExecutionMode)
}
}

// Validate validators if set using mode-aware constraints.
if cfg.Validators != nil {
mode := ""
if cfg.ExecutionMode != nil {
mode = string(*cfg.ExecutionMode)
}
if err := ValidateValidatorCount(mode, *cfg.Validators); err != nil {
return err
}
}

// Validate accounts if set
if cfg.Accounts != nil {
if *cfg.Accounts < 0 || *cfg.Accounts > 100 {
Expand Down
91 changes: 91 additions & 0 deletions internal/config/validate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package config

import (
"strings"
"testing"

"github.com/altuslabsxyz/devnet-builder/types"
)

func TestValidateFileConfig_ValidatorCountByMode(t *testing.T) {
docker := types.ExecutionModeDocker
local := types.ExecutionModeLocal

tests := []struct {
name string
cfg *FileConfig
wantErr bool
errContain string
}{
{
name: "docker allows 100",
cfg: &FileConfig{
ExecutionMode: &docker,
Validators: intPtr(100),
},
wantErr: false,
},
{
name: "local rejects above 4",
cfg: &FileConfig{
ExecutionMode: &local,
Validators: intPtr(5),
},
wantErr: true,
errContain: "1-4 for local mode",
},
{
name: "empty mode defaults docker",
cfg: &FileConfig{
Validators: intPtr(50),
},
wantErr: false,
},
{
name: "invalid mode rejected",
cfg: &FileConfig{
ExecutionMode: executionModePtr("k8s"),
Validators: intPtr(2),
},
wantErr: true,
errContain: "invalid mode in config file",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateFileConfig(tt.cfg)
if (err != nil) != tt.wantErr {
t.Fatalf("ValidateFileConfig() error = %v, wantErr %v", err, tt.wantErr)
}
if tt.errContain != "" && err != nil && !strings.Contains(err.Error(), tt.errContain) {
t.Fatalf("expected error to contain %q, got %q", tt.errContain, err.Error())
}
})
}
}

func TestEffectiveConfigValidate_ValidatorCountByMode(t *testing.T) {
cfg := NewEffectiveConfig("/tmp")
cfg.BlockchainNetwork = NewStringValue("")
cfg.Mode = NewStringValue("docker")
cfg.Validators = NewIntValue(100)
if err := cfg.Validate(); err != nil {
t.Fatalf("Validate() should pass for docker validators=100: %v", err)
}

cfg.Mode = NewStringValue("local")
cfg.Validators = NewIntValue(5)
if err := cfg.Validate(); err == nil {
t.Fatalf("Validate() should fail for local validators=5")
}
}

func intPtr(v int) *int {
return &v
}

func executionModePtr(v string) *types.ExecutionMode {
mode := types.ExecutionMode(v)
return &mode
}
49 changes: 49 additions & 0 deletions internal/config/validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package config

import (
"fmt"

"github.com/altuslabsxyz/devnet-builder/types"
)

const (
// ValidatorCountMin is the minimum validators count for all modes.
ValidatorCountMin = 1
// ValidatorCountMaxLocal is the maximum validators count for local mode.
ValidatorCountMaxLocal = 4
// ValidatorCountMaxDocker is the maximum validators count for docker mode.
ValidatorCountMaxDocker = 100
)

// ValidatorCountRangeForMode returns validator bounds for the given mode.
// Empty mode defaults to docker mode.
func ValidatorCountRangeForMode(mode string) (min int, max int, resolvedMode string, err error) {
m := types.ExecutionMode(mode)
if m == "" {
m = types.ExecutionModeDocker
}

switch m {
case types.ExecutionModeLocal:
return ValidatorCountMin, ValidatorCountMaxLocal, string(types.ExecutionModeLocal), nil
case types.ExecutionModeDocker:
return ValidatorCountMin, ValidatorCountMaxDocker, string(types.ExecutionModeDocker), nil
default:
return 0, 0, "", fmt.Errorf("invalid mode: %s (must be 'docker' or 'local')", mode)
}
}

// ValidateValidatorCount validates validators count using mode-aware constraints.
// Empty mode defaults to docker mode.
func ValidateValidatorCount(mode string, validators int) error {
min, max, resolvedMode, err := ValidatorCountRangeForMode(mode)
if err != nil {
return err
}

if validators < min || validators > max {
return fmt.Errorf("invalid validators: %d (must be %d-%d for %s mode)", validators, min, max, resolvedMode)
}

return nil
}
Loading
Loading