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  }