diff --git a/internal/boxcli/services.go b/internal/boxcli/services.go index e1eb493afe1..372a2b669d8 100644 --- a/internal/boxcli/services.go +++ b/internal/boxcli/services.go @@ -20,6 +20,7 @@ type serviceUpFlags struct { background bool processComposeFile string processComposeFlags []string + pcport int } type serviceStopFlags struct { @@ -38,6 +39,8 @@ func (flags *serviceUpFlags) register(cmd *cobra.Command) { &flags.background, "background", "b", false, "run service in background") cmd.Flags().StringArrayVar( &flags.processComposeFlags, "pcflags", []string{}, "pass flags directly to process compose") + cmd.Flags().IntVarP( + &flags.pcport, "pcport", "p", 0, "specify the port for process-compose to use. You can also set the pcport by exporting DEVBOX_PC_PORT_NUM") } func (flags *serviceStopFlags) register(cmd *cobra.Command) { @@ -245,6 +248,10 @@ func startProcessManager( return err } + if flags.pcport < 0 { + return errors.Errorf("invalid pcport %d: ports cannot be less than 0", flags.pcport) + } + box, err := devbox.Open(&devopt.Opts{ Dir: servicesFlags.config.path, Env: env, @@ -261,8 +268,9 @@ func startProcessManager( servicesFlags.runInCurrentShell, args, devopt.ProcessComposeOpts{ - Background: flags.background, - ExtraFlags: flags.processComposeFlags, + Background: flags.background, + ExtraFlags: flags.processComposeFlags, + ProcessComposePort: flags.pcport, }, ) } diff --git a/internal/devbox/devopt/devboxopts.go b/internal/devbox/devopt/devboxopts.go index 8e4ec45c5b4..df47b1c2174 100644 --- a/internal/devbox/devopt/devboxopts.go +++ b/internal/devbox/devopt/devboxopts.go @@ -18,8 +18,9 @@ type Opts struct { } type ProcessComposeOpts struct { - ExtraFlags []string - Background bool + ExtraFlags []string + Background bool + ProcessComposePort int } type GenerateOpts struct { diff --git a/internal/devbox/services.go b/internal/devbox/services.go index ead19ded6e9..ff3585fa591 100644 --- a/internal/devbox/services.go +++ b/internal/devbox/services.go @@ -3,6 +3,7 @@ package devbox import ( "context" "fmt" + "strconv" "text/tabwriter" "go.jetpack.io/devbox/internal/boxcli/usererr" @@ -217,6 +218,9 @@ func (d *Devbox) StartProcessManager( for _, flag := range processComposeOpts.ExtraFlags { args = append(args, "--pcflags", flag) } + if processComposeOpts.ProcessComposePort != 0 { + args = append(args, "--pcport", strconv.Itoa(processComposeOpts.ProcessComposePort)) + } return d.runDevboxServicesScript(ctx, args) } @@ -254,9 +258,10 @@ func (d *Devbox) StartProcessManager( svcs, d.projectDir, services.ProcessComposeOpts{ - BinPath: processComposeBinPath, - Background: processComposeOpts.Background, - ExtraFlags: processComposeOpts.ExtraFlags, + BinPath: processComposeBinPath, + Background: processComposeOpts.Background, + ExtraFlags: processComposeOpts.ExtraFlags, + ProcessComposePort: processComposeOpts.ProcessComposePort, }, ) } diff --git a/internal/services/manager.go b/internal/services/manager.go index df38f4c4bf6..7a71d18ac5d 100644 --- a/internal/services/manager.go +++ b/internal/services/manager.go @@ -41,9 +41,10 @@ type globalProcessComposeConfig struct { } type ProcessComposeOpts struct { - BinPath string - ExtraFlags []string - Background bool + BinPath string + ExtraFlags []string + Background bool + ProcessComposePort int } func newGlobalProcessComposeConfig() *globalProcessComposeConfig { @@ -128,10 +129,9 @@ func StartProcessManager( config := readGlobalProcessComposeJSON(configFile) config.File = configFile - // Get the port to use for this project - port, err := getAvailablePort() + port, err := selectPort(processComposeConfig.ProcessComposePort) if err != nil { - return err + return fmt.Errorf("failed to select port: %v", err) } // Start building the process-compose command diff --git a/internal/services/ports.go b/internal/services/ports.go index 1593576ee49..b03f9792285 100644 --- a/internal/services/ports.go +++ b/internal/services/ports.go @@ -1,7 +1,10 @@ package services import ( + "fmt" "net" + "os" + "strconv" "github.com/pkg/errors" ) @@ -33,17 +36,11 @@ var disallowedPorts = map[int]string{ func getAvailablePort() (int, error) { get := func() (int, error) { - addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + port, err := isPortAvailable(0) if err != nil { return 0, errors.WithStack(err) } - - l, err := net.ListenTCP("tcp", addr) - if err != nil { - return 0, errors.WithStack(err) - } - defer l.Close() - return l.Addr().(*net.TCPAddr).Port, nil + return port, nil } for range 1000 { @@ -60,6 +57,34 @@ func getAvailablePort() (int, error) { return 0, errors.New("no available port") } +func selectPort(configPort int) (int, error) { + if configPort != 0 { + return isPortAvailable(configPort) + } + + if portStr, exists := os.LookupEnv("DEVBOX_PC_PORT_NUM"); exists { + port, err := strconv.Atoi(portStr) + if err != nil { + return 0, fmt.Errorf("invalid DEVBOX_PC_PORT_NUM environment variable: %v", err) + } + if port <= 0 { + return 0, fmt.Errorf("invalid DEVBOX_PC_PORT_NUM environment variable: ports cannot be less than 0") + } + return isPortAvailable(port) + } + + return getAvailablePort() +} + func isAllowed(port int) bool { return port > 1024 && disallowedPorts[port] == "" } + +func isPortAvailable(port int) (int, error) { + ln, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port)) + if err != nil { + return 0, fmt.Errorf("port %d is already in use", port) + } + defer ln.Close() + return ln.Addr().(*net.TCPAddr).Port, nil +}