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

supabase / cli / 3795253716

28 Dec 2022 04:24PM UTC coverage: 61.91% (+0.02%) from 61.889%
3795253716

Pull #747

github

Qiao Han
chore: fix tests
Pull Request #747: fix: deno upgrade version mismatch

8 of 8 new or added lines in 1 file covered. (100.0%)

3423 of 5529 relevant lines covered (61.91%)

1358.04 hits per line

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

65.42
/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
        "strconv"
20
        "strings"
21
        "time"
22

23
        "github.com/spf13/afero"
24
        "github.com/supabase/cli/internal/utils/credentials"
25
)
26

27
const (
28
        Pg13Image = "supabase/postgres:13.3.0"
29
        Pg14Image = "supabase/postgres:14.1.0.89"
30
        Pg15Image = "supabase/postgres:15.1.0.11"
31
        // Append to ServiceImages when adding new dependencies below
32
        KongImage       = "library/kong:2.8.1"
33
        InbucketImage   = "inbucket/inbucket:3.0.3"
34
        PostgrestImage  = "postgrest/postgrest:v10.1.1.20221215"
35
        DifferImage     = "supabase/pgadmin-schema-diff:cli-0.0.5"
36
        MigraImage      = "djrobstep/migra:3.0.1621480950"
37
        PgmetaImage     = "supabase/postgres-meta:v0.54.1"
38
        StudioImage     = "supabase/studio:20221214-4eecc99"
39
        DenoRelayImage  = "supabase/deno-relay:v1.5.0"
40
        ImageProxyImage = "supabase/imgproxy:v1.0.4"
41
        // Update initial schemas in internal/utils/templates/initial_schemas when
42
        // updating any one of these.
43
        GotrueImage   = "supabase/gotrue:v2.25.1"
44
        RealtimeImage = "supabase/realtime:v1.0.0-rc.11"
45
        StorageImage  = "supabase/storage-api:v0.26.1"
46
        // Should be kept in-sync with DenoRelayImage
47
        DenoVersion = "1.28.0"
48
)
49

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

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

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

87
        ConfigPath     = "supabase/config.toml"
88
        ProjectRefPath = "supabase/.temp/project-ref"
89
        RemoteDbPath   = "supabase/.temp/remote-db-url"
90
        CurrBranchPath = "supabase/.branches/_current_branch"
91
        MigrationsDir  = "supabase/migrations"
92
        FunctionsDir   = "supabase/functions"
93
        DbTestsDir     = "supabase/tests"
94
        SeedDataPath   = "supabase/seed.sql"
95
)
96

