github.com/Redstoneguy129/cli@v0.0.0-20230211220159-15dca4e91917/internal/functions/serve/serve.go (about) 1 package serve 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "path/filepath" 9 "strconv" 10 "strings" 11 12 "github.com/Redstoneguy129/cli/internal/utils" 13 "github.com/docker/docker/api/types" 14 "github.com/docker/docker/api/types/container" 15 "github.com/docker/docker/pkg/stdcopy" 16 "github.com/joho/godotenv" 17 "github.com/spf13/afero" 18 "github.com/spf13/viper" 19 ) 20 21 const ( 22 relayFuncDir = "/home/deno/functions" 23 customDockerImportMapPath = "/home/deno/import_map.json" 24 ) 25 26 func ParseEnvFile(envFilePath string) ([]string, error) { 27 env := []string{} 28 if len(envFilePath) == 0 { 29 return env, nil 30 } 31 envMap, err := godotenv.Read(envFilePath) 32 if err != nil { 33 return env, err 34 } 35 for name, value := range envMap { 36 if strings.HasPrefix(name, "SUPABASE_") { 37 return env, errors.New("Invalid env name: " + name + ". Env names cannot start with SUPABASE_.") 38 } 39 env = append(env, name+"="+value) 40 } 41 return env, nil 42 } 43 44 func Run(ctx context.Context, slug string, envFilePath string, noVerifyJWT *bool, importMapPath string, serveAll bool, fsys afero.Fs) error { 45 if serveAll { 46 return runServeAll(ctx, envFilePath, noVerifyJWT, importMapPath, fsys) 47 } 48 49 // 1. Sanity checks. 50 { 51 if err := utils.LoadConfigFS(fsys); err != nil { 52 return err 53 } 54 if err := utils.AssertSupabaseDbIsRunning(); err != nil { 55 return err 56 } 57 if err := utils.ValidateFunctionSlug(slug); err != nil { 58 return err 59 } 60 if envFilePath != "" { 61 if _, err := fsys.Stat(envFilePath); err != nil { 62 return fmt.Errorf("Failed to read env file: %w", err) 63 } 64 } 65 if importMapPath != "" { 66 // skip 67 } else if functionConfig, ok := utils.Config.Functions[slug]; ok && functionConfig.ImportMap != "" { 68 if filepath.IsAbs(functionConfig.ImportMap) { 69 importMapPath = functionConfig.ImportMap 70 } else { 71 importMapPath = filepath.Join(utils.SupabaseDirPath, functionConfig.ImportMap) 72 } 73 } else if f, err := fsys.Stat(utils.FallbackImportMapPath); err == nil && !f.IsDir() { 74 importMapPath = utils.FallbackImportMapPath 75 } 76 if importMapPath != "" { 77 if _, err := fsys.Stat(importMapPath); err != nil { 78 return fmt.Errorf("Failed to read import map: %w", err) 79 } 80 } 81 } 82 83 // 2. Parse user defined env 84 userEnv, err := ParseEnvFile(envFilePath) 85 if err != nil { 86 return err 87 } 88 89 // 3. Start relay. 90 { 91 _ = utils.Docker.ContainerRemove(ctx, utils.DenoRelayId, types.ContainerRemoveOptions{ 92 RemoveVolumes: true, 93 Force: true, 94 }) 95 96 env := []string{ 97 "JWT_SECRET=" + utils.JWTSecret, 98 "DENO_ORIGIN=http://" + utils.Config.Hostname + ":8000", 99 } 100 verifyJWTEnv := "VERIFY_JWT=true" 101 if noVerifyJWT == nil { 102 if functionConfig, ok := utils.Config.Functions[slug]; ok && !*functionConfig.VerifyJWT { 103 verifyJWTEnv = "VERIFY_JWT=false" 104 } 105 } else if *noVerifyJWT { 106 verifyJWTEnv = "VERIFY_JWT=false" 107 } 108 env = append(env, verifyJWTEnv) 109 110 cwd, err := os.Getwd() 111 if err != nil { 112 return err 113 } 114 115 binds := []string{filepath.Join(cwd, utils.FunctionsDir) + ":" + relayFuncDir + ":ro,z"} 116 // If a import map path is explcitly provided, mount it as a separate file 117 if importMapPath != "" { 118 binds = append(binds, filepath.Join(cwd, importMapPath)+":"+customDockerImportMapPath+":ro,z") 119 } 120 if _, err := utils.DockerStart( 121 ctx, 122 container.Config{ 123 Image: utils.DenoRelayImage, 124 Env: append(env, userEnv...), 125 }, 126 container.HostConfig{ 127 Binds: binds, 128 // Allows containerized functions on Linux to reach host OS 129 ExtraHosts: []string{"host.docker.internal:host-gateway"}, 130 }, 131 utils.DenoRelayId, 132 ); err != nil { 133 return err 134 } 135 136 go func() { 137 <-ctx.Done() 138 if ctx.Err() != nil { 139 utils.DockerRemove(utils.DenoRelayId) 140 } 141 }() 142 } 143 144 // 4. Start Function. 145 localFuncDir := filepath.Join(utils.FunctionsDir, slug) 146 localImportMapPath := filepath.Join(localFuncDir, "import_map.json") 147 148 // We assume the image is always Linux, so path separator must always be `/`. 149 // We can't use filepath.Join because it uses the path separator for the host system, which is `\` for Windows. 150 dockerFuncPath := relayFuncDir + "/" + slug + "/index.ts" 151 dockerImportMapPath := relayFuncDir + "/" + slug + "/import_map.json" 152 153 if importMapPath != "" { 154 localImportMapPath = importMapPath 155 dockerImportMapPath = customDockerImportMapPath 156 } 157 158 denoCacheCmd := []string{"deno", "cache"} 159 { 160 if _, err := fsys.Stat(localImportMapPath); err == nil { 161 denoCacheCmd = append(denoCacheCmd, "--import-map="+dockerImportMapPath) 162 } else if errors.Is(err, os.ErrNotExist) { 163 // skip 164 } else { 165 return fmt.Errorf("failed to check import_map.json for function %s: %w", slug, err) 166 } 167 denoCacheCmd = append(denoCacheCmd, dockerFuncPath) 168 } 169 170 fmt.Println("Starting " + utils.Bold(localFuncDir)) 171 if _, err := utils.DockerExecOnce(ctx, utils.DenoRelayId, userEnv, denoCacheCmd); err != nil { 172 return err 173 } 174 175 { 176 fmt.Println("Serving " + utils.Bold(localFuncDir)) 177 178 env := []string{ 179 "SUPABASE_URL=http://" + utils.KongId + ":8000", 180 "SUPABASE_ANON_KEY=" + utils.AnonKey, 181 "SUPABASE_SERVICE_ROLE_KEY=" + utils.ServiceRoleKey, 182 "SUPABASE_DB_URL=postgresql://postgres:postgres@" + utils.Config.Hostname + ":" + strconv.FormatUint(uint64(utils.Config.Db.Port), 10) + "/postgres", 183 } 184 185 denoRunCmd := []string{"deno", "run", "--no-check=remote", "--allow-all", "--watch", "--no-clear-screen", "--no-npm"} 186 { 187 if _, err := fsys.Stat(localImportMapPath); err == nil { 188 denoRunCmd = append(denoRunCmd, "--import-map="+dockerImportMapPath) 189 } else if errors.Is(err, os.ErrNotExist) { 190 // skip 191 } else { 192 return fmt.Errorf("failed to check index.ts for function %s: %w", slug, err) 193 } 194 denoRunCmd = append(denoRunCmd, dockerFuncPath) 195 } 196 197 exec, err := utils.Docker.ContainerExecCreate( 198 ctx, 199 utils.DenoRelayId, 200 types.ExecConfig{ 201 Env: append(env, userEnv...), 202 Cmd: denoRunCmd, 203 AttachStderr: true, 204 AttachStdout: true, 205 }, 206 ) 207 if err != nil { 208 return err 209 } 210 211 resp, err := utils.Docker.ContainerExecAttach(ctx, exec.ID, types.ExecStartCheck{}) 212 if err != nil { 213 return err 214 } 215 216 if _, err := stdcopy.StdCopy(os.Stdout, os.Stderr, resp.Reader); err != nil { 217 return err 218 } 219 } 220 221 fmt.Println("Stopped serving " + utils.Bold(localFuncDir)) 222 return nil 223 } 224 225 func runServeAll(ctx context.Context, envFilePath string, noVerifyJWT *bool, importMapPath string, fsys afero.Fs) error { 226 // 1. Sanity checks. 227 { 228 if err := utils.LoadConfigFS(fsys); err != nil { 229 return err 230 } 231 if err := utils.AssertSupabaseDbIsRunning(); err != nil { 232 return err 233 } 234 if envFilePath != "" { 235 if _, err := fsys.Stat(envFilePath); err != nil { 236 return fmt.Errorf("Failed to read env file: %w", err) 237 } 238 } 239 if importMapPath != "" { 240 // skip 241 } else if f, err := fsys.Stat(utils.FallbackImportMapPath); err == nil && !f.IsDir() { 242 importMapPath = utils.FallbackImportMapPath 243 } 244 if importMapPath != "" { 245 if _, err := fsys.Stat(importMapPath); err != nil { 246 return fmt.Errorf("Failed to read import map: %w", err) 247 } 248 } 249 } 250 251 // 2. Parse user defined env 252 userEnv, err := ParseEnvFile(envFilePath) 253 if err != nil { 254 return err 255 } 256 257 // 3. Start container 258 { 259 _ = utils.Docker.ContainerRemove(ctx, utils.DenoRelayId, types.ContainerRemoveOptions{ 260 RemoveVolumes: true, 261 Force: true, 262 }) 263 264 env := []string{ 265 "JWT_SECRET=" + utils.JWTSecret, 266 "SUPABASE_URL=http://" + utils.KongId + ":8000", 267 "SUPABASE_ANON_KEY=" + utils.AnonKey, 268 "SUPABASE_SERVICE_ROLE_KEY=" + utils.ServiceRoleKey, 269 "SUPABASE_DB_URL=postgresql://postgres:postgres@" + utils.Config.Hostname + ":" + strconv.FormatUint(uint64(utils.Config.Db.Port), 10) + "/postgres", 270 } 271 verifyJWTEnv := "VERIFY_JWT=true" 272 if noVerifyJWT != nil { 273 verifyJWTEnv = "VERIFY_JWT=false" 274 } 275 env = append(env, verifyJWTEnv) 276 277 cwd, err := os.Getwd() 278 if err != nil { 279 return err 280 } 281 282 binds := []string{ 283 filepath.Join(cwd, utils.FunctionsDir) + ":" + relayFuncDir + ":ro,z", 284 utils.DenoRelayId + ":/root/.cache/deno:rw,z", 285 } 286 // If a import map path is explcitly provided, mount it as a separate file 287 if importMapPath != "" { 288 binds = append(binds, filepath.Join(cwd, importMapPath)+":"+customDockerImportMapPath+":ro,z") 289 } 290 291 fmt.Println("Serving " + utils.Bold(utils.FunctionsDir)) 292 293 cmd := []string{"start", "--dir", relayFuncDir, "-p", "8081"} 294 if viper.GetBool("DEBUG") { 295 cmd = append(cmd, "--verbose") 296 } 297 if err := utils.DockerRunOnceWithStream( 298 ctx, 299 utils.EdgeRuntimeImage, 300 append(env, userEnv...), 301 cmd, 302 binds, 303 utils.DenoRelayId, 304 os.Stdout, 305 os.Stderr, 306 ); err != nil { 307 return err 308 } 309 } 310 311 fmt.Println("Stopped serving " + utils.Bold(utils.FunctionsDir)) 312 return nil 313 314 }