From 04f14c8478b2697022d07cd63f2e017c14e28d4d Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Tue, 2 Jun 2026 11:35:59 +0100 Subject: [PATCH] fix(cli): bind a free port for edge-runtime diff containers The schema diff path runs `edge-runtime start` to execute one-shot scripts (migra, pg-delta, pgcache) with host networking but without a `--port` flag, so the runtime's HTTP listener bound the edge-runtime default port directly in the host network namespace. When that port was already in use (a leftover diff container, the local stack, or anything else on the port) the bind failed with "Address already in use (os error 98)" and the container exited 1, surfacing as `error diffing schema: error running container: exit 1`. Allocate a free host port and pass it as `--port` via a shared `EdgeRuntimeStartCmd` helper used by both `RunEdgeRuntimeScript` and `diffWithStream`, so concurrent or leftover one-shot containers no longer collide on the default port. Fixes #5407 --- apps/cli-go/internal/db/diff/diff.go | 2 +- apps/cli-go/internal/utils/edgeruntime.go | 29 +++++++++++- .../cli-go/internal/utils/edgeruntime_test.go | 47 +++++++++++++++++++ 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 apps/cli-go/internal/utils/edgeruntime_test.go diff --git a/apps/cli-go/internal/db/diff/diff.go b/apps/cli-go/internal/db/diff/diff.go index 05991423d6..46d465aff2 100644 --- a/apps/cli-go/internal/db/diff/diff.go +++ b/apps/cli-go/internal/db/diff/diff.go @@ -230,7 +230,7 @@ func migrateBaseDatabase(ctx context.Context, config pgconn.Config, migrations [ } func diffWithStream(ctx context.Context, env []string, script string, stdout io.Writer) error { - cmd := []string{"edge-runtime", "start", "--main-service=."} + cmd := utils.EdgeRuntimeStartCmd() if viper.GetBool("DEBUG") { cmd = append(cmd, "--verbose") } diff --git a/apps/cli-go/internal/utils/edgeruntime.go b/apps/cli-go/internal/utils/edgeruntime.go index 06a42b464c..d7070f3f9e 100644 --- a/apps/cli-go/internal/utils/edgeruntime.go +++ b/apps/cli-go/internal/utils/edgeruntime.go @@ -3,6 +3,8 @@ package utils import ( "bytes" "context" + "fmt" + "net" "strings" "github.com/docker/docker/api/types/container" @@ -11,10 +13,35 @@ import ( "github.com/spf13/viper" ) +// getFreeHostPort asks the OS for an unused TCP port on the host. +func getFreeHostPort() (int, error) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return 0, errors.Errorf("failed to allocate free port: %w", err) + } + defer listener.Close() + return listener.Addr().(*net.TCPAddr).Port, nil +} + +// EdgeRuntimeStartCmd builds the base command for launching a one-shot Edge +// Runtime script. The runtime's HTTP listener is bound to a free host port so +// concurrent or leftover containers (which share the host network namespace +// because diff containers run with NetworkMode=host) don't collide on the +// edge-runtime default port, which surfaces as "Address already in use (os +// error 98)". See https://github.com/supabase/cli/issues/5407. +func EdgeRuntimeStartCmd() []string { + cmd := []string{"edge-runtime", "start", "--main-service=."} + // Skip the flag on the rare allocation failure to preserve prior behavior. + if port, err := getFreeHostPort(); err == nil { + cmd = append(cmd, fmt.Sprintf("--port=%d", port)) + } + return cmd +} + // RunEdgeRuntimeScript executes a TypeScript program inside the configured Edge // Runtime container and streams stdout/stderr back to the caller. func RunEdgeRuntimeScript(ctx context.Context, env []string, script string, binds []string, errPrefix string, stdout, stderr *bytes.Buffer) error { - cmd := []string{"edge-runtime", "start", "--main-service=."} + cmd := EdgeRuntimeStartCmd() if viper.GetBool("DEBUG") { cmd = append(cmd, "--verbose") } diff --git a/apps/cli-go/internal/utils/edgeruntime_test.go b/apps/cli-go/internal/utils/edgeruntime_test.go new file mode 100644 index 0000000000..8bed5b0974 --- /dev/null +++ b/apps/cli-go/internal/utils/edgeruntime_test.go @@ -0,0 +1,47 @@ +package utils + +import ( + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEdgeRuntimeStartCmd(t *testing.T) { + t.Run("binds an explicit free port", func(t *testing.T) { + cmd := EdgeRuntimeStartCmd() + // Base command must always be present. + assert.Equal(t, []string{"edge-runtime", "start", "--main-service=."}, cmd[:3]) + // A --port flag avoids collisions on the edge-runtime default port (#5407). + var portFlag string + for _, arg := range cmd { + if strings.HasPrefix(arg, "--port=") { + portFlag = arg + } + } + require.NotEmpty(t, portFlag, "expected a --port flag to be set") + port, err := strconv.Atoi(strings.TrimPrefix(portFlag, "--port=")) + require.NoError(t, err) + assert.Greater(t, port, 0) + assert.LessOrEqual(t, port, 65535) + }) + + t.Run("allocates a distinct port per invocation", func(t *testing.T) { + first := getPortArg(t, EdgeRuntimeStartCmd()) + second := getPortArg(t, EdgeRuntimeStartCmd()) + assert.NotEqual(t, first, second) + }) +} + +func getPortArg(t *testing.T, cmd []string) string { + t.Helper() + for _, arg := range cmd { + if strings.HasPrefix(arg, "--port=") { + return arg + } + } + require.FailNow(t, "missing --port flag") + return "" +}