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 }