288 lines
8.4 KiB
Go
288 lines
8.4 KiB
Go
package utils
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"time"
|
|
|
|
"github.com/docker/docker/client"
|
|
"github.com/go-errors/errors"
|
|
"github.com/go-git/go-git/v5"
|
|
"github.com/spf13/afero"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
// Assigned using `-ldflags` https://stackoverflow.com/q/11354518
|
|
var (
|
|
Version string
|
|
SentryDsn string
|
|
)
|
|
|
|
func ShortContainerImageName(imageName string) string {
|
|
matches := ImageNamePattern.FindStringSubmatch(imageName)
|
|
if len(matches) < 2 {
|
|
return imageName
|
|
}
|
|
return matches[1]
|
|
}
|
|
|
|
const SuggestDebugFlag = "Try rerunning the command with --debug to troubleshoot the error."
|
|
|
|
var (
|
|
CmdSuggestion string
|
|
CurrentDirAbs string
|
|
|
|
// pg_dumpall --globals-only --no-role-passwords --dbname $DB_URL \
|
|
// | sed '/^CREATE ROLE postgres;/d' \
|
|
// | sed '/^ALTER ROLE postgres WITH /d' \
|
|
// | sed "/^ALTER ROLE .* WITH .* LOGIN /s/;$/ PASSWORD 'postgres';/"
|
|
//go:embed templates/globals.sql
|
|
GlobalsSql string
|
|
|
|
ProjectRefPattern = regexp.MustCompile(`^[a-z]{20}$`)
|
|
UUIDPattern = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
|
|
ProjectHostPattern = regexp.MustCompile(`^(db\.)([a-z]{20})\.supabase\.(co|red)$`)
|
|
BranchNamePattern = regexp.MustCompile(`[[:word:]-]+`)
|
|
FuncSlugPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]*$`)
|
|
ImageNamePattern = regexp.MustCompile(`\/(.*):`)
|
|
|
|
// These schemas are ignored from db diff and db dump
|
|
PgSchemas = []string{
|
|
"information_schema",
|
|
"pg_*", // Wildcard pattern follows pg_dump
|
|
}
|
|
// Initialised by postgres image and owned by postgres role
|
|
InternalSchemas = append([]string{
|
|
"_analytics",
|
|
"_realtime",
|
|
"_supavisor",
|
|
"auth",
|
|
"extensions",
|
|
"pgbouncer",
|
|
"realtime",
|
|
"storage",
|
|
"supabase_functions",
|
|
"supabase_migrations",
|
|
// Owned by extensions
|
|
"cron",
|
|
"dbdev",
|
|
"graphql",
|
|
"graphql_public",
|
|
"net",
|
|
"pgmq",
|
|
"pgsodium",
|
|
"pgsodium_masks",
|
|
"pgtle",
|
|
"repack",
|
|
"tiger",
|
|
"tiger_data",
|
|
"timescaledb_*",
|
|
"_timescaledb_*",
|
|
"topology",
|
|
"vault",
|
|
}, PgSchemas...)
|
|
ReservedRoles = []string{
|
|
"anon",
|
|
"authenticated",
|
|
"authenticator",
|
|
"dashboard_user",
|
|
"pgbouncer",
|
|
"postgres",
|
|
"service_role",
|
|
"supabase_admin",
|
|
"supabase_auth_admin",
|
|
"supabase_functions_admin",
|
|
"supabase_read_only_user",
|
|
"supabase_realtime_admin",
|
|
"supabase_replication_admin",
|
|
"supabase_storage_admin",
|
|
// Managed by extensions
|
|
"pgsodium_keyholder",
|
|
"pgsodium_keyiduser",
|
|
"pgsodium_keymaker",
|
|
"pgtle_admin",
|
|
}
|
|
AllowedConfigs = []string{
|
|
// Ref: https://github.com/supabase/postgres/blob/develop/ansible/files/postgresql_config/supautils.conf.j2#L10
|
|
"pgaudit.*",
|
|
"pgrst.*",
|
|
"session_replication_role",
|
|
"statement_timeout",
|
|
"track_io_timing",
|
|
}
|
|
|
|
SupabaseDirPath = "supabase"
|
|
ConfigPath = filepath.Join(SupabaseDirPath, "config.toml")
|
|
GitIgnorePath = filepath.Join(SupabaseDirPath, ".gitignore")
|
|
TempDir = filepath.Join(SupabaseDirPath, ".temp")
|
|
ImportMapsDir = filepath.Join(TempDir, "import_maps")
|
|
ProjectRefPath = filepath.Join(TempDir, "project-ref")
|
|
PoolerUrlPath = filepath.Join(TempDir, "pooler-url")
|
|
PostgresVersionPath = filepath.Join(TempDir, "postgres-version")
|
|
GotrueVersionPath = filepath.Join(TempDir, "gotrue-version")
|
|
RestVersionPath = filepath.Join(TempDir, "rest-version")
|
|
StorageVersionPath = filepath.Join(TempDir, "storage-version")
|
|
StudioVersionPath = filepath.Join(TempDir, "studio-version")
|
|
PgmetaVersionPath = filepath.Join(TempDir, "pgmeta-version")
|
|
PoolerVersionPath = filepath.Join(TempDir, "pooler-version")
|
|
RealtimeVersionPath = filepath.Join(TempDir, "realtime-version")
|
|
CliVersionPath = filepath.Join(TempDir, "cli-latest")
|
|
CurrBranchPath = filepath.Join(SupabaseDirPath, ".branches", "_current_branch")
|
|
SchemasDir = filepath.Join(SupabaseDirPath, "schemas")
|
|
MigrationsDir = filepath.Join(SupabaseDirPath, "migrations")
|
|
FunctionsDir = filepath.Join(SupabaseDirPath, "functions")
|
|
FallbackImportMapPath = filepath.Join(FunctionsDir, "import_map.json")
|
|
FallbackEnvFilePath = filepath.Join(FunctionsDir, ".env")
|
|
DbTestsDir = filepath.Join(SupabaseDirPath, "tests")
|
|
CustomRolesPath = filepath.Join(SupabaseDirPath, "roles.sql")
|
|
|
|
ErrNotLinked = errors.Errorf("Cannot find project ref. Have you run %s?", Aqua("supabase link"))
|
|
ErrInvalidRef = errors.New("Invalid project ref format. Must be like `abcdefghijklmnopqrst`.")
|
|
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_-]*$)")
|
|
ErrNotRunning = errors.Errorf("%s is not running.", Aqua("supabase start"))
|
|
)
|
|
|
|
func GetCurrentTimestamp() string {
|
|
// Magic number: https://stackoverflow.com/q/45160822.
|
|
return time.Now().UTC().Format("20060102150405")
|
|
}
|
|
|
|
func GetCurrentBranchFS(fsys afero.Fs) (string, error) {
|
|
branch, err := afero.ReadFile(fsys, CurrBranchPath)
|
|
if err != nil {
|
|
return "", errors.Errorf("failed to load current branch: %w", err)
|
|
}
|
|
|
|
return string(branch), nil
|
|
}
|
|
|
|
func AssertSupabaseDbIsRunning() error {
|
|
return AssertServiceIsRunning(context.Background(), DbId)
|
|
}
|
|
|
|
func AssertServiceIsRunning(ctx context.Context, containerId string) error {
|
|
if _, err := Docker.ContainerInspect(ctx, containerId); err != nil {
|
|
if client.IsErrNotFound(err) {
|
|
return errors.New(ErrNotRunning)
|
|
}
|
|
if client.IsErrConnectionFailed(err) {
|
|
CmdSuggestion = suggestDockerInstall
|
|
}
|
|
return errors.Errorf("failed to inspect service: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func IsGitRepo() bool {
|
|
opts := &git.PlainOpenOptions{DetectDotGit: true}
|
|
_, err := git.PlainOpenWithOptions(".", opts)
|
|
return err == nil
|
|
}
|
|
|
|
// If the `os.Getwd()` is within a supabase project, this will return
|
|
// the root of the given project as the current working directory.
|
|
// Otherwise, the `os.Getwd()` is kept as is.
|
|
func getProjectRoot(absPath string, fsys afero.Fs) string {
|
|
for cwd := absPath; ; cwd = filepath.Dir(cwd) {
|
|
path := filepath.Join(cwd, ConfigPath)
|
|
// Treat all errors as file not exists
|
|
if isSupaProj, err := afero.Exists(fsys, path); isSupaProj {
|
|
return cwd
|
|
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
logger := GetDebugLogger()
|
|
fmt.Fprintln(logger, err)
|
|
}
|
|
if isRootDirectory(cwd) {
|
|
break
|
|
}
|
|
}
|
|
return absPath
|
|
}
|
|
|
|
func isRootDirectory(cleanPath string) bool {
|
|
// A cleaned path only ends with separator if it is root
|
|
return os.IsPathSeparator(cleanPath[len(cleanPath)-1])
|
|
}
|
|
|
|
func ChangeWorkDir(fsys afero.Fs) error {
|
|
// Track the original workdir before changing to project root
|
|
if !filepath.IsAbs(CurrentDirAbs) {
|
|
var err error
|
|
if CurrentDirAbs, err = os.Getwd(); err != nil {
|
|
return errors.Errorf("failed to get current directory: %w", err)
|
|
}
|
|
}
|
|
workdir := viper.GetString("WORKDIR")
|
|
if len(workdir) == 0 {
|
|
workdir = getProjectRoot(CurrentDirAbs, fsys)
|
|
}
|
|
if err := os.Chdir(workdir); err != nil {
|
|
return errors.Errorf("failed to change workdir: %w", err)
|
|
}
|
|
if cwd, err := os.Getwd(); err == nil && cwd != CurrentDirAbs {
|
|
fmt.Fprintln(os.Stderr, "Using workdir", Bold(workdir))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func IsBranchNameReserved(branch string) bool {
|
|
switch branch {
|
|
case "_current_branch", "main", "postgres", "template0", "template1":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func MkdirIfNotExist(path string) error {
|
|
return MkdirIfNotExistFS(afero.NewOsFs(), path)
|
|
}
|
|
|
|
func MkdirIfNotExistFS(fsys afero.Fs, path string) error {
|
|
if err := fsys.MkdirAll(path, 0755); err != nil && !errors.Is(err, os.ErrExist) {
|
|
return errors.Errorf("failed to mkdir: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func WriteFile(path string, contents []byte, fsys afero.Fs) error {
|
|
if err := MkdirIfNotExistFS(fsys, filepath.Dir(path)); err != nil {
|
|
return err
|
|
}
|
|
if err := afero.WriteFile(fsys, path, contents, 0644); err != nil {
|
|
return errors.Errorf("failed to write file: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func AssertProjectRefIsValid(projectRef string) error {
|
|
if !ProjectRefPattern.MatchString(projectRef) {
|
|
return errors.New(ErrInvalidRef)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ValidateFunctionSlug(slug string) error {
|
|
if !FuncSlugPattern.MatchString(slug) {
|
|
return errors.New(ErrInvalidSlug)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func GetHostname() string {
|
|
host := Docker.DaemonHost()
|
|
if parsed, err := client.ParseHostURL(host); err == nil && parsed.Scheme == "tcp" {
|
|
if host, _, err := net.SplitHostPort(parsed.Host); err == nil {
|
|
return host
|
|
}
|
|
}
|
|
return "127.0.0.1"
|
|
}
|