97
var (
98
        // pg_dumpall --globals-only --no-role-passwords --dbname $DB_URL \
99
        // | sed '/^CREATE ROLE postgres;/d' \
100
        // | sed '/^ALTER ROLE postgres WITH /d' \
101
        // | sed "/^ALTER ROLE .* WITH .* LOGIN /s/;$/ PASSWORD 'postgres';/"
102
        //go:embed templates/globals.sql
103
        GlobalsSql string
104

105
        //go:embed eszip/*
106
        eszipEmbedDir embed.FS
107

108
        AccessTokenPattern = regexp.MustCompile(`^sbp_[a-f0-9]{40}$`)
109
        ProjectRefPattern  = regexp.MustCompile(`^[a-z]{20}$`)
110
        PostgresUrlPattern = regexp.MustCompile(`^postgres(?:ql)?:\/\/postgres:(.*)@(.+)\/postgres$`)
111
        MigrateFilePattern = regexp.MustCompile(`^([0-9]+)_.*\.sql$`)
112
        BranchNamePattern  = regexp.MustCompile(`[[:word:]-]+`)
113
        FuncSlugPattern    = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]*$`)
114
        ImageNamePattern   = regexp.MustCompile(`\/(.*):`)
115

116
        // These schemas are ignored from schema diffs
117
        InternalSchemas = []string{
118
                "auth",
119
                "extensions",
120
                "graphql",
121
                "graphql_public",
122
                "pgbouncer",
123
                "pgsodium",
124
                "pgsodium_masks",
125
                "_realtime",
126
                "realtime",
127
                "storage",
128
                "supabase_functions",
129
                "supabase_migrations",
130
                "pg_catalog",
131
                "pg_toast",
132
                "information_schema",
133
        }
134
)
135

136
// Used by unit tests
137
var (
138
        DenoPathOverride string
139
)
140

141
func GetCurrentTimestamp() string {
6✔
142
        // Magic number: https://stackoverflow.com/q/45160822.
6✔
143
        return time.Now().UTC().Format("20060102150405")
6✔
144
}
6✔
145

146
func GetCurrentBranchFS(fsys afero.Fs) (string, error) {
18✔
147
        branch, err := afero.ReadFile(fsys, CurrBranchPath)
18✔
148
        if err != nil {
32✔
149
                return "", err
14✔
150
        }
14✔
151

152
        return string(branch), nil
4✔
153
}
154

155
// TODO: Make all errors use this.
156
func NewError(s string) error {
2✔
157
        // Ask runtime.Callers for up to 5 PCs, excluding runtime.Callers and NewError.
2✔
158
        pc := make([]uintptr, 5)
2✔
159
        n := runtime.Callers(2, pc)
2✔
160

2✔
161
        pc = pc[:n] // pass only valid pcs to runtime.CallersFrames
2✔
162
        frames := runtime.CallersFrames(pc)
2✔
163

2✔
164
        // Loop to get frames.
2✔
165
        // A fixed number of PCs can expand to an indefinite number of Frames.
2✔
166
        for {
12✔
167
                frame, more := frames.Next()
10✔
168

10✔
169
                // Process this frame.
10✔
170
                //
10✔
171
                // We're only interested in the stack trace in this repo.
10✔
172
                if strings.HasPrefix(frame.Function, "github.com/supabase/cli/internal") {
16✔
173
                        s += fmt.Sprintf("\n  in %s:%d", frame.Function, frame.Line)
6✔
174
                }
6✔
175

176
                // Check whether there are more frames to process after this one.
177
                if !more {
12✔
178
                        break
2✔
179
                }
180
        }
181

182
        return errors.New(s)
2✔
183
}
184

185
func AssertSupabaseDbIsRunning() error {
28✔
186
        if _, err := Docker.ContainerInspect(context.Background(), DbId); err != nil {
37✔
187
                return errors.New(Aqua("supabase start") + " is not running.")
9✔
188
        }
9✔
189

190
        return nil
19✔
191
}
192

193
func GetGitRoot(fsys afero.Fs) (*string, error) {
8✔
194
        origWd, err := os.Getwd()
8✔
195
        if err != nil {
8✔
196
                return nil, err
×
197
        }
×
198

199
        for {
41✔
200
                _, err := afero.ReadDir(fsys, ".git")
33✔
201

33✔
202
                if err == nil {
40✔
203
                        gitRoot, err := os.Getwd()
7✔
204
                        if err != nil {
7✔
205
                                return nil, err
×
206
                        }
×
207

208
                        if err := os.Chdir(origWd); err != nil {
7✔
209
                                return nil, err
×
210
                        }
×
211

212
                        return &gitRoot, nil
7✔
213
                }
214

215
                if cwd, err := os.Getwd(); err != nil {
26✔
216
                        return nil, err
×
217
                } else if isRootDirectory(cwd) {
27✔
218
                        return nil, nil
1✔
219
                }
1✔
220

221
                if err := os.Chdir(".."); err != nil {
25✔
222
                        return nil, err
×
223
                }
×
224
        }
225
}
226

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

245
func IsBranchNameReserved(branch string) bool {
13✔
246
        switch branch {
13✔
247
        case "_current_branch", "main", "postgres", "template0", "template1":
3✔
248
                return true
3✔
249
        default:
10✔
250
                return false
10✔
251
        }
252
}
253

254
func MkdirIfNotExist(path string) error {
×
255
        return MkdirIfNotExistFS(afero.NewOsFs(), path)
×
256
}
×
257

258
func MkdirIfNotExistFS(fsys afero.Fs, path string) error {
110✔
259
        if err := fsys.MkdirAll(path, 0755); err != nil && !errors.Is(err, os.ErrExist) {
121✔
260
                return err
11✔
261
        }
11✔
262

263
        return nil
99✔
264
}
265

266
func AssertSupabaseCliIsSetUpFS(fsys afero.Fs) error {
45✔
267
        if _, err := fsys.Stat(ConfigPath); errors.Is(err, os.ErrNotExist) {
51✔
268
                return errors.New("Cannot find " + Bold(ConfigPath) + " in the current directory. Have you set up the project with " + Aqua("supabase init") + "?")
6✔
269
        } else if err != nil {
45✔
270
                return err
×
271
        }
×
272

273
        return nil
39✔
274
}
275

276
func AssertIsLinkedFS(fsys afero.Fs) error {
×
277
        if _, err := fsys.Stat(ProjectRefPath); errors.Is(err, os.ErrNotExist) {
×
278
                return errors.New("Cannot find project ref. Have you run " + Aqua("supabase link") + "?")
×
279
        } else if err != nil {
×
280
                return err
×
281
        }
×
282

283
        return nil
×
284
}
285

286
func LoadProjectRef(fsys afero.Fs) (string, error) {
36✔
287
        projectRefBytes, err := afero.ReadFile(fsys, ProjectRefPath)
36✔
288
        if err != nil {
41✔
289
                return "", errors.New("Cannot find project ref. Have you run " + Aqua("supabase link") + "?")
5✔
290
        }
5✔
291
        projectRef := string(bytes.TrimSpace(projectRefBytes))
31✔
292
        if !ProjectRefPattern.MatchString(projectRef) {
36✔
293
                return "", errors.New("Invalid project ref format. Must be like `abcdefghijklmnopqrst`.")
5✔
294
        }
5✔
295
        return projectRef, nil
26✔
296
}
297

298
func GetDenoPath() (string, error) {
19✔
299
        if len(DenoPathOverride) > 0 {
38✔
300
                return DenoPathOverride, nil
19✔
301
        }
19✔
302
        home, err := os.UserHomeDir()
×
303
        if err != nil {
×
304
                return "", err
×
305
        }
×
306
        denoBinName := "deno"
×
307
        if runtime.GOOS == "windows" {
×
308
                denoBinName = "deno.exe"
×
309
        }
×
310
        denoPath := filepath.Join(home, ".supabase", denoBinName)
×
311
        return denoPath, nil
×
312
}
313

314
func InstallOrUpgradeDeno(ctx context.Context, fsys afero.Fs) error {
7✔
315
        denoPath, err := GetDenoPath()
7✔
316
        if err != nil {
7✔
317
                return err
×
318
        }
×
319

320
        if _, err := fsys.Stat(denoPath); err == nil {
11✔
321
                // Upgrade Deno.
4✔
322
                cmd := exec.CommandContext(ctx, denoPath, "upgrade", "--version", DenoVersion)
4✔
323
                cmd.Stderr = os.Stderr
4✔
324
                cmd.Stdout = os.Stdout
4✔
325
                return cmd.Run()
4✔
326
        } else if !errors.Is(err, os.ErrNotExist) {
7✔
327
                return err
×
328
        }
×
329

330
        // Install Deno.
331
        if err := MkdirIfNotExistFS(fsys, filepath.Dir(denoPath)); err != nil {
4✔
332
                return err
1✔
333
        }
1✔
334

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

356
        // 2. Download & install Deno binary.
357
        {
2✔
358
                assetUrl := fmt.Sprintf("https://github.com/%s/releases/download/v%s/%s", assetRepo, DenoVersion, assetFilename)
2✔
359
                req, err := http.NewRequestWithContext(ctx, "GET", assetUrl, nil)
2✔
360
                if err != nil {
2✔
361
                        return err
×
362
                }
×
363
                resp, err := http.DefaultClient.Do(req)
2✔
364
                if err != nil {
2✔
365
                        return err
×
366
                }
×
367
                defer resp.Body.Close()
2✔
368

2✔
369
                if resp.StatusCode != 200 {
2✔
370
                        return errors.New("Failed installing Deno binary.")
×
371
                }
×
372

373
                body, err := io.ReadAll(resp.Body)
2✔
374
                if err != nil {
2✔
375
                        return err
×
376
                }
×
377

378
                r, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
2✔
379
                // There should be only 1 file: the deno binary
2✔
380
                if len(r.File) != 1 {
2✔
381
                        return err
×
382
                }
×
383
                denoContents, err := r.File[0].Open()
2✔
384
                if err != nil {
2✔
385
                        return err
×
386
                }
×
387
                defer denoContents.Close()
2✔
388

2✔
389
                denoBytes, err := io.ReadAll(denoContents)
2✔
390
                if err != nil {
2✔
391
                        return err
×
392
                }
×
393

394
                if err := afero.WriteFile(fsys, denoPath, denoBytes, 0755); err != nil {
2✔
395
                        return err
×
396
                }
×
397
        }
398

399
        return nil
2✔
400
}
401

402
func isBuildScriptModified(fsys afero.Fs, buildScriptPath string) (bool, error) {
6✔
403
        bs, err := afero.ReadFile(fsys, buildScriptPath)
6✔
404
        if err != nil {
12✔
405
                if errors.Is(err, fs.ErrNotExist) {
12✔
406
                        return true, nil
6✔
407
                }
6✔
408
                return false, err
×
409
        }
410

411
        es, err := fs.ReadFile(eszipEmbedDir, "eszip/build.ts")
×
412
        if err != nil {
×
413
                return false, err
×
414
        }
×
415

416
        // compare the md5 checksum of current build script with user's copy.
417
        // if the checksums doesn't match, build script is modified.
418
        return md5.Sum(bs) != md5.Sum(es), nil
×
419
}
420

421
// Copy ESZIP scripts needed for function deploy, returning the build script path or an error.
422
func CopyEszipScripts(ctx context.Context, fsys afero.Fs) (string, error) {
6✔
423
        denoPath, err := GetDenoPath()
6✔
424
        if err != nil {
6✔
425
                return "", err
×
426
        }
×
427

428
        denoDirPath := filepath.Dir(denoPath)
6✔
429
        scriptDirPath := filepath.Join(denoDirPath, "eszip")
6✔
430
        buildScriptPath := filepath.Join(scriptDirPath, "build.ts")
6✔
431

6✔
432
        // make the script directory if not exist
6✔
433
        if err := MkdirIfNotExistFS(fsys, scriptDirPath); err != nil {
6✔
434
                return "", err
×
435
        }
×
436

437
        // check if the build script should be copied
438
        modified, err := isBuildScriptModified(fsys, buildScriptPath)
6✔
439
        if err != nil {
6✔
440
                return "", err
×
441
        }
×
442
        if !modified {
6✔
443
                return buildScriptPath, nil
×
444
        }
×
445

446
        // copy embed files to script directory
447
        err = fs.WalkDir(eszipEmbedDir, "eszip", func(path string, d fs.DirEntry, err error) error {
18✔
448
                if err != nil {
12✔
449
                        return err
×
450
                }
×
451

452
                // skip copying the directory
453
                if d.IsDir() {
18✔
454
                        return nil
6✔
455
                }
6✔
456

457
                contents, err := fs.ReadFile(eszipEmbedDir, path)
6✔
458
                if err != nil {
6✔
459
                        return err
×
460
                }
×
461

462
                if err := afero.WriteFile(fsys, filepath.Join(denoDirPath, path), contents, 0666); err != nil {
6✔
463
                        return err
×
464
                }
×
465

466
                return nil
6✔
467
        })
468

469
        if err != nil {
6✔
470
                return "", err
×
471
        }
×
472

473
        return filepath.Join(buildScriptPath), nil
6✔
474
}
475

476
func LoadAccessToken() (string, error) {
11✔
477
        return LoadAccessTokenFS(afero.NewOsFs())
11✔
478
}
11✔
479

480
func LoadAccessTokenFS(fsys afero.Fs) (string, error) {
11✔
481
        // Env takes precedence
11✔
482
        if accessToken := os.Getenv("SUPABASE_ACCESS_TOKEN"); accessToken != "" {
22✔
483
                if !AccessTokenPattern.MatchString(accessToken) {
11✔
484
                        return "", errors.New("Invalid access token format. Must be like `sbp_0102...1920`.")
×
485
                }
×
486
                return accessToken, nil
11✔
487
        }
488
        // Load from native credentials store
489
        if token, err := credentials.Get(AccessTokenKey); err == nil {
×
490
                return token, nil
×
491
        }
×
492
        // Fallback to home directory
493
        home, err := os.UserHomeDir()
×
494
        if err != nil {
×
495
                return "", err
×
496
        }
×
497
        accessTokenPath := filepath.Join(home, ".supabase", AccessTokenKey)
×
498
        accessToken, err := afero.ReadFile(fsys, accessTokenPath)
×
499
        if errors.Is(err, os.ErrNotExist) || string(accessToken) == "" {
×
500
                return "", errors.New("Access token not provided. Supply an access token by running " + Aqua("supabase login") + " or setting the SUPABASE_ACCESS_TOKEN environment variable.")
×
501
        } else if err != nil {
×
502
                return "", err
×
503
        }
×
504
        return string(accessToken), nil
×
505
}
506

507
func ValidateFunctionSlug(slug string) error {
20✔
508
        if !FuncSlugPattern.MatchString(slug) {
23✔
509
                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✔
510
        }
3✔
511

512
        return nil
17✔
513
}
514

515
func ShowStatus() {
2✔
516
        fmt.Println(`
2✔
517
         ` + Aqua("API URL") + `: http://localhost:` + strconv.FormatUint(uint64(Config.Api.Port), 10) + `
2✔
518
          ` + Aqua("DB URL") + `: postgresql://postgres:postgres@localhost:` + strconv.FormatUint(uint64(Config.Db.Port), 10) + `/postgres
2✔
519
      ` + Aqua("Studio URL") + `: http://localhost:` + strconv.FormatUint(uint64(Config.Studio.Port), 10) + `
2✔
520
    ` + Aqua("Inbucket URL") + `: http://localhost:` + strconv.FormatUint(uint64(Config.Inbucket.Port), 10) + `
2✔
521
      ` + Aqua("JWT secret") + `: ` + JWTSecret + `
2✔
522
        ` + Aqua("anon key") + `: ` + AnonKey + `
2✔
523
` + Aqua("service_role key") + `: ` + ServiceRoleKey)
2✔
524
}
2✔
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