github.com/Redstoneguy129/cli@v0.0.0-20230211220159-15dca4e91917/internal/utils/misc.go (about)

     1  package utils
     2  
     3  import (
     4  	"archive/zip"
     5  	"bytes"
     6  	"context"
     7  	"crypto/md5"
     8  	"embed"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"io/fs"
    13  	"net/http"
    14  	"os"
    15  	"os/exec"
    16  	"path/filepath"
    17  	"regexp"
    18  	"runtime"
    19  	"strings"
    20  	"time"
    21  
    22  	"github.com/Redstoneguy129/cli/internal/utils/credentials"
    23  	"github.com/spf13/afero"
    24  )
    25  
    26  const (
    27  	Pg13Image = "supabase/postgres:13.3.0"
    28  	Pg14Image = "supabase/postgres:14.1.0.89"
    29  	Pg15Image = "supabase/postgres:15.1.0.33"
    30  	// Append to ServiceImages when adding new dependencies below
    31  	KongImage        = "library/kong:2.8.1"
    32  	InbucketImage    = "inbucket/inbucket:3.0.3"
    33  	PostgrestImage   = "postgrest/postgrest:v10.1.1.20221215"
    34  	DifferImage      = "supabase/pgadmin-schema-diff:cli-0.0.5"
    35  	MigraImage       = "djrobstep/migra:3.0.1621480950"
    36  	PgmetaImage      = "supabase/postgres-meta:v0.60.7"
    37  	StudioImage      = "supabase/studio:20230127-6bfd87b"
    38  	DenoRelayImage   = "supabase/deno-relay:v1.5.0"
    39  	ImageProxyImage  = "darthsim/imgproxy:v3.8.0"
    40  	EdgeRuntimeImage = "supabase/edge-runtime:v1.0.9"
    41  	// Update initial schemas in internal/utils/templates/initial_schemas when
    42  	// updating any one of these.
    43  	GotrueImage   = "supabase/gotrue:v2.40.1"
    44  	RealtimeImage = "supabase/realtime:v2.4.0"
    45  	StorageImage  = "supabase/storage-api:v0.26.1"
    46  	// Should be kept in-sync with DenoRelayImage
    47  	DenoVersion = "1.28.0"
    48  )
    49  
    50  var ServiceImages = []string{
    51  	GotrueImage,
    52  	RealtimeImage,
    53  	StorageImage,
    54  	ImageProxyImage,
    55  	KongImage,
    56  	InbucketImage,
    57  	PostgrestImage,
    58  	DifferImage,
    59  	MigraImage,
    60  	PgmetaImage,
    61  	StudioImage,
    62  	DenoRelayImage,
    63  }
    64  
    65  func ShortContainerImageName(imageName string) string {
    66  	matches := ImageNamePattern.FindStringSubmatch(imageName)
    67  	if len(matches) < 2 {
    68  		return imageName
    69  	}
    70  	return matches[1]
    71  }
    72  
    73  const (
    74  	// https://dba.stackexchange.com/a/11895
    75  	// Args: dbname
    76  	TerminateDbSqlFmt = `
    77  SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '%[1]s';
    78  -- Wait for WAL sender to drop replication slot.
    79  DO 'BEGIN WHILE (
    80  	SELECT COUNT(*) FROM pg_replication_slots WHERE database = ''%[1]s''
    81  ) > 0 LOOP END LOOP; END';`
    82  	AnonKey        = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"
    83  	ServiceRoleKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU"
    84  	JWTSecret      = "super-secret-jwt-token-with-at-least-32-characters-long"
    85  	AccessTokenKey = "access-token"
    86  )
    87  
    88  var (
    89  	CmdSuggestion string
    90  
    91  	// pg_dumpall --globals-only --no-role-passwords --dbname $DB_URL \
    92  	// | sed '/^CREATE ROLE postgres;/d' \
    93  	// | sed '/^ALTER ROLE postgres WITH /d' \
    94  	// | sed "/^ALTER ROLE .* WITH .* LOGIN /s/;$/ PASSWORD 'postgres';/"
    95  	//go:embed templates/globals.sql
    96  	GlobalsSql string
    97  
    98  	//go:embed denos/*
    99  	denoEmbedDir embed.FS
   100  
   101  	AccessTokenPattern = regexp.MustCompile(`^sbp_[a-f0-9]{40}$`)
   102  	ProjectRefPattern  = regexp.MustCompile(`^[a-z]{20}$`)
   103  	PostgresUrlPattern = regexp.MustCompile(`^postgres(?:ql)?:\/\/postgres:(.*)@(.+)\/postgres$`)
   104  	MigrateFilePattern = regexp.MustCompile(`^([0-9]+)_.*\.sql$`)
   105  	BranchNamePattern  = regexp.MustCompile(`[[:word:]-]+`)
   106  	FuncSlugPattern    = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]*$`)
   107  	ImageNamePattern   = regexp.MustCompile(`\/(.*):`)
   108  
   109  	// These schemas are ignored from schema diffs
   110  	SystemSchemas = []string{
   111  		"information_schema",
   112  		"pg_catalog",
   113  		"pg_toast",
   114  		// "pg_temp_1",
   115  		// "pg_toast_temp_1",
   116  		// Owned by extensions
   117  		"cron",
   118  		"graphql",
   119  		"graphql_public",
   120  		"net",
   121  		"pgsodium",
   122  		"pgsodium_masks",
   123  		"vault",
   124  	}
   125  	InternalSchemas = append([]string{
   126  		"auth",
   127  		"extensions",
   128  		"pgbouncer",
   129  		"realtime",
   130  		"_realtime",
   131  		"storage",
   132  		"supabase_functions",
   133  		"supabase_migrations",
   134  	}, SystemSchemas...)
   135  
   136  	SupabaseDirPath       = "supabase"
   137  	ConfigPath            = filepath.Join(SupabaseDirPath, "config.toml")
   138  	ProjectRefPath        = filepath.Join(SupabaseDirPath, ".temp", "project-ref")
   139  	RemoteDbPath          = filepath.Join(SupabaseDirPath, ".temp", "remote-db-url")
   140  	CurrBranchPath        = filepath.Join(SupabaseDirPath, ".branches", "_current_branch")
   141  	MigrationsDir         = filepath.Join(SupabaseDirPath, "migrations")
   142  	FunctionsDir          = filepath.Join(SupabaseDirPath, "functions")
   143  	FallbackImportMapPath = filepath.Join(FunctionsDir, "import_map.json")
   144  	DbTestsDir            = filepath.Join(SupabaseDirPath, "tests")
   145  	SeedDataPath          = filepath.Join(SupabaseDirPath, "seed.sql")
   146  )
   147  
   148  // Used by unit tests
   149  var (
   150  	DenoPathOverride string
   151  )
   152  
   153  func GetCurrentTimestamp() string {
   154  	// Magic number: https://stackoverflow.com/q/45160822.
   155  	return time.Now().UTC().Format("20060102150405")
   156  }
   157  
   158  func GetCurrentBranchFS(fsys afero.Fs) (string, error) {
   159  	branch, err := afero.ReadFile(fsys, CurrBranchPath)
   160  	if err != nil {
   161  		return "", err
   162  	}
   163  
   164  	return string(branch), nil
   165  }
   166  
   167  // TODO: Make all errors use this.
   168  func NewError(s string) error {
   169  	// Ask runtime.Callers for up to 5 PCs, excluding runtime.Callers and NewError.
   170  	pc := make([]uintptr, 5)
   171  	n := runtime.Callers(2, pc)
   172  
   173  	pc = pc[:n] // pass only valid pcs to runtime.CallersFrames
   174  	frames := runtime.CallersFrames(pc)
   175  
   176  	// Loop to get frames.
   177  	// A fixed number of PCs can expand to an indefinite number of Frames.
   178  	for {
   179  		frame, more := frames.Next()
   180  
   181  		// Process this frame.
   182  		//
   183  		// We're only interested in the stack trace in this repo.
   184  		if strings.HasPrefix(frame.Function, "github.com/Redstoneguy129/cli/internal") {
   185  			s += fmt.Sprintf("\n  in %s:%d", frame.Function, frame.Line)
   186  		}
   187  
   188  		// Check whether there are more frames to process after this one.
   189  		if !more {
   190  			break
   191  		}
   192  	}
   193  
   194  	return errors.New(s)
   195  }
   196  
   197  func AssertSupabaseDbIsRunning() error {
   198  	if _, err := Docker.ContainerInspect(context.Background(), DbId); err != nil {
   199  		return errors.New(Aqua("supabase start") + " is not running.")
   200  	}
   201  
   202  	return nil
   203  }
   204  
   205  func GetGitRoot(fsys afero.Fs) (*string, error) {
   206  	origWd, err := os.Getwd()
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  
   211  	for {
   212  		_, err := afero.ReadDir(fsys, ".git")
   213  
   214  		if err == nil {
   215  			gitRoot, err := os.Getwd()
   216  			if err != nil {
   217  				return nil, err
   218  			}
   219  
   220  			if err := os.Chdir(origWd); err != nil {
   221  				return nil, err
   222  			}
   223  
   224  			return &gitRoot, nil
   225  		}
   226  
   227  		if cwd, err := os.Getwd(); err != nil {
   228  			return nil, err
   229  		} else if isRootDirectory(cwd) {
   230  			return nil, nil
   231  		}
   232  
   233  		if err := os.Chdir(".."); err != nil {
   234  			return nil, err
   235  		}
   236  	}
   237  }
   238  
   239  // If the `os.Getwd()` is within a supabase project, this will return
   240  // the root of the given project as the current working directory.
   241  // Otherwise, the `os.Getwd()` is kept as is.
   242  func GetProjectRoot(fsys afero.Fs) (string, error) {
   243  	origWd, err := os.Getwd()
   244  	for cwd := origWd; err == nil; cwd = filepath.Dir(cwd) {
   245  		path := filepath.Join(cwd, ConfigPath)
   246  		// Treat all errors as file not exists
   247  		if isSupaProj, _ := afero.Exists(fsys, path); isSupaProj {
   248  			return cwd, nil
   249  		}
   250  		if isRootDirectory(cwd) {
   251  			break
   252  		}
   253  	}
   254  	return origWd, err
   255  }
   256  
   257  func IsBranchNameReserved(branch string) bool {
   258  	switch branch {
   259  	case "_current_branch", "main", "postgres", "template0", "template1":
   260  		return true
   261  	default:
   262  		return false
   263  	}
   264  }
   265  
   266  func MkdirIfNotExist(path string) error {
   267  	return MkdirIfNotExistFS(afero.NewOsFs(), path)
   268  }
   269  
   270  func MkdirIfNotExistFS(fsys afero.Fs, path string) error {
   271  	if err := fsys.MkdirAll(path, 0755); err != nil && !errors.Is(err, os.ErrExist) {
   272  		return err
   273  	}
   274  
   275  	return nil
   276  }
   277  
   278  func AssertSupabaseCliIsSetUpFS(fsys afero.Fs) error {
   279  	if _, err := fsys.Stat(ConfigPath); errors.Is(err, os.ErrNotExist) {
   280  		return errors.New("Cannot find " + Bold(ConfigPath) + " in the current directory. Have you set up the project with " + Aqua("supabase init") + "?")
   281  	} else if err != nil {
   282  		return err
   283  	}
   284  
   285  	return nil
   286  }
   287  
   288  func AssertIsLinkedFS(fsys afero.Fs) error {
   289  	if _, err := fsys.Stat(ProjectRefPath); errors.Is(err, os.ErrNotExist) {
   290  		return errors.New("Cannot find project ref. Have you run " + Aqua("supabase link") + "?")
   291  	} else if err != nil {
   292  		return err
   293  	}
   294  
   295  	return nil
   296  }
   297  
   298  func LoadProjectRef(fsys afero.Fs) (string, error) {
   299  	projectRefBytes, err := afero.ReadFile(fsys, ProjectRefPath)
   300  	if err != nil {
   301  		return "", errors.New("Cannot find project ref. Have you run " + Aqua("supabase link") + "?")
   302  	}
   303  	projectRef := string(bytes.TrimSpace(projectRefBytes))
   304  	if !ProjectRefPattern.MatchString(projectRef) {
   305  		return "", errors.New("Invalid project ref format. Must be like `abcdefghijklmnopqrst`.")
   306  	}
   307  	return projectRef, nil
   308  }
   309  
   310  func GetDenoPath() (string, error) {
   311  	if len(DenoPathOverride) > 0 {
   312  		return DenoPathOverride, nil
   313  	}
   314  	home, err := os.UserHomeDir()
   315  	if err != nil {
   316  		return "", err
   317  	}
   318  	denoBinName := "deno"
   319  	if runtime.GOOS == "windows" {
   320  		denoBinName = "deno.exe"
   321  	}
   322  	denoPath := filepath.Join(home, ".supabase", denoBinName)
   323  	return denoPath, nil
   324  }
   325  
   326  func InstallOrUpgradeDeno(ctx context.Context, fsys afero.Fs) error {
   327  	denoPath, err := GetDenoPath()
   328  	if err != nil {
   329  		return err
   330  	}
   331  
   332  	if _, err := fsys.Stat(denoPath); err == nil {
   333  		// Upgrade Deno.
   334  		cmd := exec.CommandContext(ctx, denoPath, "upgrade", "--version", DenoVersion)
   335  		cmd.Stderr = os.Stderr
   336  		cmd.Stdout = os.Stdout
   337  		return cmd.Run()
   338  	} else if !errors.Is(err, os.ErrNotExist) {
   339  		return err
   340  	}
   341  
   342  	// Install Deno.
   343  	if err := MkdirIfNotExistFS(fsys, filepath.Dir(denoPath)); err != nil {
   344  		return err
   345  	}
   346  
   347  	// 1. Determine OS triple
   348  	var assetFilename string
   349  	assetRepo := "denoland/deno"
   350  	{
   351  		if runtime.GOOS == "darwin" && runtime.GOARCH == "amd64" {
   352  			assetFilename = "deno-x86_64-apple-darwin.zip"
   353  		} else if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
   354  			assetFilename = "deno-aarch64-apple-darwin.zip"
   355  		} else if runtime.GOOS == "linux" && runtime.GOARCH == "amd64" {
   356  			assetFilename = "deno-x86_64-unknown-linux-gnu.zip"
   357  		} else if runtime.GOOS == "linux" && runtime.GOARCH == "arm64" {
   358  			// TODO: version pin to official release once available https://github.com/denoland/deno/issues/1846
   359  			assetRepo = "LukeChannings/deno-arm64"
   360  			assetFilename = "deno-linux-arm64.zip"
   361  		} else if runtime.GOOS == "windows" && runtime.GOARCH == "amd64" {
   362  			assetFilename = "deno-x86_64-pc-windows-msvc.zip"
   363  		} else {
   364  			return errors.New("Platform " + runtime.GOOS + "/" + runtime.GOARCH + " is currently unsupported for Functions.")
   365  		}
   366  	}
   367  
   368  	// 2. Download & install Deno binary.
   369  	{
   370  		assetUrl := fmt.Sprintf("https://github.com/%s/releases/download/v%s/%s", assetRepo, DenoVersion, assetFilename)
   371  		req, err := http.NewRequestWithContext(ctx, "GET", assetUrl, nil)
   372  		if err != nil {
   373  			return err
   374  		}
   375  		resp, err := http.DefaultClient.Do(req)
   376  		if err != nil {
   377  			return err
   378  		}
   379  		defer resp.Body.Close()
   380  
   381  		if resp.StatusCode != 200 {
   382  			return errors.New("Failed installing Deno binary.")
   383  		}
   384  
   385  		body, err := io.ReadAll(resp.Body)
   386  		if err != nil {
   387  			return err
   388  		}
   389  
   390  		r, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
   391  		// There should be only 1 file: the deno binary
   392  		if len(r.File) != 1 {
   393  			return err
   394  		}
   395  		denoContents, err := r.File[0].Open()
   396  		if err != nil {
   397  			return err
   398  		}
   399  		defer denoContents.Close()
   400  
   401  		denoBytes, err := io.ReadAll(denoContents)
   402  		if err != nil {
   403  			return err
   404  		}
   405  
   406  		if err := afero.WriteFile(fsys, denoPath, denoBytes, 0755); err != nil {
   407  			return err
   408  		}
   409  	}
   410  
   411  	return nil
   412  }
   413  
   414  func isScriptModified(fsys afero.Fs, destPath string, src []byte) (bool, error) {
   415  	dest, err := afero.ReadFile(fsys, destPath)
   416  	if err != nil {
   417  		if errors.Is(err, fs.ErrNotExist) {
   418  			return true, nil
   419  		}
   420  		return false, err
   421  	}
   422  
   423  	// compare the md5 checksum of src bytes with user's copy.
   424  	// if the checksums doesn't match, script is modified.
   425  	return md5.Sum(dest) != md5.Sum(src), nil
   426  }
   427  
   428  type DenoScriptDir struct {
   429  	ExtractPath string
   430  	BuildPath   string
   431  }
   432  
   433  // Copy Deno scripts needed for function deploy and downloads, returning a DenoScriptDir struct or an error.
   434  func CopyDenoScripts(ctx context.Context, fsys afero.Fs) (*DenoScriptDir, error) {
   435  	denoPath, err := GetDenoPath()
   436  	if err != nil {
   437  		return nil, err
   438  	}
   439  
   440  	denoDirPath := filepath.Dir(denoPath)
   441  	scriptDirPath := filepath.Join(denoDirPath, "denos")
   442  
   443  	// make the script directory if not exist
   444  	if err := MkdirIfNotExistFS(fsys, scriptDirPath); err != nil {
   445  		return nil, err
   446  	}
   447  
   448  	// copy embed files to script directory
   449  	err = fs.WalkDir(denoEmbedDir, "denos", func(path string, d fs.DirEntry, err error) error {
   450  		if err != nil {
   451  			return err
   452  		}
   453  
   454  		// skip copying the directory
   455  		if d.IsDir() {
   456  			return nil
   457  		}
   458  
   459  		destPath := filepath.Join(denoDirPath, path)
   460  
   461  		contents, err := fs.ReadFile(denoEmbedDir, path)
   462  		if err != nil {
   463  			return err
   464  		}
   465  
   466  		// check if the script should be copied
   467  		modified, err := isScriptModified(fsys, destPath, contents)
   468  		if err != nil {
   469  			return err
   470  		}
   471  		if !modified {
   472  			return nil
   473  		}
   474  
   475  		if err := afero.WriteFile(fsys, filepath.Join(denoDirPath, path), contents, 0666); err != nil {
   476  			return err
   477  		}
   478  
   479  		return nil
   480  	})
   481  
   482  	if err != nil {
   483  		return nil, err
   484  	}
   485  
   486  	sd := DenoScriptDir{
   487  		ExtractPath: filepath.Join(scriptDirPath, "extract.ts"),
   488  		BuildPath:   filepath.Join(scriptDirPath, "build.ts"),
   489  	}
   490  
   491  	return &sd, nil
   492  }
   493  
   494  func LoadAccessToken() (string, error) {
   495  	return LoadAccessTokenFS(afero.NewOsFs())
   496  }
   497  
   498  func LoadAccessTokenFS(fsys afero.Fs) (string, error) {
   499  	// Env takes precedence
   500  	if accessToken := os.Getenv("SUPABASE_ACCESS_TOKEN"); accessToken != "" {
   501  		if !AccessTokenPattern.MatchString(accessToken) {
   502  			return "", errors.New("Invalid access token format. Must be like `sbp_0102...1920`.")
   503  		}
   504  		return accessToken, nil
   505  	}
   506  	// Load from native credentials store
   507  	if token, err := credentials.Get(AccessTokenKey); err == nil {
   508  		return token, nil
   509  	}
   510  	// Fallback to home directory
   511  	home, err := os.UserHomeDir()
   512  	if err != nil {
   513  		return "", err
   514  	}
   515  	accessTokenPath := filepath.Join(home, ".supabase", AccessTokenKey)
   516  	accessToken, err := afero.ReadFile(fsys, accessTokenPath)
   517  	if errors.Is(err, os.ErrNotExist) || string(accessToken) == "" {
   518  		return "", errors.New("Access token not provided. Supply an access token by running " + Aqua("supabase login") + " or setting the SUPABASE_ACCESS_TOKEN environment variable.")
   519  	} else if err != nil {
   520  		return "", err
   521  	}
   522  	return string(accessToken), nil
   523  }
   524  
   525  func ValidateFunctionSlug(slug string) error {
   526  	if !FuncSlugPattern.MatchString(slug) {
   527  		return errors.New("Invalid Function name. Must start with at least one letter, and only include alphanumeric characters, underscores, and hyphens. (^[A-Za-z][A-Za-z0-9_-]*$)")
   528  	}
   529  
   530  	return nil
   531  }