package serve import ( "context" _ "embed" "encoding/json" "fmt" "os" "path/filepath" "strconv" "strings" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" "github.com/docker/go-connections/nat" "github.com/go-errors/errors" "github.com/spf13/afero" "github.com/spf13/viper" "github.com/supabase/cli/internal/functions/deploy" "github.com/supabase/cli/internal/secrets/set" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" ) type InspectMode string const ( InspectModeRun InspectMode = "run" InspectModeBrk InspectMode = "brk" InspectModeWait InspectMode = "wait" ) func (mode InspectMode) toFlag() string { switch mode { case InspectModeBrk: return "inspect-brk" case InspectModeWait: return "inspect-wait" case InspectModeRun: fallthrough default: return "inspect" } } type RuntimeOption struct { InspectMode *InspectMode InspectMain bool } func (i *RuntimeOption) toArgs() []string { flags := []string{} if i.InspectMode != nil { flags = append(flags, fmt.Sprintf("--%s=0.0.0.0:%d", i.InspectMode.toFlag(), dockerRuntimeInspectorPort)) if i.InspectMain { flags = append(flags, "--inspect-main") } } return flags } const ( dockerRuntimeServerPort = 8081 dockerRuntimeInspectorPort = 8083 ) var ( //go:embed templates/main.ts mainFuncEmbed string ) func Run(ctx context.Context, envFilePath string, noVerifyJWT *bool, importMapPath string, runtimeOption RuntimeOption, fsys afero.Fs) error { // 1. Sanity checks. if err := flags.LoadConfig(fsys); err != nil { return err } if err := utils.AssertSupabaseDbIsRunning(); err != nil { return err } // 2. Remove existing container. _ = utils.Docker.ContainerRemove(ctx, utils.EdgeRuntimeId, container.RemoveOptions{ RemoveVolumes: true, Force: true, }) // Use network alias because Deno cannot resolve `_` in hostname dbUrl := fmt.Sprintf("postgresql://postgres:postgres@%s:5432/postgres", utils.DbAliases[0]) // 3. Serve and log to console fmt.Fprintln(os.Stderr, "Setting up Edge Functions runtime...") if err := ServeFunctions(ctx, envFilePath, noVerifyJWT, importMapPath, dbUrl, runtimeOption, fsys); err != nil { return err } if err := utils.DockerStreamLogs(ctx, utils.EdgeRuntimeId, os.Stdout, os.Stderr); err != nil { return err } fmt.Println("Stopped serving " + utils.Bold(utils.FunctionsDir)) return nil } func ServeFunctions(ctx context.Context, envFilePath string, noVerifyJWT *bool, importMapPath string, dbUrl string, runtimeOption RuntimeOption, fsys afero.Fs) error { // 1. Load default values if envFilePath == "" { if f, err := fsys.Stat(utils.FallbackEnvFilePath); err == nil && !f.IsDir() { envFilePath = utils.FallbackEnvFilePath } } else if !filepath.IsAbs(envFilePath) { envFilePath = filepath.Join(utils.CurrentDirAbs, envFilePath) } // 2. Parse user defined env env, err := parseEnvFile(envFilePath, fsys) if err != nil { return err } env = append(env, fmt.Sprintf("SUPABASE_URL=http://%s:8000", utils.KongAliases[0]), "SUPABASE_ANON_KEY="+utils.Config.Auth.AnonKey, "SUPABASE_SERVICE_ROLE_KEY="+utils.Config.Auth.ServiceRoleKey, "SUPABASE_DB_URL="+dbUrl, "SUPABASE_INTERNAL_JWT_SECRET="+utils.Config.Auth.JwtSecret, fmt.Sprintf("SUPABASE_INTERNAL_HOST_PORT=%d", utils.Config.Api.Port), ) if viper.GetBool("DEBUG") { env = append(env, "SUPABASE_INTERNAL_DEBUG=true") } if runtimeOption.InspectMode != nil { env = append(env, "SUPABASE_INTERNAL_WALLCLOCK_LIMIT_SEC=0") } // 3. Parse custom import map cwd, err := os.Getwd() if err != nil { return errors.Errorf("failed to get working directory: %w", err) } binds, functionsConfigString, err := populatePerFunctionConfigs(cwd, importMapPath, noVerifyJWT, fsys) if err != nil { return err } env = append(env, "SUPABASE_INTERNAL_FUNCTIONS_CONFIG="+functionsConfigString) // 4. Parse entrypoint script cmd := append([]string{ "edge-runtime", "start", "--main-service=/root", fmt.Sprintf("--port=%d", dockerRuntimeServerPort), fmt.Sprintf("--policy=%s", utils.Config.EdgeRuntime.Policy), }, runtimeOption.toArgs()...) if viper.GetBool("DEBUG") { cmd = append(cmd, "--verbose") } cmdString := strings.Join(cmd, " ") entrypoint := []string{"sh", "-c", `cat <<'EOF' > /root/index.ts && ` + cmdString + ` ` + mainFuncEmbed + ` EOF `} // 5. Parse exposed ports dockerRuntimePort := nat.Port(fmt.Sprintf("%d/tcp", dockerRuntimeServerPort)) exposedPorts := nat.PortSet{dockerRuntimePort: struct{}{}} portBindings := nat.PortMap{} if runtimeOption.InspectMode != nil { dockerInspectorPort := nat.Port(fmt.Sprintf("%d/tcp", dockerRuntimeInspectorPort)) exposedPorts[dockerInspectorPort] = struct{}{} portBindings[dockerInspectorPort] = []nat.PortBinding{{ HostPort: strconv.FormatUint(uint64(utils.Config.EdgeRuntime.InspectorPort), 10), }} } // 6. Start container _, err = utils.DockerStart( ctx, container.Config{ Image: utils.Config.EdgeRuntime.Image, Env: env, Entrypoint: entrypoint, ExposedPorts: exposedPorts, WorkingDir: utils.ToDockerPath(cwd), // No tcp health check because edge runtime logs them as client connection error }, container.HostConfig{ Binds: binds, PortBindings: portBindings, }, network.NetworkingConfig{ EndpointsConfig: map[string]*network.EndpointSettings{ utils.NetId: { Aliases: utils.EdgeRuntimeAliases, }, }, }, utils.EdgeRuntimeId, ) return err } func parseEnvFile(envFilePath string, fsys afero.Fs) ([]string, error) { env := []string{} if len(envFilePath) == 0 { return env, nil } envMap, err := set.ParseEnvFile(envFilePath, fsys) if err != nil { return env, err } for name, value := range envMap { if strings.HasPrefix(name, "SUPABASE_") { fmt.Fprintln(os.Stderr, "Env name cannot start with SUPABASE_, skipping: "+name) continue } env = append(env, name+"="+value) } return env, nil } func populatePerFunctionConfigs(cwd, importMapPath string, noVerifyJWT *bool, fsys afero.Fs) ([]string, string, error) { slugs, err := deploy.GetFunctionSlugs(fsys) if err != nil { return nil, "", err } functionsConfig, err := deploy.GetFunctionConfig(slugs, importMapPath, noVerifyJWT, fsys) if err != nil { return nil, "", err } binds := []string{} for slug, fc := range functionsConfig { if !fc.Enabled { fmt.Fprintln(os.Stderr, "Skipped serving Function:", slug) continue } modules, err := deploy.GetBindMounts(cwd, utils.FunctionsDir, "", fc.Entrypoint, fc.ImportMap, fsys) if err != nil { return nil, "", err } binds = append(binds, modules...) fc.ImportMap = utils.ToDockerPath(fc.ImportMap) fc.Entrypoint = utils.ToDockerPath(fc.Entrypoint) functionsConfig[slug] = fc for i, val := range fc.StaticFiles { fc.StaticFiles[i] = utils.ToDockerPath(val) } } functionsConfigBytes, err := json.Marshal(functionsConfig) if err != nil { return nil, "", errors.Errorf("failed to marshal config json: %w", err) } return utils.RemoveDuplicates(binds), string(functionsConfigBytes), nil }