• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / cli / 3955003912

19 Jan 2023 03:24AM UTC coverage: 62.321% (-0.5%) from 62.852%
3955003912

Pull #794

github

GitHub
Merge branch 'main' into function-download
Pull Request #794: feat: added functions download command

92 of 92 new or added lines in 4 files covered. (100.0%)

3609 of 5791 relevant lines covered (62.32%)

1297.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.85
/internal/utils/misc.go
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/spf13/afero"
23
        "github.com/supabase/cli/internal/utils/credentials"
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.21"
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.58.0"
37
        StudioImage     = "supabase/studio:20221214-4eecc99"
38
        DenoRelayImage  = "supabase/deno-relay:v1.5.0"
39
        ImageProxyImage = "darthsim/imgproxy:v3.8.0"
40
        // Update initial schemas in internal/utils/templates/initial_schemas when
41
        // updating any one of these.
42
        GotrueImage   = "supabase/gotrue:v2.40.1"
43
        RealtimeImage = "supabase/realtime:v2.1.0"
44
        StorageImage  = "supabase/storage-api:v0.26.1"
45
        // Should be kept in-sync with DenoRelayImage
46
        DenoVersion = "1.28.0"
47
)
48

49
var ServiceImages = []string{
50
        GotrueImage,
51
        RealtimeImage,
52
        StorageImage,
53
        ImageProxyImage,
54
        KongImage,
55
        InbucketImage,
56
        PostgrestImage,
57
        DifferImage,
58
        MigraImage,
59
        PgmetaImage,
60
        StudioImage,
61
        DenoRelayImage,
62
}
63

64
func ShortContainerImageName(imageName string) string {
528✔
65
        matches := ImageNamePattern.FindStringSubmatch(imageName)
528✔
66
        if len(matches) < 2 {
528✔
67
                return imageName
×
68
        }
×
69
        return matches[1]
528✔
70
}
71

72
const (
73
        // https://dba.stackexchange.com/a/11895
74
        // Args: dbname
75
        TerminateDbSqlFmt = `
76
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '%[1]s';
77
-- Wait for WAL sender to drop replication slot.
78
DO 'BEGIN WHILE (
79
        SELECT COUNT(*) FROM pg_replication_slots WHERE database = ''%[1]s''
80
) > 0 LOOP END LOOP; END';`
81
        AnonKey        = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"
82
        ServiceRoleKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU"
83
        JWTSecret      = "super-secret-jwt-token-with-at-least-32-characters-long"
84
        AccessTokenKey = "access-token"
85
)
86

