github.com/supabase/cli@v1.168.1/internal/functions/deploy/deploy.go (about) 1 package deploy 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "net/http" 9 "os" 10 "path" 11 "path/filepath" 12 13 "github.com/andybalholm/brotli" 14 "github.com/cenkalti/backoff/v4" 15 "github.com/docker/docker/api/types/container" 16 "github.com/docker/docker/api/types/network" 17 "github.com/docker/go-units" 18 "github.com/go-errors/errors" 19 "github.com/spf13/afero" 20 "github.com/spf13/viper" 21 "github.com/supabase/cli/internal/db/start" 22 "github.com/supabase/cli/internal/utils" 23 "github.com/supabase/cli/pkg/api" 24 ) 25 26 const ( 27 eszipContentType = "application/vnd.denoland.eszip" 28 compressedEszipMagicId = "EZBR" 29 30 // Import Map from CLI flag, i.e. --import-map, takes priority over config.toml & fallback. 31 dockerImportMapPath = utils.DockerDenoDir + "/import_map.json" 32 dockerOutputDir = "/root/eszips" 33 ) 34 35 func Run(ctx context.Context, slugs []string, projectRef string, noVerifyJWT *bool, importMapPath string, fsys afero.Fs) error { 36 // Load function config and project id 37 if err := utils.LoadConfigFS(fsys); err != nil { 38 return err 39 } 40 if len(slugs) == 0 { 41 allSlugs, err := getFunctionSlugs(fsys) 42 if err != nil { 43 return err 44 } 45 slugs = allSlugs 46 } else { 47 for _, slug := range slugs { 48 if err := utils.ValidateFunctionSlug(slug); err != nil { 49 return err 50 } 51 } 52 } 53 if len(slugs) == 0 { 54 return errors.New("No Functions specified or found in " + utils.Bold(utils.FunctionsDir)) 55 } 56 return deployAll(ctx, slugs, projectRef, importMapPath, noVerifyJWT, fsys) 57 } 58 59 func RunDefault(ctx context.Context, projectRef string, fsys afero.Fs) error { 60 slugs, err := getFunctionSlugs(fsys) 61 if len(slugs) == 0 { 62 return err 63 } 64 return deployAll(ctx, slugs, projectRef, "", nil, fsys) 65 } 66 67 func getFunctionSlugs(fsys afero.Fs) ([]string, error) { 68 pattern := filepath.Join(utils.FunctionsDir, "*", "index.ts") 69 paths, err := afero.Glob(fsys, pattern) 70 if err != nil { 71 return nil, errors.Errorf("failed to glob function slugs: %w", err) 72 } 73 var slugs []string 74 for _, path := range paths { 75 slug := filepath.Base(filepath.Dir(path)) 76 if utils.FuncSlugPattern.MatchString(slug) { 77 slugs = append(slugs, slug) 78 } 79 } 80 return slugs, nil 81 } 82 83 func bundleFunction(ctx context.Context, slug, dockerEntrypointPath, importMapPath string, fsys afero.Fs) (*bytes.Buffer, error) { 84 cwd, err := os.Getwd() 85 if err != nil { 86 return nil, errors.Errorf("failed to get working directory: %w", err) 87 } 88 89 // Create temp directory to store generated eszip 90 hostOutputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug)) 91 // BitBucket pipelines require docker bind mounts to be world writable 92 if err := fsys.MkdirAll(hostOutputDir, 0777); err != nil { 93 return nil, errors.Errorf("failed to mkdir: %w", err) 94 } 95 defer func() { 96 if err := fsys.RemoveAll(hostOutputDir); err != nil { 97 fmt.Fprintln(os.Stderr, err) 98 } 99 }() 100 101 outputPath := dockerOutputDir + "/output.eszip" 102 binds := []string{ 103 // Reuse deno cache directory, ie. DENO_DIR, between container restarts 104 // https://denolib.gitbook.io/guide/advanced/deno_dir-code-fetch-and-cache 105 utils.EdgeRuntimeId + ":/root/.cache/deno:rw,z", 106 filepath.Join(cwd, utils.FunctionsDir) + ":" + utils.DockerFuncDirPath + ":ro,z", 107 filepath.Join(cwd, hostOutputDir) + ":" + dockerOutputDir + ":rw,z", 108 } 109 110 cmd := []string{"bundle", "--entrypoint", dockerEntrypointPath, "--output", outputPath} 111 if viper.GetBool("DEBUG") { 112 cmd = append(cmd, "--verbose") 113 } 114 115 if importMapPath != "" { 116 modules, err := utils.BindImportMap(importMapPath, dockerImportMapPath, fsys) 117 if err != nil { 118 return nil, err 119 } 120 binds = append(binds, modules...) 121 cmd = append(cmd, "--import-map", dockerImportMapPath) 122 } 123 124 err = utils.DockerRunOnceWithConfig( 125 ctx, 126 container.Config{ 127 Image: utils.EdgeRuntimeImage, 128 Env: []string{}, 129 Cmd: cmd, 130 }, 131 start.WithSyslogConfig(container.HostConfig{ 132 Binds: binds, 133 ExtraHosts: []string{"host.docker.internal:host-gateway"}, 134 }), 135 network.NetworkingConfig{}, 136 "", 137 os.Stdout, 138 os.Stderr, 139 ) 140 if err != nil { 141 return nil, err 142 } 143 144 eszipBytes, err := fsys.Open(filepath.Join(hostOutputDir, "output.eszip")) 145 if err != nil { 146 return nil, errors.Errorf("failed to open eszip: %w", err) 147 } 148 defer eszipBytes.Close() 149 150 compressedBuf := bytes.NewBufferString(compressedEszipMagicId) 151 brw := brotli.NewWriter(compressedBuf) 152 defer brw.Close() 153 154 _, err = io.Copy(brw, eszipBytes) 155 if err != nil { 156 return nil, errors.Errorf("failed to compress brotli: %w", err) 157 } 158 159 return compressedBuf, nil 160 } 161 162 func deployFunction(ctx context.Context, projectRef, slug, entrypointUrl, importMapUrl string, verifyJWT bool, functionBody io.Reader) error { 163 resp, err := utils.GetSupabase().GetFunctionWithResponse(ctx, projectRef, slug) 164 if err != nil { 165 return errors.Errorf("failed to retrieve function: %w", err) 166 } 167 168 switch resp.StatusCode() { 169 case http.StatusNotFound: // Function doesn't exist yet, so do a POST 170 resp, err := utils.GetSupabase().CreateFunctionWithBodyWithResponse(ctx, projectRef, &api.CreateFunctionParams{ 171 Slug: &slug, 172 Name: &slug, 173 VerifyJwt: &verifyJWT, 174 ImportMapPath: &importMapUrl, 175 EntrypointPath: &entrypointUrl, 176 }, eszipContentType, functionBody) 177 if err != nil { 178 return errors.Errorf("failed to create function: %w", err) 179 } 180 if resp.JSON201 == nil { 181 return errors.New("Failed to create a new Function on the Supabase project: " + string(resp.Body)) 182 } 183 case http.StatusOK: // Function already exists, so do a PATCH 184 resp, err := utils.GetSupabase().UpdateFunctionWithBodyWithResponse(ctx, projectRef, slug, &api.UpdateFunctionParams{ 185 VerifyJwt: &verifyJWT, 186 ImportMapPath: &importMapUrl, 187 EntrypointPath: &entrypointUrl, 188 }, eszipContentType, functionBody) 189 if err != nil { 190 return errors.Errorf("failed to update function: %w", err) 191 } 192 if resp.JSON200 == nil { 193 return errors.New("Failed to update an existing Function's body on the Supabase project: " + string(resp.Body)) 194 } 195 default: 196 return errors.New("Unexpected error deploying Function: " + string(resp.Body)) 197 } 198 199 fmt.Println("Deployed Function " + utils.Aqua(slug) + " on project " + utils.Aqua(projectRef)) 200 url := fmt.Sprintf("%s/project/%v/functions/%v/details", utils.GetSupabaseDashboardURL(), projectRef, slug) 201 fmt.Println("You can inspect your deployment in the Dashboard: " + url) 202 return nil 203 } 204 205 func deployOne(ctx context.Context, slug, projectRef, importMapPath string, noVerifyJWT *bool, fsys afero.Fs) error { 206 // 1. Ensure noVerifyJWT is not nil. 207 if noVerifyJWT == nil { 208 x := false 209 if functionConfig, ok := utils.Config.Functions[slug]; ok && !*functionConfig.VerifyJWT { 210 x = true 211 } 212 noVerifyJWT = &x 213 } 214 resolved, err := utils.AbsImportMapPath(importMapPath, slug, fsys) 215 if err != nil { 216 return err 217 } 218 // Upstream server expects import map to be always defined 219 if importMapPath == "" { 220 resolved, err = filepath.Abs(utils.FallbackImportMapPath) 221 if err != nil { 222 return errors.Errorf("failed to resolve absolute path: %w", err) 223 } 224 } 225 exists, err := afero.Exists(fsys, resolved) 226 if err != nil { 227 logger := utils.GetDebugLogger() 228 fmt.Fprintln(logger, err) 229 } 230 if exists { 231 importMapPath = resolved 232 } else { 233 importMapPath = "" 234 } 235 236 // 2. Bundle Function. 237 fmt.Println("Bundling " + utils.Bold(slug)) 238 dockerEntrypointPath := path.Join(utils.DockerFuncDirPath, slug, "index.ts") 239 functionBody, err := bundleFunction(ctx, slug, dockerEntrypointPath, importMapPath, fsys) 240 if err != nil { 241 return err 242 } 243 // 3. Deploy new Function. 244 functionSize := units.HumanSize(float64(functionBody.Len())) 245 fmt.Println("Deploying " + utils.Bold(slug) + " (script size: " + utils.Bold(functionSize) + ")") 246 policy := backoff.WithContext(backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 3), ctx) 247 return backoff.Retry(func() error { 248 return deployFunction( 249 ctx, 250 projectRef, 251 slug, 252 "file://"+dockerEntrypointPath, 253 "file://"+dockerImportMapPath, 254 !*noVerifyJWT, 255 functionBody, 256 ) 257 }, policy) 258 } 259 260 func deployAll(ctx context.Context, slugs []string, projectRef, importMapPath string, noVerifyJWT *bool, fsys afero.Fs) error { 261 // TODO: api has a race condition that prevents deploying in parallel 262 for _, slug := range slugs { 263 if err := deployOne(ctx, slug, projectRef, importMapPath, noVerifyJWT, fsys); err != nil { 264 return err 265 } 266 } 267 return nil 268 }