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

supabase / cli / 12001912209

25 Nov 2024 02:09AM UTC coverage: 59.524% (-0.03%) from 59.552%
12001912209

Pull #2906

github

sweatybridge
chore: revert to single quote
Pull Request #2906: chore: update api/beta.yaml from staging v1-yaml

8 of 14 new or added lines in 7 files covered. (57.14%)

5 existing lines in 1 file now uncovered.

6381 of 10720 relevant lines covered (59.52%)

6.05 hits per line

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

28.27
/internal/bootstrap/bootstrap.go
1
package bootstrap
2

3
import (
4
        "context"
5
        "encoding/json"
6
        "fmt"
7
        "net/http"
8
        "net/url"
9
        "os"
10
        "path/filepath"
11
        "strings"
12
        "time"
13

14
        "github.com/cenkalti/backoff/v4"
15
        "github.com/go-errors/errors"
16
        "github.com/google/go-github/v62/github"
17
        "github.com/jackc/pgconn"
18
        "github.com/jackc/pgx/v4"
19
        "github.com/joho/godotenv"
20
        "github.com/spf13/afero"
21
        "github.com/spf13/viper"
22
        "github.com/supabase/cli/internal/db/push"
23
        initBlank "github.com/supabase/cli/internal/init"
24
        "github.com/supabase/cli/internal/link"
25
        "github.com/supabase/cli/internal/login"
26
        "github.com/supabase/cli/internal/projects/apiKeys"
27
        "github.com/supabase/cli/internal/projects/create"
28
        "github.com/supabase/cli/internal/utils"
29
        "github.com/supabase/cli/internal/utils/flags"
30
        "github.com/supabase/cli/internal/utils/tenant"
31
        "github.com/supabase/cli/pkg/api"
32
        "github.com/supabase/cli/pkg/fetcher"
33
        "github.com/supabase/cli/pkg/queue"
34
        "golang.org/x/term"
35
)
36

37
func Run(ctx context.Context, starter StarterTemplate, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
×
38
        workdir := viper.GetString("WORKDIR")
×
39
        if !filepath.IsAbs(workdir) {
×
40
                workdir = filepath.Join(utils.CurrentDirAbs, workdir)
×
41
        }
×
42
        if err := utils.MkdirIfNotExistFS(fsys, workdir); err != nil {
×
43
                return err
×
44
        }
×
45
        if empty, err := afero.IsEmpty(fsys, workdir); err != nil {
×
46
                return errors.Errorf("failed to read workdir: %w", err)
×
47
        } else if !empty {
×
48
                title := fmt.Sprintf("Do you want to overwrite existing files in %s directory?", utils.Bold(workdir))
×
49
                if shouldOverwrite, err := utils.NewConsole().PromptYesNo(ctx, title, true); err != nil {
×
50
                        return err
×
51
                } else if !shouldOverwrite {
×
52
                        return errors.New(context.Canceled)
×
53
                }
×
54
        }
55
        if err := utils.ChangeWorkDir(fsys); err != nil {
×
56
                return err
×
57
        }
×
58
        // 0. Download starter template
59
        if len(starter.Url) > 0 {
×
60
                client := utils.GetGitHubClient(ctx)
×
61
                if err := downloadSample(ctx, client, starter.Url, fsys); err != nil {
×
62
                        return err
×
63
                }
×
64
        } else if err := initBlank.Run(ctx, fsys, nil, nil, utils.InitParams{Overwrite: true}); err != nil {
×
65
                return err
×
66
        }
×
67
        // 1. Login
68
        _, err := utils.LoadAccessTokenFS(fsys)
×
69
        if errors.Is(err, utils.ErrMissingToken) {
×
70
                if err := login.Run(ctx, os.Stdout, login.RunParams{
×
71
                        OpenBrowser: term.IsTerminal(int(os.Stdin.Fd())),
×
72
                        Fsys:        fsys,
×
73
                }); err != nil {
×
74
                        return err
×
75
                }
×
76
        } else if err != nil {
×
77
                return err
×
78
        }
×
79
        // 2. Create project
NEW
80
        params := api.V1CreateProjectBodyDto{
×
81
                Name:        filepath.Base(workdir),
×
82
                TemplateUrl: &starter.Url,
×
83
        }
×
84
        if err := create.Run(ctx, params, fsys); err != nil {
×
85
                return err
×
86
        }
×
87
        // 3. Get api keys
88
        var keys []api.ApiKeyResponse
×
89
        policy := newBackoffPolicy(ctx)
×
90
        if err := backoff.RetryNotify(func() error {
×
91
                fmt.Fprintln(os.Stderr, "Linking project...")
×
92
                keys, err = apiKeys.RunGetApiKeys(ctx, flags.ProjectRef)
×
93
                return err
×
94
        }, policy, newErrorCallback()); err != nil {
×
95
                return err
×
96
        }
×
97
        // 4. Link project
98
        if err := utils.LoadConfigFS(fsys); err != nil {
×
99
                return err
×
100
        }
×
101
        link.LinkServices(ctx, flags.ProjectRef, tenant.NewApiKey(keys).Anon, fsys)
×
102
        if err := utils.WriteFile(utils.ProjectRefPath, []byte(flags.ProjectRef), fsys); err != nil {
×
103
                return err
×
104
        }
×
105
        // 5. Wait for project healthy
106
        policy.Reset()
×
107
        if err := backoff.RetryNotify(func() error {
×
108
                fmt.Fprintln(os.Stderr, "Checking project health...")
×
109
                return checkProjectHealth(ctx)
×
110
        }, policy, newErrorCallback()); err != nil {
×
111
                return err
×
112
        }
×
113
        // 6. Push migrations
114
        config := flags.NewDbConfigWithPassword(flags.ProjectRef)
×
115
        if err := writeDotEnv(keys, config, fsys); err != nil {
×
116
                fmt.Fprintln(os.Stderr, "Failed to create .env file:", err)
×
117
        }
×
118
        policy.Reset()
×
119
        if err := backoff.RetryNotify(func() error {
×
120
                return push.Run(ctx, false, false, true, true, config, fsys)
×
121
        }, policy, newErrorCallback()); err != nil {
×
122
                return err
×
123
        }
×
124
        // 7. TODO: deploy functions
125
        utils.CmdSuggestion = suggestAppStart(utils.CurrentDirAbs, starter.Start)
×
126
        return nil
×
127
}
128

