github.com/supabase/cli@v1.168.1/internal/functions/serve/serve.go (about) 1 package serve 2 3 import ( 4 "context" 5 _ "embed" 6 "encoding/json" 7 "fmt" 8 "io" 9 "os" 10 "path/filepath" 11 "strings" 12 13 "github.com/docker/docker/api/types/container" 14 "github.com/docker/docker/api/types/network" 15 "github.com/docker/go-connections/nat" 16 "github.com/go-errors/errors" 17 "github.com/spf13/afero" 18 "github.com/spf13/viper" 19 "github.com/supabase/cli/internal/db/start" 20 "github.com/supabase/cli/internal/secrets/set" 21 "github.com/supabase/cli/internal/utils" 22 ) 23 24 const ( 25 // Import Map from CLI flag, i.e. --import-map, takes priority over config.toml & fallback. 26 dockerFlagImportMapPath = utils.DockerDenoDir + "/flag_import_map.json" 27 dockerFallbackImportMapPath = utils.DockerDenoDir + "/fallback_import_map.json" 28 ) 29 30 var ( 31 //go:embed templates/main.ts 32 mainFuncEmbed string 33 ) 34 35 func Run(ctx context.Context, envFilePath string, noVerifyJWT *bool, importMapPath string, fsys afero.Fs) error { 36 // 1. Sanity checks. 37 if err := utils.LoadConfigFS(fsys); err != nil { 38 return err 39 } 40 if err := utils.AssertSupabaseDbIsRunning(); err != nil { 41 return err 42 } 43 // 2. Remove existing container. 44 _ = utils.Docker.ContainerRemove(ctx, utils.EdgeRuntimeId, container.RemoveOptions{ 45 RemoveVolumes: true, 46 Force: true, 47 }) 48 // Use network alias because Deno cannot resolve `_` in hostname 49 dbUrl := "postgresql://postgres:postgres@" + utils.DbAliases[0] + ":5432/postgres" 50 // 3. Serve and log to console 51 if err := ServeFunctions(ctx, envFilePath, noVerifyJWT, importMapPath, dbUrl, os.Stderr, fsys); err != nil { 52 return err 53 } 54 if err := utils.DockerStreamLogs(ctx, utils.EdgeRuntimeId, os.Stdout, os.Stderr); err != nil { 55 return err 56 } 57 fmt.Println("Stopped serving " + utils.Bold(utils.FunctionsDir)) 58 return nil 59 } 60 61 func ServeFunctions(ctx context.Context, envFilePath string, noVerifyJWT *bool, importMapPath string, dbUrl string, w io.Writer, fsys afero.Fs) error { 62 // 1. Load default values 63 if envFilePath == "" { 64 if f, err := fsys.Stat(utils.FallbackEnvFilePath); err == nil && !f.IsDir() { 65 envFilePath = utils.FallbackEnvFilePath 66 } 67 } else if _, err := fsys.Stat(envFilePath); err != nil { 68 return errors.Errorf("Failed to read env file: %w", err) 69 } 70 cwd, err := os.Getwd() 71 if err != nil { 72 return errors.Errorf("failed to get working directory: %w", err) 73 } 74 if importMapPath != "" { 75 if !filepath.IsAbs(importMapPath) { 76 importMapPath = filepath.Join(cwd, importMapPath) 77 } 78 if _, err := fsys.Stat(importMapPath); err != nil { 79 return errors.Errorf("Failed to read import map: %w", err) 80 } 81 } 82 // 2. Parse user defined env 83 userEnv, err := parseEnvFile(envFilePath, fsys) 84 if err != nil { 85 return err 86 } 87 env := []string{ 88 "SUPABASE_URL=http://" + utils.KongAliases[0] + ":8000", 89 "SUPABASE_ANON_KEY=" + utils.Config.Auth.AnonKey, 90 "SUPABASE_SERVICE_ROLE_KEY=" + utils.Config.Auth.ServiceRoleKey, 91 "SUPABASE_DB_URL=" + dbUrl, 92 "SUPABASE_INTERNAL_JWT_SECRET=" + utils.Config.Auth.JwtSecret, 93 fmt.Sprintf("SUPABASE_INTERNAL_HOST_PORT=%d", utils.Config.Api.Port), 94 "SUPABASE_INTERNAL_FUNCTIONS_PATH=" + utils.DockerFuncDirPath, 95 } 96 if viper.GetBool("DEBUG") { 97 env = append(env, "SUPABASE_INTERNAL_DEBUG=true") 98 } 99 // 3. Parse custom import map 100 binds := []string{ 101 // Reuse deno cache directory, ie. DENO_DIR, between container restarts 102 // https://denolib.gitbook.io/guide/advanced/deno_dir-code-fetch-and-cache 103 utils.EdgeRuntimeId + ":/root/.cache/deno:rw,z", 104 filepath.Join(cwd, utils.FunctionsDir) + ":" + utils.DockerFuncDirPath + ":rw,z", 105 } 106 if importMapPath != "" { 107 modules, err := utils.BindImportMap(importMapPath, dockerFlagImportMapPath, fsys) 108 if err != nil { 109 return err 110 } 111 binds = append(binds, modules...) 112 } 113 114 fallbackImportMapPath := filepath.Join(cwd, utils.FallbackImportMapPath) 115 if exists, err := afero.Exists(fsys, fallbackImportMapPath); err != nil { 116 return errors.Errorf("Failed to read fallback import map: %w", err) 117 } else if !exists { 118 fallbackImportMapPath = utils.AbsTempImportMapPath(cwd, utils.ImportMapsDir) 119 if err := utils.WriteFile(fallbackImportMapPath, []byte(`{"imports":{}}`), fsys); err != nil { 120 return err 121 } 122 } 123 if fallbackImportMapPath != importMapPath { 124 modules, err := utils.BindImportMap(fallbackImportMapPath, dockerFallbackImportMapPath, fsys) 125 if err != nil { 126 return err 127 } 128 binds = append(binds, modules...) 129 } 130 131 if err := utils.MkdirIfNotExistFS(fsys, utils.FunctionsDir); err != nil { 132 return err 133 } 134 binds, functionsConfigString, err := populatePerFunctionConfigs(binds, importMapPath, noVerifyJWT, fsys) 135 if err != nil { 136 return err 137 } 138 env = append(env, "SUPABASE_INTERNAL_FUNCTIONS_CONFIG="+functionsConfigString) 139 140 // 4. Start container 141 fmt.Fprintln(w, "Setting up Edge Functions runtime...") 142 143 var cmdString string 144 { 145 cmd := []string{"edge-runtime", "start", "--main-service", "/home/deno/main", "-p", "8081"} 146 if viper.GetBool("DEBUG") { 147 cmd = append(cmd, "--verbose") 148 } 149 cmdString = strings.Join(cmd, " ") 150 } 151 152 entrypoint := []string{"sh", "-c", `mkdir -p /home/deno/main && cat <<'EOF' > /home/deno/main/index.ts && ` + cmdString + ` 153 ` + mainFuncEmbed + ` 154 EOF 155 `} 156 _, err = utils.DockerStart( 157 ctx, 158 container.Config{ 159 Image: utils.EdgeRuntimeImage, 160 Env: append(env, userEnv...), 161 Entrypoint: entrypoint, 162 ExposedPorts: nat.PortSet{"8081/tcp": {}}, 163 // No tcp health check because edge runtime logs them as client connection error 164 }, 165 start.WithSyslogConfig(container.HostConfig{ 166 Binds: binds, 167 ExtraHosts: []string{"host.docker.internal:host-gateway"}, 168 }), 169 network.NetworkingConfig{ 170 EndpointsConfig: map[string]*network.EndpointSettings{ 171 utils.NetId: { 172 Aliases: utils.EdgeRuntimeAliases, 173 }, 174 }, 175 }, 176 utils.EdgeRuntimeId, 177 ) 178 return err 179 } 180 181 func parseEnvFile(envFilePath string, fsys afero.Fs) ([]string, error) { 182 env := []string{} 183 if len(envFilePath) == 0 { 184 return env, nil 185 } 186 envMap, err := set.ParseEnvFile(envFilePath, fsys) 187 if err != nil { 188 return env, err 189 } 190 for name, value := range envMap { 191 if strings.HasPrefix(name, "SUPABASE_") { 192 fmt.Fprintln(os.Stderr, "Env name cannot start with SUPABASE_, skipping: "+name) 193 continue 194 } 195 env = append(env, name+"="+value) 196 } 197 return env, nil 198 } 199 200 func populatePerFunctionConfigs(binds []string, importMapPath string, noVerifyJWT *bool, fsys afero.Fs) ([]string, string, error) { 201 type functionConfig struct { 202 ImportMapPath string `json:"importMapPath"` 203 VerifyJWT bool `json:"verifyJWT"` 204 } 205 206 functionsConfig := map[string]functionConfig{} 207 208 cwd, err := os.Getwd() 209 if err != nil { 210 return nil, "", errors.Errorf("failed to get working directory: %w", err) 211 } 212 213 functions, err := afero.ReadDir(fsys, utils.FunctionsDir) 214 if err != nil { 215 return nil, "", errors.Errorf("failed to read directory: %w", err) 216 } 217 for _, function := range functions { 218 if !function.IsDir() { 219 continue 220 } 221 222 functionName := function.Name() 223 if !utils.FuncSlugPattern.MatchString(functionName) { 224 continue 225 } 226 227 // CLI flags take priority over config.toml. 228 229 dockerImportMapPath := dockerFallbackImportMapPath 230 if importMapPath != "" { 231 dockerImportMapPath = dockerFlagImportMapPath 232 } else if functionConfig, ok := utils.Config.Functions[functionName]; ok && functionConfig.ImportMap != "" { 233 dockerImportMapPath = "/home/deno/import_maps/" + functionName + "/import_map.json" 234 hostImportMapPath := filepath.Join(cwd, utils.SupabaseDirPath, functionConfig.ImportMap) 235 modules, err := utils.BindImportMap(hostImportMapPath, dockerImportMapPath, fsys) 236 if err != nil { 237 return nil, "", err 238 } 239 binds = append(binds, modules...) 240 } 241 242 verifyJWT := true 243 if noVerifyJWT != nil { 244 verifyJWT = !*noVerifyJWT 245 } else if functionConfig, ok := utils.Config.Functions[functionName]; ok && functionConfig.VerifyJWT != nil { 246 verifyJWT = *functionConfig.VerifyJWT 247 } 248 249 functionsConfig[functionName] = functionConfig{ 250 ImportMapPath: dockerImportMapPath, 251 VerifyJWT: verifyJWT, 252 } 253 } 254 255 functionsConfigBytes, err := json.Marshal(functionsConfig) 256 if err != nil { 257 return nil, "", errors.Errorf("failed to marshal config json: %w", err) 258 } 259 260 return binds, string(functionsConfigBytes), nil 261 }