diff --git a/changelog.md b/changelog.md index da26e9b7cb..6ddc00384f 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,7 @@ ### Changes +- [#3621](/~https://github.com/ignite/cli/pull/3621) Change `pkg/availableport` to allow custom parameters in `Find` function and handle duplicated ports - [#3559](/~https://github.com/ignite/cli/pull/3559) Bump network plugin version to `v0.1.1` - [#3581](/~https://github.com/ignite/cli/pull/3581) Bump cometbft and cometbft-db in the template - [#3522](/~https://github.com/ignite/cli/pull/3522) Remove indentation from `chain serve` output diff --git a/ignite/config/chain/base/config.go b/ignite/config/chain/base/config.go index b71de4e1ef..d7900c022d 100644 --- a/ignite/config/chain/base/config.go +++ b/ignite/config/chain/base/config.go @@ -126,7 +126,7 @@ type Faucet struct { Host string `yaml:"host,omitempty"` // Port number for faucet server to listen at. - Port int `yaml:"port,omitempty"` + Port uint `yaml:"port,omitempty"` } // Init overwrites sdk configurations with given values. diff --git a/ignite/config/chain/config.go b/ignite/config/chain/config.go index e77c65c77d..b19cc7fbe3 100644 --- a/ignite/config/chain/config.go +++ b/ignite/config/chain/config.go @@ -75,7 +75,7 @@ func FaucetHost(cfg *Config) string { // We keep supporting Port option for backward compatibility // TODO: drop this option in the future host := cfg.Faucet.Host - if cfg.Faucet.Port != 0 { + if cfg.Faucet.Port != uint(0) { host = fmt.Sprintf(":%d", cfg.Faucet.Port) } diff --git a/ignite/pkg/availableport/availableport.go b/ignite/pkg/availableport/availableport.go index b5247c6caa..77f9445d63 100644 --- a/ignite/pkg/availableport/availableport.go +++ b/ignite/pkg/availableport/availableport.go @@ -4,29 +4,85 @@ import ( "fmt" "math/rand" "net" + "time" ) +type availablePortOptions struct { + randomizer *rand.Rand + minPort uint + maxPort uint +} + +type Options func(o *availablePortOptions) + +func WithRandomizer(r *rand.Rand) Options { + return func(o *availablePortOptions) { + o.randomizer = r + } +} + +func WithMaxPort(maxPort uint) Options { + return func(o *availablePortOptions) { + o.maxPort = maxPort + } +} + +func WithMinPort(minPort uint) Options { + return func(o *availablePortOptions) { + o.minPort = minPort + } +} + // Find finds n number of unused ports. // it is not guaranteed that these ports will not be allocated to // another program in the time of calling Find(). -func Find(n int) (ports []int, err error) { - min := 44000 - max := 55000 - - for i := 0; i < n; i++ { - for { - port := rand.Intn(max-min+1) + min - - conn, err := net.Dial("tcp", fmt.Sprintf(":%d", port)) - // if there is an error, this might mean that no one is listening from this port - // which is what we need. - if err == nil { - conn.Close() - continue - } - ports = append(ports, port) - break +func Find(n uint, options ...Options) (ports []uint, err error) { + // Defining them before so we can set a value depending on the AvailablePortOptions + opts := availablePortOptions{ + minPort: 44000, + maxPort: 55000, + randomizer: rand.New(rand.NewSource(time.Now().UnixNano())), + } + + for _, apply := range options { + apply(&opts) + } + // If the number of ports required is bigger than the range, this stops it + if opts.maxPort < opts.minPort { + return nil, fmt.Errorf("invalid ports range: max < min (%d < %d)", opts.maxPort, opts.minPort) + } + + // If the number of ports required is bigger than the range, this stops it + if n > (opts.maxPort - opts.minPort) { + return nil, fmt.Errorf("invalid amount of ports requested: limit is %d", opts.maxPort-opts.minPort) + } + + // Marker to point if a port is already added in the list + registered := make(map[uint]bool) + for len(registered) < int(n) { + // Greater or equal to min and lower than max + totalPorts := opts.maxPort - opts.minPort + 1 + randomPort := opts.randomizer.Intn(int(totalPorts)) + port := uint(randomPort) + opts.minPort + + conn, err := net.Dial("tcp", fmt.Sprintf(":%d", port)) + // if there is an error, this might mean that no one is listening from this port + // which is what we need. + if err == nil { + conn.Close() + continue + } + if conn != nil { + defer conn.Close() + } + + // if the port is already registered we skip it to the next one + // otherwise it's added to the ports list and pointed in our map + if registered[port] { + continue } + ports = append(ports, port) + registered[port] = true } return ports, nil } diff --git a/ignite/pkg/availableport/availableport_test.go b/ignite/pkg/availableport/availableport_test.go new file mode 100644 index 0000000000..cd42b0b6ff --- /dev/null +++ b/ignite/pkg/availableport/availableport_test.go @@ -0,0 +1,78 @@ +package availableport_test + +import ( + "fmt" + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ignite/cli/ignite/pkg/availableport" +) + +func TestFind(t *testing.T) { + tests := []struct { + name string + n uint + options []availableport.Options + err error + }{ + { + name: "test 10 ports", + n: 10, + }, + { + name: "invalid port range", + n: 10, + options: []availableport.Options{ + availableport.WithMinPort(5), + availableport.WithMaxPort(1), + }, + err: fmt.Errorf("invalid ports range: max < min (1 < 5)"), + }, + { + name: "invalid maximum port range", + n: 10, + options: []availableport.Options{ + availableport.WithMinPort(55001), + availableport.WithMaxPort(1), + }, + err: fmt.Errorf("invalid ports range: max < min (1 < 55001)"), + }, + { + name: "only invalid maximum port range", + n: 10, + options: []availableport.Options{ + availableport.WithMaxPort(43999), + }, + err: fmt.Errorf("invalid ports range: max < min (43999 < 44000)"), + }, + { + name: "with randomizer", + n: 100, + options: []availableport.Options{ + availableport.WithRandomizer(rand.New(rand.NewSource(2023))), + availableport.WithMinPort(100), + availableport.WithMaxPort(200), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := availableport.Find(tt.n, tt.options...) + if tt.err != nil { + require.Equal(t, tt.err, err) + return + } + require.NoError(t, err) + require.Len(t, got, int(tt.n)) + + seen := make(map[uint]struct{}) + for _, val := range got { + _, ok := seen[val] + require.Falsef(t, ok, "duplicated port %d", val) + seen[val] = struct{}{} + } + }) + } +} diff --git a/integration/app.go b/integration/app.go index c9c5a0396a..54516c5d7b 100644 --- a/integration/app.go +++ b/integration/app.go @@ -218,7 +218,7 @@ func (a App) RandomizeServerPorts() Hosts { ports, err := availableport.Find(7) require.NoError(a.env.t, err) - genAddr := func(port int) string { + genAddr := func(port uint) string { return fmt.Sprintf("127.0.0.1:%d", port) }