github.com/supabase/cli@v1.168.1/internal/utils/misc.go (about)

     1  package utils
     2  
     3  import (
     4  	"context"
     5  	_ "embed"
     6  	"fmt"
     7  	"net"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"time"
    12  
    13  	"github.com/docker/docker/client"
    14  	"github.com/go-errors/errors"
    15  	"github.com/go-git/go-git/v5"
    16  	"github.com/spf13/afero"
    17  	"github.com/spf13/viper"
    18  )
    19  
    20  // Assigned using `-ldflags` https://stackoverflow.com/q/11354518
    21  var (
    22  	Version   string
    23  	SentryDsn string
    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.1.41"
    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:v12.0.1"
    34  	DifferImage      = "supabase/pgadmin-schema-diff:cli-0.0.5"
    35  	MigraImage       = "supabase/migra:3.0.1663481299"
    36  	PgmetaImage      = "supabase/postgres-meta:v0.80.0"
    37  	StudioImage      = "supabase/studio:20240514-6f5cabd"
    38  	ImageProxyImage  = "darthsim/imgproxy:v3.8.0"
    39  	EdgeRuntimeImage = "supabase/edge-runtime:v1.53.0"
    40  	VectorImage      = "timberio/vector:0.28.1-alpine"
    41  	PgbouncerImage   = "bitnami/pgbouncer:1.20.1-debian-11-r39"
    42  	PgProveImage     = "supabase/pg_prove:3.36"
    43  	GotrueImage      = "supabase/gotrue:v2.151.0"
    44  	RealtimeImage    = "supabase/realtime:v2.28.32"
    45  	StorageImage     = "supabase/storage-api:v1.0.6"
    46  	LogflareImage    = "supabase/logflare:1.4.0"
    47  	// Should be kept in-sync with EdgeRuntimeImage
    48  	DenoVersion = "1.30.3"
    49  )
    50  
    51  var ServiceImages = []string{
    52  	GotrueImage,
    53  	RealtimeImage,
    54  	StorageImage,
    55  	ImageProxyImage,
    56  	KongImage,
    57  	InbucketImage,
    58  	PostgrestImage,
    59  	DifferImage,
    60  	MigraImage,
    61  	PgmetaImage,
    62  	StudioImage,
    63  	EdgeRuntimeImage,
    64  	LogflareImage,
    65  	VectorImage,
    66  	PgbouncerImage,
    67  	PgProveImage,
    68  }
    69  
    70  func ShortContainerImageName(imageName string) string {
    71  	matches := ImageNamePattern.FindStringSubmatch(imageName)
    72  	if len(matches) < 2 {
    73  		return imageName
    74  	}
    75  	return matches[1]
    76  }
    77  
    78  const (
    79  	// https://dba.stackexchange.com/a/11895
    80  	// Args: dbname
    81  	TerminateDbSqlFmt = `
    82  SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '%[1]s';
    83  -- Wait for WAL sender to drop replication slot.
    84  DO 'BEGIN WHILE (
    85  	SELECT COUNT(*) FROM pg_replication_slots WHERE database = ''%[1]s''
    86  ) > 0 LOOP END LOOP; END';`
    87  	SuggestDebugFlag = "Try rerunning the command with --debug to troubleshoot the error."
    88  )
    89  
    90  var (
    91  	CmdSuggestion string
    92  	CurrentDirAbs string
    93  
    94  	// pg_dumpall --globals-only --no-role-passwords --dbname $DB_URL \
    95  	// | sed '/^CREATE ROLE postgres;/d' \
    96  	// | sed '/^ALTER ROLE postgres WITH /d' \
    97  	// | sed "/^ALTER ROLE .* WITH .* LOGIN /s/;$/ PASSWORD 'postgres';/"
    98  	//go:embed templates/globals.sql
    99  	GlobalsSql string
   100  
   101  	ProjectRefPattern  = regexp.MustCompile(`^[a-z]{20}$`)
   102  	UUIDPattern        = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
   103  	ProjectHostPattern = regexp.MustCompile(`^(db\.)([a-z]{20})\.supabase\.(co|red)$`)
   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 db diff and db dump
   110  	PgSchemas = []string{
   111  		"information_schema",
   112  		"pg_*", // Wildcard pattern follows pg_dump
   113  	}
   114  	// Initialised by postgres image and owned by postgres role
   115  	ManagedSchemas = append([]string{
   116  		"pgbouncer",
   117  		"pgsodium",
   118  		"pgtle",
   119  		"supabase_migrations",
   120  		"vault",
   121  	}, PgSchemas...)
   122  	InternalSchemas = append([]string{
   123  		"auth",
   124  		"extensions",
   125  		"pgbouncer",
   126  		"realtime",
   127  		"_realtime",
   128  		"storage",
   129  		"_analytics",
   130  		"supabase_functions",
   131  		"supabase_migrations",
   132  		// Owned by extensions
   133  		"cron",
   134  		"dbdev",
   135  		"graphql",
   136  		"graphql_public",
   137  		"net",
   138  		"pgsodium",
   139  		"pgsodium_masks",
   140  		"pgtle",
   141  		"repack",
   142  		"tiger",
   143  		"tiger_data",
   144  		"timescaledb_*",
   145  		"_timescaledb_*",
   146  		"topology",
   147  		"vault",
   148  	}, PgSchemas...)
   149  	ReservedRoles = []string{
   150  		"anon",
   151  		"authenticated",
   152  		"authenticator",
   153  		"dashboard_user",
   154  		"pgbouncer",
   155  		"postgres",
   156  		"service_role",
   157  		"supabase_admin",
   158  		"supabase_auth_admin",
   159  		"supabase_functions_admin",
   160  		"supabase_read_only_user",
   161  		"supabase_realtime_admin",
   162  		"supabase_replication_admin",
   163  		"supabase_storage_admin",
   164  		// Managed by extensions
   165  		"pgsodium_keyholder",
   166  		"pgsodium_keyiduser",
   167  		"pgsodium_keymaker",
   168  		"pgtle_admin",
   169  	}
   170  	AllowedConfigs = []string{
   171  		// Ref: https://github.com/supabase/postgres/blob/develop/ansible/files/postgresql_config/supautils.conf.j2#L10
   172  		"pgaudit.*",
   173  		"pgrst.*",
   174  		"session_replication_role",
   175  		"statement_timeout",
   176  		"track_io_timing",
   177  	}
   178  
   179  	SupabaseDirPath       = "supabase"
   180  	ConfigPath            = filepath.Join(SupabaseDirPath, "config.toml")
   181  	GitIgnorePath         = filepath.Join(SupabaseDirPath, ".gitignore")
   182  	TempDir               = filepath.Join(SupabaseDirPath, ".temp")
   183  	ImportMapsDir         = filepath.Join(TempDir, "import_maps")
   184  	ProjectRefPath        = filepath.Join(TempDir, "project-ref")
   185  	PoolerUrlPath         = filepath.Join(TempDir, "pooler-url")
   186  	PostgresVersionPath   = filepath.Join(TempDir, "postgres-version")
   187  	GotrueVersionPath     = filepath.Join(TempDir, "gotrue-version")
   188  	RestVersionPath       = filepath.Join(TempDir, "rest-version")
   189  	StorageVersionPath    = filepath.Join(TempDir, "storage-version")
   190  	CliVersionPath        = filepath.Join(TempDir, "cli-latest")
   191  	CurrBranchPath        = filepath.Join(SupabaseDirPath, ".branches", "_current_branch")
   192  	SchemasDir            = filepath.Join(SupabaseDirPath, "schemas")
   193  	MigrationsDir         = filepath.Join(SupabaseDirPath, "migrations")
   194  	FunctionsDir          = filepath.Join(SupabaseDirPath, "functions")
   195  	FallbackImportMapPath = filepath.Join(FunctionsDir, "import_map.json")
   196  	FallbackEnvFilePath   = filepath.Join(FunctionsDir, ".env")
   197  	DbTestsDir            = filepath.Join(SupabaseDirPath, "tests")
   198  	SeedDataPath          = filepath.Join(SupabaseDirPath, "seed.sql")
   199  	CustomRolesPath       = filepath.Join(SupabaseDirPath, "roles.sql")
   200  
   201  	ErrNotLinked   = errors.Errorf("Cannot find project ref. Have you run %s?", Aqua("supabase link"))
   202  	ErrInvalidRef  = errors.New("Invalid project ref format. Must be like `abcdefghijklmnopqrst`.")
   203  	ErrInvalidSlug = 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_-]*$)")
   204  	ErrNotRunning  = errors.Errorf("%s is not running.", Aqua("supabase start"))
   205  )
   206  
   207  func GetCurrentTimestamp() string {
   208  	// Magic number: https://stackoverflow.com/q/45160822.
   209  	return time.Now().UTC().Format("20060102150405")
   210  }
   211  
   212  func GetCurrentBranchFS(fsys afero.Fs) (string, error) {
   213  	branch, err := afero.ReadFile(fsys, CurrBranchPath)
   214  	if err != nil {
   215  		return "", errors.Errorf("failed to load current branch: %w", err)
   216  	}
   217  
   218  	return string(branch), nil
   219  }
   220  
   221  func AssertSupabaseDbIsRunning() error {
   222  	if _, err := Docker.ContainerInspect(context.Background(), DbId); err != nil {
   223  		if client.IsErrNotFound(err) {
   224  			return errors.New(ErrNotRunning)
   225  		}
   226  		if client.IsErrConnectionFailed(err) {
   227  			CmdSuggestion = suggestDockerInstall
   228  		}
   229  		return errors.Errorf("failed to inspect database container: %w", err)
   230  	}
   231  	return nil
   232  }
   233  
   234  func IsGitRepo() bool {
   235  	opts := &git.PlainOpenOptions{DetectDotGit: true}
   236  	_, err := git.PlainOpenWithOptions(".", opts)
   237  	return err == nil
   238  }
   239  
   240  // If the `os.Getwd()` is within a supabase project, this will return
   241  // the root of the given project as the current working directory.
   242  // Otherwise, the `os.Getwd()` is kept as is.
   243  func getProjectRoot(absPath string, fsys afero.Fs) string {
   244  	for cwd := absPath; ; cwd = filepath.Dir(cwd) {
   245  		path := filepath.Join(cwd, ConfigPath)
   246  		// Treat all errors as file not exists
   247  		if isSupaProj, err := afero.Exists(fsys, path); isSupaProj {
   248  			return cwd
   249  		} else if err != nil && !errors.Is(err, os.ErrNotExist) {
   250  			logger := GetDebugLogger()
   251  			fmt.Fprintln(logger, err)
   252  		}
   253  		if isRootDirectory(cwd) {
   254  			break
   255  		}
   256  	}
   257  	return absPath
   258  }
   259  
   260  func isRootDirectory(cleanPath string) bool {
   261  	// A cleaned path only ends with separator if it is root
   262  	return os.IsPathSeparator(cleanPath[len(cleanPath)-1])
   263  }
   264  
   265  func ChangeWorkDir(fsys afero.Fs) error {
   266  	// Track the original workdir before changing to project root
   267  	if !filepath.IsAbs(CurrentDirAbs) {
   268  		var err error
   269  		if CurrentDirAbs, err = os.Getwd(); err != nil {
   270  			return errors.Errorf("failed to get current directory: %w", err)
   271  		}
   272  	}
   273  	workdir := viper.GetString("WORKDIR")
   274  	if len(workdir) == 0 {
   275  		workdir = getProjectRoot(CurrentDirAbs, fsys)
   276  	}
   277  	if err := os.Chdir(workdir); err != nil {
   278  		return errors.Errorf("failed to change workdir: %w", err)
   279  	}
   280  	return nil
   281  }
   282  
   283  func IsBranchNameReserved(branch string) bool {
   284  	switch branch {
   285  	case "_current_branch", "main", "postgres", "template0", "template1":
   286  		return true
   287  	default:
   288  		return false
   289  	}
   290  }
   291  
   292  func MkdirIfNotExist(path string) error {
   293  	return MkdirIfNotExistFS(afero.NewOsFs(), path)
   294  }
   295  
   296  func MkdirIfNotExistFS(fsys afero.Fs, path string) error {
   297  	if err := fsys.MkdirAll(path, 0755); err != nil && !errors.Is(err, os.ErrExist) {
   298  		return errors.Errorf("failed to mkdir: %w", err)
   299  	}
   300  
   301  	return nil
   302  }
   303  
   304  func WriteFile(path string, contents []byte, fsys afero.Fs) error {
   305  	if err := MkdirIfNotExistFS(fsys, filepath.Dir(path)); err != nil {
   306  		return err
   307  	}
   308  	if err := afero.WriteFile(fsys, path, contents, 0644); err != nil {
   309  		return errors.Errorf("failed to write file: %w", err)
   310  	}
   311  	return nil
   312  }
   313  
   314  func AssertSupabaseCliIsSetUpFS(fsys afero.Fs) error {
   315  	if _, err := fsys.Stat(ConfigPath); errors.Is(err, os.ErrNotExist) {
   316  		return errors.Errorf("Cannot find %s in the current directory. Have you set up the project with %s?", Bold(ConfigPath), Aqua("supabase init"))
   317  	} else if err != nil {
   318  		return errors.Errorf("failed to read config file: %w", err)
   319  	}
   320  
   321  	return nil
   322  }
   323  
   324  func AssertProjectRefIsValid(projectRef string) error {
   325  	if !ProjectRefPattern.MatchString(projectRef) {
   326  		return errors.New(ErrInvalidRef)
   327  	}
   328  	return nil
   329  }
   330  
   331  func ValidateFunctionSlug(slug string) error {
   332  	if !FuncSlugPattern.MatchString(slug) {
   333  		return errors.New(ErrInvalidSlug)
   334  	}
   335  
   336  	return nil
   337  }
   338  
   339  func Ptr[T any](v T) *T {
   340  	return &v
   341  }
   342  
   343  func GetHostname() string {
   344  	host := Docker.DaemonHost()
   345  	if parsed, err := client.ParseHostURL(host); err == nil && parsed.Scheme == "tcp" {
   346  		if host, _, err := net.SplitHostPort(parsed.Host); err == nil {
   347  			return host
   348  		}
   349  	}
   350  	return "127.0.0.1"
   351  }