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" }