Repository: supabase/cli
Pull request #828 (2023-01-31 06:39:00)

feat: Allow serving all edge functions using edge runtime
## What kind of change does this PR introduce?

This PR introduces **experimental** `functions serve --all` option, which would use edge-runtime to serve all edge functions in a project locally.

Please note this is still experimental and missing several key features:
- `verify-jwt` option doesn't work
- `import-maps` doesn't work

TODO:

[x] Update the edge-runtime image tag


coveralls: ## Pull Request Test Coverage Report for [Build 4051485719](https://coveralls.io/builds/56441201)

* **3** of **130**   **(2.31%)**  changed or added relevant lines in **3** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage decreased (**-1.3%**) to **61.498%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [cmd/functions.go](https://coveralls.io/builds/56441201/source?filename=cmd%2Ffunctions.go#L110) | 1 | 6 | 16.67%
| [internal/utils/misc.go](https://coveralls.io/builds/56441201/source?filename=internal%2Futils%2Fmisc.go#L326) | 0 | 10 | 0.0%
| [internal/functions/serve/serve.go](https://coveralls.io/builds/56441201/source?filename=internal%2Ffunctions%2Fserve%2Fserve.go#L46) | 2 | 114 | 1.75%
<!-- | **Total:** | **3** | **130** | **2.31%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/56441201/badge)](https://coveralls.io/builds/56441201) |
| :-- | --: |
| Change from base [Build 4050603444](https://coveralls.io/builds/56437569): |  -1.3% |
| Covered Lines: | 3613 |
| Relevant Lines: | 5875 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/cmd/functions.go b/cmd/functions.go
index f7294267..772392d5 100644
--- a/cmd/functions.go
+++ b/cmd/functions.go
@@ -95,18 +95,23 @@ var (
 	}
 
 	envFilePath string
+	serveAll    bool
 
 	functionsServeCmd = &cobra.Command{
 		Use:   "serve <Function name>",
 		Short: "Serve a Function locally",
-		Args:  cobra.ExactArgs(1),
+		Args:  cobra.RangeArgs(0, 1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
 			// Fallback to config if user did not set the flag.
 			if !cmd.Flags().Changed("no-verify-jwt") {
 				noVerifyJWT = nil
 			}
-			return serve.Run(ctx, args[0], envFilePath, noVerifyJWT, importMapPath, afero.NewOsFs())
+			slug := ""
+			if len(args) > 1 {
+				slug = args[0]
+			}
+			return serve.Run(ctx, slug, envFilePath, noVerifyJWT, importMapPath, serveAll, afero.NewOsFs())
 		},
 	}
 )
@@ -120,6 +125,7 @@ func init() {
 	functionsServeCmd.Flags().BoolVar(noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.")
 	functionsServeCmd.Flags().StringVar(&envFilePath, "env-file", "", "Path to an env file to be populated to the Function environment.")
 	functionsServeCmd.Flags().StringVar(&importMapPath, "import-map", "", "Path to import map file.")
+	functionsServeCmd.Flags().BoolVar(&serveAll, "all", false, "Serve all functions (caution: Experimental feature)")
 	functionsDownloadCmd.Flags().StringVar(&projectRef, "project-ref", "", "Project ref of the Supabase project.")
 	functionsCmd.AddCommand(functionsDeleteCmd)
 	functionsCmd.AddCommand(functionsDeployCmd)
diff --git a/internal/db/diff/diff.go b/internal/db/diff/diff.go
index 655f8033..7c1691c0 100644
--- a/internal/db/diff/diff.go
+++ b/internal/db/diff/diff.go
@@ -82,6 +82,7 @@ func DiffSchema(ctx context.Context, source, target string, schema []string, p u
 			utils.DifferImage,
 			nil,
 			args,
+			nil,
 			stream.Stdout(),
 			stream.Stderr(),
 		); err != nil {
@@ -95,6 +96,7 @@ func DiffSchema(ctx context.Context, source, target string, schema []string, p u
 			utils.DifferImage,
 			nil,
 			append([]string{"--schema", s}, args...),
+			nil,
 			stream.Stdout(),
 			stream.Stderr(),
 		); err != nil {
diff --git a/internal/functions/serve/serve.go b/internal/functions/serve/serve.go
index 68795220..88f30e39 100644
--- a/internal/functions/serve/serve.go
+++ b/internal/functions/serve/serve.go
@@ -40,7 +40,11 @@ func ParseEnvFile(envFilePath string) ([]string, error) {
 	return env, nil
 }
 
-func Run(ctx context.Context, slug string, envFilePath string, noVerifyJWT *bool, importMapPath string, fsys afero.Fs) error {
+func Run(ctx context.Context, slug string, envFilePath string, noVerifyJWT *bool, importMapPath string, serveAll bool, fsys afero.Fs) error {
+	if serveAll {
+		return runServeAll(ctx, envFilePath, noVerifyJWT, importMapPath, fsys)
+	}
+
 	// 1. Sanity checks.
 	{
 		if err := utils.LoadConfigFS(fsys); err != nil {
@@ -216,3 +220,89 @@ func Run(ctx context.Context, slug string, envFilePath string, noVerifyJWT *bool
 	fmt.Println("Stopped serving " + utils.Bold(localFuncDir))
 	return nil
 }
+
+func runServeAll(ctx context.Context, envFilePath string, noVerifyJWT *bool, importMapPath string, fsys afero.Fs) error {
+	// 1. Sanity checks.
+	{
+		if err := utils.LoadConfigFS(fsys); err != nil {
+			return err
+		}
+		if err := utils.AssertSupabaseDbIsRunning(); err != nil {
+			return err
+		}
+		if envFilePath != "" {
+			if _, err := fsys.Stat(envFilePath); err != nil {
+				return fmt.Errorf("Failed to read env file: %w", err)
+			}
+		}
+		if importMapPath != "" {
+			// skip
+		} else if f, err := fsys.Stat(utils.FallbackImportMapPath); err == nil && !f.IsDir() {
+			importMapPath = utils.FallbackImportMapPath
+		}
+		if importMapPath != "" {
+			if _, err := fsys.Stat(importMapPath); err != nil {
+				return fmt.Errorf("Failed to read import map: %w", err)
+			}
+		}
+	}
+
+	// 2. Parse user defined env
+	userEnv, err := ParseEnvFile(envFilePath)
+	if err != nil {
+		return err
+	}
+
+	// 3. Start container
+	{
+		_ = utils.Docker.ContainerRemove(ctx, utils.DenoRelayId, types.ContainerRemoveOptions{
+			RemoveVolumes: true,
+			Force:         true,
+		})
+
+		env := []string{
+			"JWT_SECRET=" + utils.JWTSecret,
+			"SUPABASE_URL=http://" + utils.KongId + ":8000",
+			"SUPABASE_ANON_KEY=" + utils.AnonKey,
+			"SUPABASE_SERVICE_ROLE_KEY=" + utils.ServiceRoleKey,
+			"SUPABASE_DB_URL=postgresql://postgres:postgres@localhost:" + strconv.FormatUint(uint64(utils.Config.Db.Port), 10) + "/postgres",
+		}
+		verifyJWTEnv := "VERIFY_JWT=true"
+		if noVerifyJWT != nil {
+			verifyJWTEnv = "VERIFY_JWT=false"
+		}
+		env = append(env, verifyJWTEnv)
+
+		cwd, err := os.Getwd()
+		if err != nil {
+			return err
+		}
+
+		binds := []string{
+			filepath.Join(cwd, utils.FunctionsDir) + ":" + relayFuncDir + ":ro,z",
+			utils.DenoRelayId + ":/root/.cache/deno:rw,z",
+		}
+		// If a import map path is explcitly provided, mount it as a separate file
+		if importMapPath != "" {
+			binds = append(binds, filepath.Join(cwd, importMapPath)+":"+customDockerImportMapPath+":ro,z")
+		}
+
+		fmt.Println("Serving " + utils.Bold(utils.FunctionsDir))
+
+		if err := utils.DockerRunOnceWithStream(
+			ctx,
+			utils.EdgeRuntimeImage,
+			append(env, userEnv...),
+			[]string{"start", "--dir", relayFuncDir, "-p", "8081"},
+			binds,
+			os.Stdout,
+			os.Stderr,
+		); err != nil {
+			return err
+		}
+	}
+
+	fmt.Println("Stopped serving " + utils.Bold(utils.FunctionsDir))
+	return nil
+
+}
diff --git a/internal/utils/docker.go b/internal/utils/docker.go
index 3a35151a..428d3adb 100644
--- a/internal/utils/docker.go
+++ b/internal/utils/docker.go
@@ -302,11 +302,11 @@ func DockerRunOnce(ctx context.Context, image string, env []string, cmd []string
 		stderr = os.Stderr
 	}
 	var out bytes.Buffer
-	err := DockerRunOnceWithStream(ctx, image, env, cmd, &out, stderr)
+	err := DockerRunOnceWithStream(ctx, image, env, cmd, nil, &out, stderr)
 	return out.String(), err
 }
 
-func DockerRunOnceWithStream(ctx context.Context, image string, env []string, cmd []string, stdout, stderr io.Writer) error {
+func DockerRunOnceWithStream(ctx context.Context, image string, env, cmd, binds []string, stdout, stderr io.Writer) error {
 	// Cannot rely on docker's auto remove because
 	//   1. We must inspect exit code after container stops
 	//   2. Context cancellation may happen after start
@@ -314,7 +314,11 @@ func DockerRunOnceWithStream(ctx context.Context, image string, env []string, cm
 		Image: image,
 		Env:   env,
 		Cmd:   cmd,
-	}, container.HostConfig{}, "")
+	}, container.HostConfig{
+		Binds: binds,
+		// Allows containerized functions on Linux to reach host OS
+		ExtraHosts: []string{"host.docker.internal:host-gateway"},
+	}, "")
 	if err != nil {
 		return err
 	}
diff --git a/internal/utils/misc.go b/internal/utils/misc.go
index 4d6941f7..9bd75612 100644
--- a/internal/utils/misc.go
+++ b/internal/utils/misc.go
@@ -28,15 +28,16 @@ const (
 	Pg14Image = "supabase/postgres:14.1.0.89"
 	Pg15Image = "supabase/postgres:15.1.0.33"
 	// Append to ServiceImages when adding new dependencies below
-	KongImage       = "library/kong:2.8.1"
-	InbucketImage   = "inbucket/inbucket:3.0.3"
-	PostgrestImage  = "postgrest/postgrest:v10.1.1.20221215"
-	DifferImage     = "supabase/pgadmin-schema-diff:cli-0.0.5"
-	MigraImage      = "djrobstep/migra:3.0.1621480950"
-	PgmetaImage     = "supabase/postgres-meta:v0.60.3"
-	StudioImage     = "supabase/studio:20230127-6bfd87b"
-	DenoRelayImage  = "supabase/deno-relay:v1.5.0"
-	ImageProxyImage = "darthsim/imgproxy:v3.8.0"
+	KongImage        = "library/kong:2.8.1"
+	InbucketImage    = "inbucket/inbucket:3.0.3"
+	PostgrestImage   = "postgrest/postgrest:v10.1.1.20221215"
+	DifferImage      = "supabase/pgadmin-schema-diff:cli-0.0.5"
+	MigraImage       = "djrobstep/migra:3.0.1621480950"
+	PgmetaImage      = "supabase/postgres-meta:v0.60.3"
+	StudioImage      = "supabase/studio:20230127-6bfd87b"
+	DenoRelayImage   = "supabase/deno-relay:v1.5.0"
+	ImageProxyImage  = "darthsim/imgproxy:v3.8.0"
+	EdgeRuntimeImage = "supabase/edge-runtime:v1.0.7"
 	// Update initial schemas in internal/utils/templates/initial_schemas when
 	// updating any one of these.
 	GotrueImage   = "supabase/gotrue:v2.40.1"


Test patch:
diff --git a/internal/functions/serve/serve_test.go b/internal/functions/serve/serve_test.go
index 50cba2fb..845bbea0 100644
--- a/internal/functions/serve/serve_test.go
+++ b/internal/functions/serve/serve_test.go
@@ -35,7 +35,7 @@ func TestServeCommand(t *testing.T) {
 			Post("/v" + utils.Docker.ClientVersion() + "/containers").
 			Reply(http.StatusServiceUnavailable)
 		// Run test
-		err := Run(context.Background(), "test-func", "", nil, "", fsys)
+		err := Run(context.Background(), "test-func", "", nil, "", false, fsys)
 		// Check error
 		assert.ErrorContains(t, err, "request returned Service Unavailable for API route and version http://localhost/v1.41/containers/supabase_deno_relay_serve/exec")
 		assert.Empty(t, apitest.ListUnmatchedRequests())