129
func suggestAppStart(cwd, command string) string {
3✔
130
        logger := utils.GetDebugLogger()
3✔
131
        workdir, err := os.Getwd()
3✔
132
        if err != nil {
3✔
133
                fmt.Fprintln(logger, err)
×
134
        }
×
135
        workdir, err = filepath.Rel(cwd, workdir)
3✔
136
        if err != nil {
4✔
137
                fmt.Fprintln(logger, err)
1✔
138
        }
1✔
139
        var cmd []string
3✔
140
        if len(workdir) > 0 && workdir != "." {
4✔
141
                cmd = append(cmd, "cd "+workdir)
1✔
142
        }
1✔
143
        if len(command) > 0 {
6✔
144
                cmd = append(cmd, command)
3✔
145
        }
3✔
146
        suggestion := "To start your app:"
3✔
147
        for _, c := range cmd {
7✔
148
                suggestion += fmt.Sprintf("\n  %s", utils.Aqua(c))
4✔
149
        }
4✔
150
        return suggestion
3✔
151
}
152

153
func checkProjectHealth(ctx context.Context) error {
×
154
        params := api.V1GetServicesHealthParams{
×
155
                Services: []api.V1GetServicesHealthParamsServices{
×
156
                        api.V1GetServicesHealthParamsServicesDb,
×
157
                },
×
158
        }
×
159
        resp, err := utils.GetSupabase().V1GetServicesHealthWithResponse(ctx, flags.ProjectRef, &params)
×
160
        if err != nil {
×
161
                return err
×
162
        }
×
163
        if resp.JSON200 == nil {
×
164
                return errors.Errorf("Error status %d: %s", resp.StatusCode(), resp.Body)
×
165
        }
×
166
        for _, service := range *resp.JSON200 {
×
167
                if !service.Healthy {
×
168
                        return errors.Errorf("Service not healthy: %s (%s)", service.Name, service.Status)
×
169
                }
×
170
        }
171
        return nil
×
172
}
173

174
const maxRetries = 8
175

176
func newBackoffPolicy(ctx context.Context) backoff.BackOffContext {
×
177
        b := backoff.ExponentialBackOff{
×
178
                InitialInterval:     3 * time.Second,
×
179
                RandomizationFactor: backoff.DefaultRandomizationFactor,
×
180
                Multiplier:          backoff.DefaultMultiplier,
×
181
                MaxInterval:         backoff.DefaultMaxInterval,
×
182
                MaxElapsedTime:      backoff.DefaultMaxElapsedTime,
×
183
                Stop:                backoff.Stop,
×
184
                Clock:               backoff.SystemClock,
×
185
        }
×
186
        b.Reset()
×
187
        return backoff.WithContext(backoff.WithMaxRetries(&b, maxRetries), ctx)
×
188
}
×
189

190
func newErrorCallback() backoff.Notify {
×
191
        failureCount := 0
×
192
        logger := utils.GetDebugLogger()
×
193
        return func(err error, d time.Duration) {
×
194
                failureCount += 1
×
195
                fmt.Fprintln(logger, err)
×
196
                fmt.Fprintf(os.Stderr, "Retry (%d/%d): ", failureCount, maxRetries)
×
197
        }
×
198
}
199

200
const (
201
        SUPABASE_SERVICE_ROLE_KEY = "SUPABASE_SERVICE_ROLE_KEY"
202
        SUPABASE_ANON_KEY         = "SUPABASE_ANON_KEY"
203
        SUPABASE_URL              = "SUPABASE_URL"
204
        POSTGRES_URL              = "POSTGRES_URL"
205
        // Derived keys
206
        POSTGRES_PRISMA_URL           = "POSTGRES_PRISMA_URL"
207
        POSTGRES_URL_NON_POOLING      = "POSTGRES_URL_NON_POOLING"
208
        POSTGRES_USER                 = "POSTGRES_USER"
209
        POSTGRES_HOST                 = "POSTGRES_HOST"
210
        POSTGRES_PASSWORD             = "POSTGRES_PASSWORD" //nolint:gosec
211
        POSTGRES_DATABASE             = "POSTGRES_DATABASE"
212
        NEXT_PUBLIC_SUPABASE_ANON_KEY = "NEXT_PUBLIC_SUPABASE_ANON_KEY"
213
        NEXT_PUBLIC_SUPABASE_URL      = "NEXT_PUBLIC_SUPABASE_URL"
214
        EXPO_PUBLIC_SUPABASE_ANON_KEY = "EXPO_PUBLIC_SUPABASE_ANON_KEY"
215
        EXPO_PUBLIC_SUPABASE_URL      = "EXPO_PUBLIC_SUPABASE_URL"
216
)
217

218
func writeDotEnv(keys []api.ApiKeyResponse, config pgconn.Config, fsys afero.Fs) error {
4✔
219
        // Initialise default envs
4✔
220
        transactionMode := *config.Copy()
4✔
221
        transactionMode.Port = 6543
4✔
222
        initial := map[string]string{
4✔
223
                SUPABASE_URL: "https://" + utils.GetSupabaseHost(flags.ProjectRef),
4✔
224
                POSTGRES_URL: utils.ToPostgresURL(transactionMode),
4✔
225
        }
4✔
226
        for _, entry := range keys {
8✔
227
                name := strings.ToUpper(entry.Name)
4✔
228
                key := fmt.Sprintf("SUPABASE_%s_KEY", name)
4✔
229
                initial[key] = entry.ApiKey
4✔
230
        }
4✔
231
        // Populate from .env.example if exists
232
        envs, err := parseExampleEnv(fsys)
4✔
233
        if err != nil {
5✔
234
                return err
1✔
235
        }
1✔
236
        for k, v := range envs {
16✔
237
                switch k {
13✔
238
                case SUPABASE_SERVICE_ROLE_KEY:
1✔
239
                case SUPABASE_ANON_KEY:
1✔
240
                case SUPABASE_URL:
1✔
241
                case POSTGRES_URL:
1✔
242
                // Derived keys
243
                case POSTGRES_PRISMA_URL:
1✔
244
                        initial[k] = initial[POSTGRES_URL]
1✔
245
                case POSTGRES_URL_NON_POOLING:
1✔
246
                        initial[k] = utils.ToPostgresURL(config)
1✔
247
                case POSTGRES_USER:
1✔
248
                        initial[k] = config.User
1✔
249
                case POSTGRES_HOST:
1✔
250
                        initial[k] = config.Host
1✔
251
                case POSTGRES_PASSWORD:
1✔
252
                        initial[k] = config.Password
1✔
253
                case POSTGRES_DATABASE:
1✔
254
                        initial[k] = config.Database
1✔
255
                case NEXT_PUBLIC_SUPABASE_ANON_KEY:
1✔
256
                        fallthrough
1✔
257
                case EXPO_PUBLIC_SUPABASE_ANON_KEY:
1✔
258
                        initial[k] = initial[SUPABASE_ANON_KEY]
1✔
259
                case NEXT_PUBLIC_SUPABASE_URL:
1✔
260
                        fallthrough
1✔
261
                case EXPO_PUBLIC_SUPABASE_URL:
1✔
262
                        initial[k] = initial[SUPABASE_URL]
1✔
263
                default:
1✔
264
                        initial[k] = v
1✔
265
                }
266
        }
267
        // Write to .env file
268
        out, err := godotenv.Marshal(initial)
3✔
269
        if err != nil {
3✔
270
                return errors.Errorf("failed to marshal env map: %w", err)
×
271
        }
×
272
        return utils.WriteFile(".env", []byte(out), fsys)
3✔
273
}
274

275
func parseExampleEnv(fsys afero.Fs) (map[string]string, error) {
4✔
276
        path := ".env.example"
4✔
277
        f, err := fsys.Open(path)
4✔
278
        if errors.Is(err, os.ErrNotExist) {
6✔
279
                return nil, nil
2✔
280
        } else if err != nil {
4✔
281
                return nil, errors.Errorf("failed to open %s: %w", path, err)
×
282
        }
×
283
        defer f.Close()
2✔
284
        envs, err := godotenv.Parse(f)
2✔
285
        if err != nil {
3✔
286
                return nil, errors.Errorf("failed to parse %s: %w", path, err)
1✔
287
        }
1✔
288
        return envs, nil
1✔
289
}
290

291
type samplesRepo struct {
292
        Samples []StarterTemplate `json:"samples"`
293
}
294

295
type StarterTemplate struct {
296
        Name        string `json:"name"`
297
        Description string `json:"description"`
298
        Url         string `json:"url"`
299
        Start       string `json:"start"`
300
}
301

302
func ListSamples(ctx context.Context, client *github.Client) ([]StarterTemplate, error) {
×
303
        owner := "supabase-community"
×
304
        repo := "supabase-samples"
×
305
        path := "samples.json"
×
306
        ref := "main"
×
307
        opts := github.RepositoryContentGetOptions{Ref: ref}
×
308
        file, _, _, err := client.Repositories.GetContents(ctx, owner, repo, path, &opts)
×
309
        if err != nil {
×
310
                return nil, errors.Errorf("failed to list samples: %w", err)
×
311
        }
×
312
        content, err := file.GetContent()
×
313
        if err != nil {
×
314
                return nil, errors.Errorf("failed to decode samples: %w", err)
×
315
        }
×
316
        var data samplesRepo
×
317
        if err := json.Unmarshal([]byte(content), &data); err != nil {
×
318
                return nil, errors.Errorf("failed to unmarshal samples: %w", err)
×
319
        }
×
320
        return data.Samples, nil
×
321
}
322

323
func downloadSample(ctx context.Context, client *github.Client, templateUrl string, fsys afero.Fs) error {
×
324
        fmt.Println("Downloading:", templateUrl)
×
325
        // https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-user-management
×
326
        parsed, err := url.Parse(templateUrl)
×
327
        if err != nil {
×
328
                return errors.Errorf("failed to parse template url: %w", err)
×
329
        }
×
330
        parts := strings.Split(parsed.Path, "/")
×
331
        owner := parts[1]
×
332
        repo := parts[2]
×
333
        ref := parts[4]
×
334
        root := strings.Join(parts[5:], "/")
×
335
        opts := github.RepositoryContentGetOptions{Ref: ref}
×
336
        queue := make([]string, 0)
×
337
        queue = append(queue, root)
×
338
        download := NewDownloader(5, fsys)
×
339
        for len(queue) > 0 {
×
340
                contentPath := queue[0]
×
341
                queue = queue[1:]
×
342
                _, directory, _, err := client.Repositories.GetContents(ctx, owner, repo, contentPath, &opts)
×
343
                if err != nil {
×
344
                        return errors.Errorf("failed to download template: %w", err)
×
345
                }
×
346
                for _, file := range directory {
×
347
                        switch file.GetType() {
×
348
                        case "file":
×
349
                                path := strings.TrimPrefix(file.GetPath(), root)
×
350
                                hostPath := filepath.Join(".", filepath.FromSlash(path))
×
351
                                if err := download.Start(ctx, hostPath, file.GetDownloadURL()); err != nil {
×
352
                                        return err
×
353
                                }
×
354
                        case "dir":
×
355
                                queue = append(queue, file.GetPath())
×
356
                        default:
×
357
                                fmt.Fprintf(os.Stderr, "Ignoring %s: %s\n", file.GetType(), file.GetPath())
×
358
                        }
359
                }
360
        }
361
        return download.Wait()
×
362
}
363

364
type Downloader struct {
365
        api   *fetcher.Fetcher
366
        queue *queue.JobQueue
367
        fsys  afero.Fs
368
}
369

370
func NewDownloader(concurrency uint, fsys afero.Fs) *Downloader {
×
371
        return &Downloader{
×
372
                api:   fetcher.NewFetcher("", fetcher.WithExpectedStatus(http.StatusOK)),
×
373
                queue: queue.NewJobQueue(concurrency),
×
374
                fsys:  fsys,
×
375
        }
×
376
}
×
377

378
func (d *Downloader) Start(ctx context.Context, localPath, remotePath string) error {
×
379
        job := func() error {
×
380
                resp, err := d.api.Send(ctx, http.MethodGet, remotePath, nil)
×
381
                if err != nil {
×
382
                        return err
×
383
                }
×
384
                defer resp.Body.Close()
×
385
                if err := afero.WriteReader(d.fsys, localPath, resp.Body); err != nil {
×
386
                        return errors.Errorf("failed to write file: %w", err)
×
387
                }
×
388
                return nil
×
389
        }
390
        return d.queue.Put(job)
×
391
}
392

393
func (d *Downloader) Wait() error {
×
394
        return d.queue.Collect()
×
395
}
×
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