github.com/Redstoneguy129/cli@v0.0.0-20230211220159-15dca4e91917/internal/functions/deploy/deploy.go (about) 1 package deploy 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "strings" 14 15 "github.com/docker/go-units" 16 "github.com/spf13/afero" 17 "github.com/Redstoneguy129/cli/internal/utils" 18 "github.com/Redstoneguy129/cli/pkg/api" 19 ) 20 21 const eszipContentType = "application/vnd.denoland.eszip" 22 23 func Run(ctx context.Context, slug string, projectRefArg string, noVerifyJWT *bool, useLegacyBundle bool, importMapPath string, fsys afero.Fs) error { 24 // 1. Sanity checks. 25 projectRef := projectRefArg 26 var scriptDir *utils.DenoScriptDir 27 { 28 if len(projectRefArg) == 0 { 29 ref, err := utils.LoadProjectRef(fsys) 30 if err != nil { 31 return err 32 } 33 projectRef = ref 34 } else if !utils.ProjectRefPattern.MatchString(projectRef) { 35 return errors.New("Invalid project ref format. Must be like `abcdefghijklmnopqrst`.") 36 } 37 // Load function config if any for fallbacks for some flags, but continue on error. 38 _ = utils.LoadConfigFS(fsys) 39 // Ensure noVerifyJWT is not nil. 40 if noVerifyJWT == nil { 41 x := false 42 if functionConfig, ok := utils.Config.Functions[slug]; ok && !*functionConfig.VerifyJWT { 43 x = true 44 } 45 noVerifyJWT = &x 46 } 47 if importMapPath != "" { 48 // skip 49 } else if functionConfig, ok := utils.Config.Functions[slug]; ok && functionConfig.ImportMap != "" { 50 if filepath.IsAbs(functionConfig.ImportMap) { 51 importMapPath = functionConfig.ImportMap 52 } else { 53 importMapPath = filepath.Join(utils.SupabaseDirPath, functionConfig.ImportMap) 54 } 55 } else if f, err := fsys.Stat(utils.FallbackImportMapPath); err == nil && !f.IsDir() { 56 importMapPath = utils.FallbackImportMapPath 57 } 58 if importMapPath != "" { 59 if _, err := fsys.Stat(importMapPath); err != nil { 60 return fmt.Errorf("Failed to read import map: %w", err) 61 } 62 } 63 if err := utils.ValidateFunctionSlug(slug); err != nil { 64 return err 65 } 66 if err := utils.InstallOrUpgradeDeno(ctx, fsys); err != nil { 67 return err 68 } 69 70 var err error 71 scriptDir, err = utils.CopyDenoScripts(ctx, fsys) 72 if err != nil { 73 return err 74 } 75 } 76 77 // 2. Bundle Function. 78 var functionBody io.Reader 79 var functionSize int 80 { 81 fmt.Println("Bundling " + utils.Bold(slug)) 82 denoPath, err := utils.GetDenoPath() 83 if err != nil { 84 return err 85 } 86 87 functionPath := filepath.Join(utils.FunctionsDir, slug) 88 if _, err := fsys.Stat(functionPath); errors.Is(err, os.ErrNotExist) { 89 // allow deploy from within supabase/ 90 functionPath = filepath.Join("functions", slug) 91 if _, err := fsys.Stat(functionPath); errors.Is(err, os.ErrNotExist) { 92 // allow deploy from current directory 93 functionPath = slug 94 } 95 } 96 97 buildScriptPath := scriptDir.BuildPath 98 args := []string{"run", "-A", buildScriptPath, filepath.Join(functionPath, "index.ts"), importMapPath} 99 if useLegacyBundle { 100 args = []string{"bundle", "--no-check=remote", "--quiet", filepath.Join(functionPath, "index.ts")} 101 } 102 cmd := exec.CommandContext(ctx, denoPath, args...) 103 var outBuf, errBuf bytes.Buffer 104 cmd.Stdout = &outBuf 105 cmd.Stderr = &errBuf 106 if err := cmd.Run(); err != nil { 107 return fmt.Errorf("Error bundling function: %w\n%v", err, errBuf.String()) 108 } 109 110 functionBody = &outBuf 111 functionSize = outBuf.Len() 112 } 113 114 // 3. Deploy new Function. 115 fmt.Println("Deploying " + utils.Bold(slug) + " (script size: " + utils.Bold(units.HumanSize(float64(functionSize))) + ")") 116 return deployFunction(ctx, projectRef, slug, functionBody, !*noVerifyJWT, useLegacyBundle) 117 } 118 119 func makeLegacyFunctionBody(functionBody io.Reader) (string, error) { 120 buf := new(strings.Builder) 121 _, err := io.Copy(buf, functionBody) 122 if err != nil { 123 return "", err 124 } 125 126 return buf.String(), nil 127 } 128 129 func deployFunction(ctx context.Context, projectRef, slug string, functionBody io.Reader, verifyJWT, useLegacyBundle bool) error { 130 var deployedFuncId string 131 { 132 resp, err := utils.GetSupabase().GetFunctionWithResponse(ctx, projectRef, slug) 133 if err != nil { 134 return err 135 } 136 137 var functionBodyStr string 138 if useLegacyBundle { 139 functionBodyStr, err = makeLegacyFunctionBody(functionBody) 140 if err != nil { 141 return err 142 } 143 } 144 145 // Note: imageMap is always set to true, since eszip created will always contain a `import_map.json`. 146 importMap := true 147 148 switch resp.StatusCode() { 149 case http.StatusNotFound: // Function doesn't exist yet, so do a POST 150 var resp *api.CreateFunctionResponse 151 var err error 152 if useLegacyBundle { 153 resp, err = utils.GetSupabase().CreateFunctionWithResponse(ctx, projectRef, &api.CreateFunctionParams{}, api.CreateFunctionJSONRequestBody{ 154 Body: functionBodyStr, 155 Name: slug, 156 Slug: slug, 157 VerifyJwt: &verifyJWT, 158 }) 159 } else { 160 resp, err = utils.GetSupabase().CreateFunctionWithBodyWithResponse(ctx, projectRef, &api.CreateFunctionParams{ 161 Slug: &slug, 162 Name: &slug, 163 VerifyJwt: &verifyJWT, 164 ImportMap: &importMap, 165 }, eszipContentType, functionBody) 166 } 167 if err != nil { 168 return err 169 } 170 if resp.JSON201 == nil { 171 return errors.New("Failed to create a new Function on the Supabase project: " + string(resp.Body)) 172 } 173 deployedFuncId = resp.JSON201.Id 174 case http.StatusOK: // Function already exists, so do a PATCH 175 var resp *api.UpdateFunctionResponse 176 var err error 177 if useLegacyBundle { 178 resp, err = utils.GetSupabase().UpdateFunctionWithResponse(ctx, projectRef, slug, &api.UpdateFunctionParams{}, api.UpdateFunctionJSONRequestBody{ 179 Body: &functionBodyStr, 180 VerifyJwt: &verifyJWT, 181 }) 182 } else { 183 resp, err = utils.GetSupabase().UpdateFunctionWithBodyWithResponse(ctx, projectRef, slug, &api.UpdateFunctionParams{ 184 VerifyJwt: &verifyJWT, 185 ImportMap: &importMap, 186 }, eszipContentType, functionBody) 187 } 188 if err != nil { 189 return err 190 } 191 if resp.JSON200 == nil { 192 return errors.New("Failed to update an existing Function's body on the Supabase project: " + string(resp.Body)) 193 } 194 deployedFuncId = resp.JSON200.Id 195 default: 196 return errors.New("Unexpected error deploying Function: " + string(resp.Body)) 197 } 198 } 199 200 fmt.Println("Deployed Function " + utils.Aqua(slug) + " on project " + utils.Aqua(projectRef)) 201 202 url := fmt.Sprintf("%s/project/%v/functions/%v/details", utils.GetSupabaseDashboardURL(), projectRef, deployedFuncId) 203 fmt.Println("You can inspect your deployment in the Dashboard: " + url) 204 205 return nil 206 }