87
var (
88
        // pg_dumpall --globals-only --no-role-passwords --dbname $DB_URL \
89
        // | sed '/^CREATE ROLE postgres;/d' \
90
        // | sed '/^ALTER ROLE postgres WITH /d' \
91
        // | sed "/^ALTER ROLE .* WITH .* LOGIN /s/;$/ PASSWORD 'postgres';/"
92
        //go:embed templates/globals.sql
93
        GlobalsSql string
94

95
        //go:embed denos/*
96
        denoEmbedDir embed.FS
97

98
        AccessTokenPattern = regexp.MustCompile(`^sbp_[a-f0-9]{40}$`)
99
        ProjectRefPattern  = regexp.MustCompile(`^[a-z]{20}$`)
100
        PostgresUrlPattern = regexp.MustCompile(`^postgres(?:ql)?:\/\/postgres:(.*)@(.+)\/postgres$`)
101
        MigrateFilePattern = regexp.MustCompile(`^([0-9]+)_.*\.sql$`)
102
        BranchNamePattern  = regexp.MustCompile(`[[:word:]-]+`)
103
        FuncSlugPattern    = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]*$`)
104
        ImageNamePattern   = regexp.MustCompile(`\/(.*):`)
105

106
        // These schemas are ignored from schema diffs
107
        SystemSchemas = []string{
108
                "information_schema",
109
                "pg_catalog",
110
                "pg_toast",
111
                // "pg_temp_1",
112
                // "pg_toast_temp_1",
113
                // Owned by extensions
114
                "cron",
115
                "graphql",
116
                "graphql_public",
117
                "net",
118
                "pgsodium",
119
                "pgsodium_masks",
120
                "vault",
121
        }
122
        InternalSchemas = append([]string{
123
                "auth",
124
                "extensions",
125
                "pgbouncer",
126
                "realtime",
127
                "_realtime",
128
                "storage",
129
                "supabase_functions",
130
                "supabase_migrations",
131
        }, SystemSchemas...)
132

133
        SupabaseDirPath       = "supabase"
134
        ConfigPath            = filepath.Join(SupabaseDirPath, "config.toml")
135
        ProjectRefPath        = filepath.Join(SupabaseDirPath, ".temp", "project-ref")
136
        RemoteDbPath          = filepath.Join(SupabaseDirPath, ".temp", "remote-db-url")
137
        CurrBranchPath        = filepath.Join(SupabaseDirPath, ".branches", "_current_branch")
138
        MigrationsDir         = filepath.Join(SupabaseDirPath, "migrations")
139
        FunctionsDir          = filepath.Join(SupabaseDirPath, "functions")
140
        FallbackImportMapPath = filepath.Join(FunctionsDir, "import_map.json")
141
        DbTestsDir            = filepath.Join(SupabaseDirPath, "tests")
142
        SeedDataPath          = filepath.Join(SupabaseDirPath, "seed.sql")
143
)
144

145
// Used by unit tests
146
var (
147
        DenoPathOverride string
148
)
149

150
func GetCurrentTimestamp() string {
6✔
151
        // Magic number: https://stackoverflow.com/q/45160822.
6✔
152
        return time.Now().UTC().Format("20060102150405")
6✔
153
}
6✔
154

155
func GetCurrentBranchFS(fsys afero.Fs) (string, error) {
15✔
156
        branch, err := afero.ReadFile(fsys, CurrBranchPath)
15✔
157
        if err != nil {
26✔
158
                return "", err
11✔
159
        }
11✔
160

161
        return string(branch), nil
4✔
162
}
163

164
// TODO: Make all errors use this.
165
func NewError(s string) error {
1✔
166
        // Ask runtime.Callers for up to 5 PCs, excluding runtime.Callers and NewError.
1✔
167
        pc := make([]uintptr, 5)
1✔
168
        n := runtime.Callers(2, pc)
1✔
169

1✔
170
        pc = pc[:n] // pass only valid pcs to runtime.CallersFrames
1✔
171
        frames := runtime.CallersFrames(pc)
1✔
172

1✔
173
        // Loop to get frames.
1✔
174
        // A fixed number of PCs can expand to an indefinite number of Frames.
1✔
175
        for {
6✔
176
                frame, more := frames.Next()
5✔
177

5✔
178
                // Process this frame.
5✔
179
                //
5✔
180
                // We're only interested in the stack trace in this repo.
5✔
181
                if strings.HasPrefix(frame.Function, "github.com/supabase/cli/internal") {
8✔
182
                        s += fmt.Sprintf("\n  in %s:%d", frame.Function, frame.Line)
3✔
183
                }
3✔
184

185
                // Check whether there are more frames to process after this one.
186
                if !more {
6✔
187
                        break
1✔
188
                }
189
        }
190

191
        return errors.New(s)
1✔
192
}
193

194
func AssertSupabaseDbIsRunning() error {
23✔
195
        if _, err := Docker.ContainerInspect(context.Background(), DbId); err != nil {
31✔
196
                return errors.New(Aqua("supabase start") + " is not running.")
8✔
197
        }
8✔
198

199
        return nil
15✔
200
}
201

202
func GetGitRoot(fsys afero.Fs) (*string, error) {
8✔
203
        origWd, err := os.Getwd()
8✔
204
        if err != nil {
8✔
205
                return nil, err
×
206
        }
×
207

208
        for {
41✔
209
                _, err := afero.ReadDir(fsys, ".git")
33✔
210

33✔
211
                if err == nil {
40✔
212
                        gitRoot, err := os.Getwd()
7✔
213
                        if err != nil {
7✔
214
                                return nil, err
×
215
                        }
×
216

217
                        if err := os.Chdir(origWd); err != nil {
7✔
218
                                return nil, err
×
219
                        }
×
220

221
                        return &gitRoot, nil
7✔
222
                }
223

224
                if cwd, err := os.Getwd(); err != nil {
26✔
225
                        return nil, err
×
226
                } else if isRootDirectory(cwd) {
27✔
227
                        return nil, nil
1✔
228
                }
1✔
229

230
                if err := os.Chdir(".."); err != nil {
25✔
231
                        return nil, err
×
232
                }
×
233
        }
234
}
235

236
// If the `os.Getwd()` is within a supabase project, this will return
237
// the root of the given project as the current working directory.
238
// Otherwise, the `os.Getwd()` is kept as is.
239
func GetProjectRoot(fsys afero.Fs) (string, error) {
3✔
240
        origWd, err := os.Getwd()
3✔
241
        for cwd := origWd; err == nil; cwd = filepath.Dir(cwd) {
27✔
242
                path := filepath.Join(cwd, ConfigPath)
24✔
243
                // Treat all errors as file not exists
24✔
244
                if isSupaProj, _ := afero.Exists(fsys, path); isSupaProj {
25✔
245
                        return cwd, nil
1✔
246
                }
1✔
247
                if isRootDirectory(cwd) {
25✔
248
                        break
2✔
249
                }
250
        }
251
        return origWd, err
2✔
252
}
253

254
func IsBranchNameReserved(branch string) bool {
13✔
255
        switch branch {
13✔
256
        case "_current_branch", "main", "postgres", "template0", "template1":
3✔
257
                return true
3✔
258
        default:
10✔
259
                return false
10✔
260
        }
261
}
262

263
func MkdirIfNotExist(path string) error {
×
264
        return MkdirIfNotExistFS(afero.NewOsFs(), path)
×
265
}
×
266

267
func MkdirIfNotExistFS(fsys afero.Fs, path string) error {
106✔
268
        if err := fsys.MkdirAll(path, 0755); err != nil && !errors.Is(err, os.ErrExist) {
116✔
269
                return err
10✔
270
        }
10✔
271

272
        return nil
96✔
273
}
274

275
func AssertSupabaseCliIsSetUpFS(fsys afero.Fs) error {
44✔
276
        if _, err := fsys.Stat(ConfigPath); errors.Is(err, os.ErrNotExist) {
50✔
277
                return errors.New("Cannot find " + Bold(ConfigPath) + " in the current directory. Have you set up the project with " + Aqua("supabase init") + "?")
6✔
278
        } else if err != nil {
44✔
279
                return err
×
280
        }
×
281

282
        return nil
38✔
283
}
284

285
func AssertIsLinkedFS(fsys afero.Fs) error {
×
286
        if _, err := fsys.Stat(ProjectRefPath); errors.Is(err, os.ErrNotExist) {
×
287
                return errors.New("Cannot find project ref. Have you run " + Aqua("supabase link") + "?")
×
288
        } else if err != nil {
×
289
                return err
×
290
        }
×
291

292
        return nil
×
293
}
294

295
func LoadProjectRef(fsys afero.Fs) (string, error) {
35✔
296
        projectRefBytes, err := afero.ReadFile(fsys, ProjectRefPath)
35✔
297
        if err != nil {
40✔
298
                return "", errors.New("Cannot find project ref. Have you run " + Aqua("supabase link") + "?")
5✔
299
        }
5✔
300
        projectRef := string(bytes.TrimSpace(projectRefBytes))
30✔
301
        if !ProjectRefPattern.MatchString(projectRef) {
35✔
302
                return "", errors.New("Invalid project ref format. Must be like `abcdefghijklmnopqrst`.")
5✔
303
        }
5✔
304
        return projectRef, nil
25✔
305
}
306

307
func GetDenoPath() (string, error) {
28✔
308
        if len(DenoPathOverride) > 0 {
56✔
309
                return DenoPathOverride, nil
28✔
310
        }
28✔
311
        home, err := os.UserHomeDir()
×
312
        if err != nil {
×
313
                return "", err
×
314
        }
×
315
        denoBinName := "deno"
×
316
        if runtime.GOOS == "windows" {
×
317
                denoBinName = "deno.exe"
×
318
        }
×
319
        denoPath := filepath.Join(home, ".supabase", denoBinName)
×
320
        return denoPath, nil
×
321
}
322

323
func InstallOrUpgradeDeno(ctx context.Context, fsys afero.Fs) error {
10✔
324
        denoPath, err := GetDenoPath()
10✔
325
        if err != nil {
10✔
326
                return err
×
327
        }
×
328

329
        if _, err := fsys.Stat(denoPath); err == nil {
17✔
330
                // Upgrade Deno.
7✔
331
                cmd := exec.CommandContext(ctx, denoPath, "upgrade", "--version", DenoVersion)
7✔
332
                cmd.Stderr = os.Stderr
7✔
333
                cmd.Stdout = os.Stdout
7✔
334
                return cmd.Run()
7✔
335
        } else if !errors.Is(err, os.ErrNotExist) {
10✔
336
                return err
×
337
        }
×
338

339
        // Install Deno.
340
        if err := MkdirIfNotExistFS(fsys, filepath.Dir(denoPath)); err != nil {
4✔
341
                return err
1✔
342
        }
1✔
343

344
        // 1. Determine OS triple
345
        var assetFilename string
2✔
346
        assetRepo := "denoland/deno"
2✔
347
        {
4✔
348
                if runtime.GOOS == "darwin" && runtime.GOARCH == "amd64" {
2✔
349
                        assetFilename = "deno-x86_64-apple-darwin.zip"
×
350
                } else if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
2✔
351
                        assetFilename = "deno-aarch64-apple-darwin.zip"
×
352
                } else if runtime.GOOS == "linux" && runtime.GOARCH == "amd64" {
4✔
353
                        assetFilename = "deno-x86_64-unknown-linux-gnu.zip"
2✔
354
                } else if runtime.GOOS == "linux" && runtime.GOARCH == "arm64" {
2✔
355
                        // TODO: version pin to official release once available https://github.com/denoland/deno/issues/1846
×
356
                        assetRepo = "LukeChannings/deno-arm64"
×
357
                        assetFilename = "deno-linux-arm64.zip"
×
358
                } else if runtime.GOOS == "windows" && runtime.GOARCH == "amd64" {
×
359
                        assetFilename = "deno-x86_64-pc-windows-msvc.zip"
×
360
                } else {
×
361
                        return errors.New("Platform " + runtime.GOOS + "/" + runtime.GOARCH + " is currently unsupported for Functions.")
×
362
                }
×
363
        }
364

365
        // 2. Download & install Deno binary.
366
        {
2✔
367
                assetUrl := fmt.Sprintf("https://github.com/%s/releases/download/v%s/%s", assetRepo, DenoVersion, assetFilename)
2✔
368
                req, err := http.NewRequestWithContext(ctx, "GET", assetUrl, nil)
2✔
369
                if err != nil {
2✔
370
                        return err
×
371
                }
×
372
                resp, err := http.DefaultClient.Do(req)
2✔
373
                if err != nil {
2✔
374
                        return err
×
375
                }
×
376
                defer resp.Body.Close()
2✔
377

2✔
378
                if resp.StatusCode != 200 {
2✔
379
                        return errors.New("Failed installing Deno binary.")
×
380
                }
×
381

382
                body, err := io.ReadAll(resp.Body)
2✔
383
                if err != nil {
2✔
384
                        return err
×
385
                }
×
386

387
                r, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
2✔
388
                // There should be only 1 file: the deno binary
2✔
389
                if len(r.File) != 1 {
2✔
390
                        return err
×
391
                }
×
392
                denoContents, err := r.File[0].Open()
2✔
393
                if err != nil {
2✔
394
                        return err
×
395
                }
×
396
                defer denoContents.Close()
2✔
397

2✔
398
                denoBytes, err := io.ReadAll(denoContents)
2✔
399
                if err != nil {
2✔
400
                        return err
×
401
                }
×
402

403
                if err := afero.WriteFile(fsys, denoPath, denoBytes, 0755); err != nil {
2✔
404
                        return err
×
405
                }
×
406
        }
407

408
        return nil
2✔
409
}
410

411
func isScriptModified(fsys afero.Fs, destPath string, src []byte) (bool, error) {
18✔
412
        dest, err := afero.ReadFile(fsys, destPath)
18✔
413
        if err != nil {
36✔
414
                if errors.Is(err, fs.ErrNotExist) {
36✔
415
                        return true, nil
18✔
416
                }
18✔
417
                return false, err
×
418
        }
419

420
        // compare the md5 checksum of src bytes with user's copy.
421
        // if the checksums doesn't match, script is modified.
422
        return md5.Sum(dest) != md5.Sum(src), nil
×
423
}
424

425
type DenoScriptDir struct {
426
        ExtractPath string
427
        BuildPath   string
428
}
429

430
// Copy Deno scripts needed for function deploy and downloads, returning a DenoScriptDir struct or an error.
431
func CopyDenoScripts(ctx context.Context, fsys afero.Fs) (*DenoScriptDir, error) {
9✔
432
        denoPath, err := GetDenoPath()
9✔
433
        if err != nil {
9✔
434
                return nil, err
×
435
        }
×
436

437
        denoDirPath := filepath.Dir(denoPath)
9✔
438
        scriptDirPath := filepath.Join(denoDirPath, "denos")
9✔
439

9✔
440
        // make the script directory if not exist
9✔
441
        if err := MkdirIfNotExistFS(fsys, scriptDirPath); err != nil {
9✔
442
                return nil, err
×
443
        }
×
444

445
        // copy embed files to script directory
446
        err = fs.WalkDir(denoEmbedDir, "denos", func(path string, d fs.DirEntry, err error) error {
36✔
447
                if err != nil {
27✔
448
                        return err
×
449
                }
×
450

451
                // skip copying the directory
452
                if d.IsDir() {
36✔
453
                        return nil
9✔
454
                }
9✔
455

456
                destPath := filepath.Join(denoDirPath, path)
18✔
457

18✔
458
                contents, err := fs.ReadFile(denoEmbedDir, path)
18✔
459
                if err != nil {
18✔
460
                        return err
×
461
                }
×
462

463
                // check if the script should be copied
464
                modified, err := isScriptModified(fsys, destPath, contents)
18✔
465
                if err != nil {
18✔
466
                        return err
×
467
                }
×
468
                if !modified {
18✔
469
                        return nil
×
470
                }
×
471

472
                if err := afero.WriteFile(fsys, filepath.Join(denoDirPath, path), contents, 0666); err != nil {
18✔
473
                        return err
×
474
                }
×
475

476
                return nil
18✔
477
        })
478

479
        if err != nil {
9✔
480
                return nil, err
×
481
        }
×
482

483
        sd := DenoScriptDir{
9✔
484
                ExtractPath: filepath.Join(scriptDirPath, "extract.ts"),
9✔
485
                BuildPath:   filepath.Join(scriptDirPath, "build.ts"),
9✔
486
        }
9✔
487

9✔
488
        return &sd, nil
9✔
489
}
490

491
func LoadAccessToken() (string, error) {
11✔
492
        return LoadAccessTokenFS(afero.NewOsFs())
11✔
493
}
11✔
494

495
func LoadAccessTokenFS(fsys afero.Fs) (string, error) {
11✔
496
        // Env takes precedence
11✔
497
        if accessToken := os.Getenv("SUPABASE_ACCESS_TOKEN"); accessToken != "" {
22✔
498
                if !AccessTokenPattern.MatchString(accessToken) {
11✔
499
                        return "", errors.New("Invalid access token format. Must be like `sbp_0102...1920`.")
×
500
                }
×
501
                return accessToken, nil
11✔
502
        }
503
        // Load from native credentials store
504
        if token, err := credentials.Get(AccessTokenKey); err == nil {
×
505
                return token, nil
×
506
        }
×
507
        // Fallback to home directory
508
        home, err := os.UserHomeDir()
×
509
        if err != nil {
×
510
                return "", err
×
511
        }
×
512
        accessTokenPath := filepath.Join(home, ".supabase", AccessTokenKey)
×
513
        accessToken, err := afero.ReadFile(fsys, accessTokenPath)
×
514
        if errors.Is(err, os.ErrNotExist) || string(accessToken) == "" {
×
515
                return "", errors.New("Access token not provided. Supply an access token by running " + Aqua("supabase login") + " or setting the SUPABASE_ACCESS_TOKEN environment variable.")
×
516
        } else if err != nil {
×
517
                return "", err
×
518
        }
×
519
        return string(accessToken), nil
×
520
}
521

522
func ValidateFunctionSlug(slug string) error {
23✔
523
        if !FuncSlugPattern.MatchString(slug) {
26✔
524
                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_-]*$)")
3✔
525
        }
3✔
526

527
        return nil
20✔
528
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc