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

supabase / cli / 6936651354

20 Nov 2023 10:38PM UTC coverage: 58.17% (+0.001%) from 58.169%
6936651354

Pull #1679

github

sweatybridge
fix: experimental version comparison for orioledb
Pull Request #1679: fix: experimental version comparison for orioledb

4 of 6 new or added lines in 1 file covered. (66.67%)

5 existing lines in 1 file now uncovered.

5778 of 9933 relevant lines covered (58.17%)

730.19 hits per line

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

49.65
/internal/utils/config.go
1
package utils
2

3
import (
4
        "bytes"
5
        _ "embed"
6
        "errors"
7
        "fmt"
8
        "os"
9
        "path/filepath"
10
        "regexp"
11
        "strings"
12
        "text/template"
13

14
        "github.com/BurntSushi/toml"
15
        "github.com/docker/go-units"
16
        "github.com/joho/godotenv"
17
        "github.com/spf13/afero"
18
        "github.com/spf13/viper"
19
)
20

21
var (
22
        NetId         string
23
        DbId          string
24
        ConfigId      string
25
        KongId        string
26
        GotrueId      string
27
        InbucketId    string
28
        RealtimeId    string
29
        RestId        string
30
        StorageId     string
31
        ImgProxyId    string
32
        DifferId      string
33
        PgmetaId      string
34
        StudioId      string
35
        EdgeRuntimeId string
36
        LogflareId    string
37
        VectorId      string
38
        PoolerId      string
39

40
        DbAliases          = []string{"db", "db.supabase.internal"}
41
        KongAliases        = []string{"kong", "api.supabase.internal"}
42
        GotrueAliases      = []string{"auth"}
43
        InbucketAliases    = []string{"inbucket"}
44
        RealtimeAliases    = []string{"realtime"}
45
        RestAliases        = []string{"rest"}
46
        StorageAliases     = []string{"storage"}
47
        ImgProxyAliases    = []string{"imgproxy"}
48
        PgmetaAliases      = []string{"pg_meta"}
49
        StudioAliases      = []string{"studio"}
50
        EdgeRuntimeAliases = []string{"edge_runtime"}
51
        LogflareAliases    = []string{"analytics"}
52
        VectorAliases      = []string{"vector"}
53
        PoolerAliases      = []string{"pooler"}
54

55
        InitialSchemaSql string
56
        //go:embed templates/initial_schemas/13.sql
57
        InitialSchemaPg13Sql string
58
        //go:embed templates/initial_schemas/14.sql
59
        InitialSchemaPg14Sql string
60

61
        //go:embed templates/init_config.toml
62
        initConfigEmbed    string
63
        initConfigTemplate = template.Must(template.New("initConfig").Parse(initConfigEmbed))
64
        invalidProjectId   = regexp.MustCompile("[^a-zA-Z0-9_.-]+")
65
        envPattern         = regexp.MustCompile(`^env\((.*)\)$`)
66
)
67

68
func GetId(name string) string {
752✔
69
        return "supabase_" + name + "_" + Config.ProjectId
752✔
70
}
752✔
71

72
// Type for turning human-friendly bytes string ("5MB", "32kB") into an int64 during toml decoding.
73
type sizeInBytes int64
74

75
func (s *sizeInBytes) UnmarshalText(text []byte) error {
122✔
76
        size, err := units.RAMInBytes(string(text))
122✔
77
        if err == nil {
243✔
78
                *s = sizeInBytes(size)
121✔
79
        }
121✔
80
        return err
122✔
81
}
82

83
func (s sizeInBytes) MarshalText() (text []byte, err error) {
×
84
        return []byte(units.BytesSize(float64(s))), nil
×
85
}
×
86

87
type LogflareBackend string
88

89
const (
90
        LogflarePostgres LogflareBackend = "postgres"
91
        LogflareBigQuery LogflareBackend = "bigquery"
92
)
93

94
type PoolMode string
95

96
const (
97
        TransactionMode PoolMode = "transaction"
98
        SessionMode     PoolMode = "session"
99
)
100

101
type AddressFamily string
102

103
const (
104
        AddressIPv6 AddressFamily = "IPv6"
105
        AddressIPv4 AddressFamily = "IPv4"
106
)
107

108
var Config = config{
109
        Api: api{
110
                Image: PostgrestImage,
111
        },
112
        Db: db{
113
                Image:    Pg15Image,
114
                Password: "postgres",
115
                RootKey:  "d4dc5b6d4a1d6a10b2c1e76112c994d65db7cec380572cc1839624d4be3fa275",
116
        },
117
        Realtime: realtime{
118
                IpVersion: AddressIPv6,
119
        },
120
        Storage: storage{
121
                Image: StorageImage,
122
        },
123
        Auth: auth{
124
                Image: GotrueImage,
125
                Email: email{
126
                        Template: map[string]emailTemplate{
127
                                "invite":       {},
128
                                "confirmation": {},
129
                                "recovery":     {},
130
                                "magic_link":   {},
131
                                "email_change": {},
132
                        },
133
                },
134
                Sms: sms{
135
                        Template: "Your code is {{ .Code }} .",
136
                },
137
                External: map[string]provider{
138
                        "apple":     {},
139
                        "azure":     {},
140
                        "bitbucket": {},
141
                        "discord":   {},
142
                        "facebook":  {},
143
                        "github":    {},
144
                        "gitlab":    {},
145
                        "google":    {},
146
                        "keycloak":  {},
147
                        "linkedin":  {},
148
                        "notion":    {},
149
                        "twitch":    {},
150
                        "twitter":   {},
151
                        "slack":     {},
152
                        "spotify":   {},
153
                        "workos":    {},
154
                        "zoom":      {},
155
                },
156
                JwtExpiry:      3600,
157
                JwtSecret:      "super-secret-jwt-token-with-at-least-32-characters-long",
158
                AnonKey:        "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0",
159
                ServiceRoleKey: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU",
160
        },
161
        Analytics: analytics{
162
                ApiKey: "api-key",
163
                // Defaults to bigquery for backwards compatibility with existing config.toml
164
                Backend: LogflareBigQuery,
165
        },
166
}
167

168
// We follow these rules when adding new config:
169
//  1. Update init_config.toml with the new key, default value, and comments to explain usage.
170
//  2. Update config struct with new field and toml tag (spelled in snake_case).
171
//  3. Add custom field validations to LoadConfigFS function for eg. integer range checks.
172
//
173
// If you are adding new user defined secrets, such as OAuth provider secret, the default value in
174
// init_config.toml should be an env var substitution. For example,
175
//
176
// > secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
177
//
178
// If you are adding an internal config or secret that doesn't need to be overridden by the user,
179
// exclude the field from toml serialization. For example,
180
//
181
//        type auth struct {
182
//                AnonKey string `toml:"-" mapstructure:"anon_key"`
183
//        }
184
//
185
// Use `mapstructure:"anon_key"` tag only if you want inject values from a predictable environment
186
// variable, such as SUPABASE_AUTH_ANON_KEY.
187
//
188
// Default values for internal configs should be added to `var Config` initializer.
189
type (
190
        config struct {
191
                ProjectId    string              `toml:"project_id"`
192
                Api          api                 `toml:"api"`
193
                Db           db                  `toml:"db" mapstructure:"db"`
194
                Realtime     realtime            `toml:"realtime"`
195
                Studio       studio              `toml:"studio"`
196
                Inbucket     inbucket            `toml:"inbucket"`
197
                Storage      storage             `toml:"storage"`
198
                Auth         auth                `toml:"auth" mapstructure:"auth"`
199
                Functions    map[string]function `toml:"functions"`
200
                Analytics    analytics           `toml:"analytics"`
201
                Experimental experimental        `toml:"experimental" mapstructure:"-"`
202
                // TODO
203
                // Scripts   scripts
204
        }
205

206
        api struct {
207
                Enabled         bool     `toml:"enabled"`
208
                Image           string   `toml:"-"`
209
                Port            uint     `toml:"port"`
210
                Schemas         []string `toml:"schemas"`
211
                ExtraSearchPath []string `toml:"extra_search_path"`
212
                MaxRows         uint     `toml:"max_rows"`
213
        }
214

215
        db struct {
216
                Image        string `toml:"-"`
217
                Port         uint   `toml:"port"`
218
                ShadowPort   uint   `toml:"shadow_port"`
219
                MajorVersion uint   `toml:"major_version"`
220
                Password     string `toml:"-"`
221
                RootKey      string `toml:"-" mapstructure:"root_key"`
222
                Pooler       pooler `toml:"pooler"`
223
        }
224

225
        pooler struct {
226
                Enabled         bool     `toml:"enabled"`
227
                Port            uint16   `toml:"port"`
228
                PoolMode        PoolMode `toml:"pool_mode"`
229
                DefaultPoolSize uint     `toml:"default_pool_size"`
230
                MaxClientConn   uint     `toml:"max_client_conn"`
231
        }
232

233
        realtime struct {
234
                Enabled   bool          `toml:"enabled"`
235
                IpVersion AddressFamily `toml:"ip_version"`
236
        }
237

238
        studio struct {
239
                Enabled bool   `toml:"enabled"`
240
                Port    uint   `toml:"port"`
241
                ApiUrl  string `toml:"api_url"`
242
        }
243

244
        inbucket struct {
245
                Enabled  bool `toml:"enabled"`
246
                Port     uint `toml:"port"`
247
                SmtpPort uint `toml:"smtp_port"`
248
                Pop3Port uint `toml:"pop3_port"`
249
        }
250

251
        storage struct {
252
                Enabled       bool        `toml:"enabled"`
253
                Image         string      `toml:"-"`
254
                FileSizeLimit sizeInBytes `toml:"file_size_limit"`
255
        }
256

257
        auth struct {
258
                Enabled                bool     `toml:"enabled"`
259
                Image                  string   `toml:"-"`
260
                SiteUrl                string   `toml:"site_url"`
261
                AdditionalRedirectUrls []string `toml:"additional_redirect_urls"`
262

263
                JwtExpiry                  uint `toml:"jwt_expiry"`
264
                EnableRefreshTokenRotation bool `toml:"enable_refresh_token_rotation"`
265
                RefreshTokenReuseInterval  uint `toml:"refresh_token_reuse_interval"`
266

267
                EnableSignup bool  `toml:"enable_signup"`
268
                Email        email `toml:"email"`
269
                Sms          sms   `toml:"sms"`
270
                External     map[string]provider
271

272
                // Custom secrets can be injected from .env file
273
                JwtSecret      string `toml:"-" mapstructure:"jwt_secret"`
274
                AnonKey        string `toml:"-" mapstructure:"anon_key"`
275
                ServiceRoleKey string `toml:"-" mapstructure:"service_role_key"`
276
        }
277

278
        email struct {
279
                EnableSignup         bool                     `toml:"enable_signup"`
280
                DoubleConfirmChanges bool                     `toml:"double_confirm_changes"`
281
                EnableConfirmations  bool                     `toml:"enable_confirmations"`
282
                Template             map[string]emailTemplate `toml:"template"`
283
        }
284

285
        emailTemplate struct {
286
                Subject     string `toml:"subject"`
287
                ContentPath string `toml:"content_path"`
288
        }
289

290
        sms struct {
291
                EnableSignup        bool              `toml:"enable_signup"`
292
                EnableConfirmations bool              `toml:"enable_confirmations"`
293
                Template            string            `toml:"template"`
294
                Twilio              twilioConfig      `toml:"twilio" mapstructure:"twilio"`
295
                TwilioVerify        twilioConfig      `toml:"twilio_verify" mapstructure:"twilio_verify"`
296
                Messagebird         messagebirdConfig `toml:"messagebird" mapstructure:"messagebird"`
297
                Textlocal           textlocalConfig   `toml:"textlocal" mapstructure:"textlocal"`
298
                Vonage              vonageConfig      `toml:"vonage" mapstructure:"vonage"`
299
                TestOTP             map[string]string `toml:"test_otp"`
300
        }
301

302
        twilioConfig struct {
303
                Enabled           bool   `toml:"enabled"`
304
                AccountSid        string `toml:"account_sid"`
305
                MessageServiceSid string `toml:"message_service_sid"`
306
                AuthToken         string `toml:"auth_token" mapstructure:"auth_token"`
307
        }
308

309
        messagebirdConfig struct {
310
                Enabled    bool   `toml:"enabled"`
311
                Originator string `toml:"originator"`
312
                AccessKey  string `toml:"access_key" mapstructure:"access_key"`
313
        }
314

315
        textlocalConfig struct {
316
                Enabled bool   `toml:"enabled"`
317
                Sender  string `toml:"sender"`
318
                ApiKey  string `toml:"api_key" mapstructure:"api_key"`
319
        }
320

321
        vonageConfig struct {
322
                Enabled   bool   `toml:"enabled"`
323
                From      string `toml:"from"`
324
                ApiKey    string `toml:"api_key" mapstructure:"api_key"`
325
                ApiSecret string `toml:"api_secret" mapstructure:"api_secret"`
326
        }
327

328
        provider struct {
329
                Enabled     bool   `toml:"enabled"`
330
                ClientId    string `toml:"client_id"`
331
                Secret      string `toml:"secret"`
332
                Url         string `toml:"url"`
333
                RedirectUri string `toml:"redirect_uri"`
334
        }
335

336
        function struct {
337
                VerifyJWT *bool  `toml:"verify_jwt"`
338
                ImportMap string `toml:"import_map"`
339
        }
340

341
        analytics struct {
342
                Enabled          bool            `toml:"enabled"`
343
                Port             uint16          `toml:"port"`
344
                Backend          LogflareBackend `toml:"backend"`
345
                VectorPort       uint16          `toml:"vector_port"`
346
                GcpProjectId     string          `toml:"gcp_project_id"`
347
                GcpProjectNumber string          `toml:"gcp_project_number"`
348
                GcpJwtPath       string          `toml:"gcp_jwt_path"`
349
                ApiKey           string          `toml:"-" mapstructure:"api_key"`
350
        }
351

352
        experimental struct {
353
                OrioleDBVersion string `toml:"orioledb_version"`
354
        }
355

356
        // TODO
357
        // scripts struct {
358
        //         BeforeMigrations string `toml:"before_migrations"`
359
        //         AfterMigrations  string `toml:"after_migrations"`
360
        // }
361
)
362

363
func LoadConfigFS(fsys afero.Fs) error {
70✔
364
        // Load default values
70✔
365
        var buf bytes.Buffer
70✔
366
        if err := initConfigTemplate.Execute(&buf, nil); err != nil {
70✔
367
                return err
×
368
        }
×
369
        dec := toml.NewDecoder(&buf)
70✔
370
        if _, err := dec.Decode(&Config); err != nil {
70✔
NEW
371
                return err
×
NEW
372
        }
×
373
        // Load user defined config
374
        if metadata, err := toml.DecodeFS(afero.NewIOFS(fsys), ConfigPath, &Config); err != nil {
93✔
375
                CmdSuggestion = fmt.Sprintf("Have you set up the project with %s?", Aqua("supabase init"))
23✔
376
                cwd, osErr := os.Getwd()
23✔
377
                if osErr != nil {
23✔
378
                        cwd = "current directory"
×
379
                }
×
380
                return fmt.Errorf("cannot read config in %s: %w", cwd, err)
23✔
381
        } else if undecoded := metadata.Undecoded(); len(undecoded) > 0 {
47✔
382
                fmt.Fprintf(os.Stderr, "Unknown config fields: %+v\n", undecoded)
×
383
        }
×
384
        // Load secrets from .env file
385
        if err := godotenv.Load(); err != nil && !errors.Is(err, os.ErrNotExist) {
47✔
386
                return err
×
387
        }
×
388
        if err := viper.Unmarshal(&Config); err != nil {
47✔
389
                return err
×
390
        }
×
391

392
        // Process decoded TOML.
393
        {
47✔
394
                if Config.ProjectId == "" {
47✔
395
                        return errors.New("Missing required field in config: project_id")
×
396
                } else {
47✔
397
                        NetId = GetId("network")
47✔
398
                        DbId = GetId(DbAliases[0])
47✔
399
                        ConfigId = GetId("config")
47✔
400
                        KongId = GetId(KongAliases[0])
47✔
401
                        GotrueId = GetId(GotrueAliases[0])
47✔
402
                        InbucketId = GetId(InbucketAliases[0])
47✔
403
                        RealtimeId = "realtime-dev." + GetId(RealtimeAliases[0])
47✔
404
                        RestId = GetId(RestAliases[0])
47✔
405
                        StorageId = GetId(StorageAliases[0])
47✔
406
                        ImgProxyId = "storage_" + ImgProxyAliases[0] + "_" + Config.ProjectId
47✔
407
                        DifferId = GetId("differ")
47✔
408
                        PgmetaId = GetId(PgmetaAliases[0])
47✔
409
                        StudioId = GetId(StudioAliases[0])
47✔
410
                        EdgeRuntimeId = GetId(EdgeRuntimeAliases[0])
47✔
411
                        LogflareId = GetId(LogflareAliases[0])
47✔
412
                        VectorId = GetId(VectorAliases[0])
47✔
413
                        PoolerId = GetId(PoolerAliases[0])
47✔
414
                }
47✔
415
                // Validate api config
416
                if Config.Api.Port == 0 {
47✔
417
                        return errors.New("Missing required field in config: api.port")
×
418
                }
×
419
                if Config.Api.Enabled {
94✔
420
                        if version, err := afero.ReadFile(fsys, RestVersionPath); err == nil && len(version) > 0 && Config.Db.MajorVersion > 14 {
47✔
421
                                index := strings.IndexByte(PostgrestImage, ':')
×
422
                                Config.Api.Image = PostgrestImage[:index+1] + string(version)
×
423
                        }
×
424
                }
425
                // Append required schemas if they are missing
426
                Config.Api.Schemas = removeDuplicates(append([]string{"public", "storage"}, Config.Api.Schemas...))
47✔
427
                Config.Api.ExtraSearchPath = removeDuplicates(append([]string{"public"}, Config.Api.ExtraSearchPath...))
47✔
428
                // Validate db config
47✔
429
                if Config.Db.Port == 0 {
47✔
430
                        return errors.New("Missing required field in config: db.port")
×
431
                }
×
432
                switch Config.Db.MajorVersion {
47✔
433
                case 0:
×
434
                        return errors.New("Missing required field in config: db.major_version")
×
435
                case 12:
×
436
                        return errors.New("Postgres version 12.x is unsupported. To use the CLI, either start a new project or follow project migration steps here: https://supabase.com/docs/guides/database#migrating-between-projects.")
×
437
                case 13:
×
438
                        Config.Db.Image = Pg13Image
×
439
                        InitialSchemaSql = InitialSchemaPg13Sql
×
440
                case 14:
×
441
                        Config.Db.Image = Pg14Image
×
442
                        InitialSchemaSql = InitialSchemaPg14Sql
×
443
                case 15:
47✔
444
                        if len(Config.Experimental.OrioleDBVersion) > 0 {
49✔
445
                                Config.Db.Image = "supabase/postgres:orioledb-" + Config.Experimental.OrioleDBVersion
2✔
446
                        } else if version, err := afero.ReadFile(fsys, PostgresVersionPath); err == nil && len(version) > 0 {
47✔
447
                                index := strings.IndexByte(Pg15Image, ':')
×
448
                                Config.Db.Image = Pg15Image[:index+1] + string(version)
×
449
                        }
×
450
                default:
×
451
                        return fmt.Errorf("Failed reading config: Invalid %s: %v.", Aqua("db.major_version"), Config.Db.MajorVersion)
×
452
                }
453
                // Validate pooler config
454
                if Config.Db.Pooler.Enabled {
49✔
455
                        allowed := []PoolMode{TransactionMode, SessionMode}
2✔
456
                        if !SliceContains(allowed, Config.Db.Pooler.PoolMode) {
2✔
457
                                return fmt.Errorf("Invalid config for db.pooler.pool_mode. Must be one of: %v", allowed)
×
458
                        }
×
459
                }
460
                // Validate realtime config
461
                if Config.Realtime.Enabled {
94✔
462
                        allowed := []AddressFamily{AddressIPv6, AddressIPv4}
47✔
463
                        if !SliceContains(allowed, Config.Realtime.IpVersion) {
47✔
464
                                return fmt.Errorf("Invalid config for realtime.ip_version. Must be one of: %v", allowed)
×
465
                        }
×
466
                }
467
                // Validate storage config
468
                if Config.Storage.Enabled {
94✔
469
                        if version, err := afero.ReadFile(fsys, StorageVersionPath); err == nil && len(version) > 0 && Config.Db.MajorVersion > 14 {
47✔
470
                                index := strings.IndexByte(StorageImage, ':')
×
471
                                Config.Storage.Image = StorageImage[:index+1] + string(version)
×
472
                        }
×
473
                }
474
                // Validate studio config
475
                if Config.Studio.Enabled {
94✔
476
                        if Config.Studio.Port == 0 {
47✔
477
                                return errors.New("Missing required field in config: studio.port")
×
478
                        }
×
479
                }
480
                // Validate email config
481
                if Config.Inbucket.Enabled {
94✔
482
                        if Config.Inbucket.Port == 0 {
47✔
483
                                return errors.New("Missing required field in config: inbucket.port")
×
484
                        }
×
485
                }
486
                // Validate auth config
487
                if Config.Auth.Enabled {
94✔
488
                        if Config.Auth.SiteUrl == "" {
47✔
489
                                return errors.New("Missing required field in config: auth.site_url")
×
490
                        }
×
491
                        if version, err := afero.ReadFile(fsys, GotrueVersionPath); err == nil && len(version) > 0 && Config.Db.MajorVersion > 14 {
47✔
492
                                index := strings.IndexByte(GotrueImage, ':')
×
493
                                Config.Auth.Image = GotrueImage[:index+1] + string(version)
×
494
                        }
×
495
                        // Validate email template
496
                        for _, tmpl := range Config.Auth.Email.Template {
282✔
497
                                if len(tmpl.ContentPath) > 0 {
237✔
498
                                        if _, err := fsys.Stat(tmpl.ContentPath); err != nil {
3✔
499
                                                return err
1✔
500
                                        }
1✔
501
                                }
502
                        }
503
                        // Validate sms config
504
                        var err error
46✔
505
                        if Config.Auth.Sms.Twilio.Enabled {
47✔
506
                                if len(Config.Auth.Sms.Twilio.AccountSid) == 0 {
1✔
507
                                        return errors.New("Missing required field in config: auth.sms.twilio.account_sid")
×
508
                                }
×
509
                                if len(Config.Auth.Sms.Twilio.MessageServiceSid) == 0 {
1✔
510
                                        return errors.New("Missing required field in config: auth.sms.twilio.message_service_sid")
×
511
                                }
×
512
                                if len(Config.Auth.Sms.Twilio.AuthToken) == 0 {
1✔
513
                                        return errors.New("Missing required field in config: auth.sms.twilio.auth_token")
×
514
                                }
×
515
                                if Config.Auth.Sms.Twilio.AuthToken, err = maybeLoadEnv(Config.Auth.Sms.Twilio.AuthToken); err != nil {
1✔
516
                                        return err
×
517
                                }
×
518
                        }
519
                        if Config.Auth.Sms.TwilioVerify.Enabled {
46✔
520
                                if len(Config.Auth.Sms.TwilioVerify.AccountSid) == 0 {
×
521
                                        return errors.New("Missing required field in config: auth.sms.twilio_verify.account_sid")
×
522
                                }
×
523
                                if len(Config.Auth.Sms.TwilioVerify.MessageServiceSid) == 0 {
×
524
                                        return errors.New("Missing required field in config: auth.sms.twilio_verify.message_service_sid")
×
525
                                }
×
526
                                if len(Config.Auth.Sms.TwilioVerify.AuthToken) == 0 {
×
527
                                        return errors.New("Missing required field in config: auth.sms.twilio_verify.auth_token")
×
528
                                }
×
529
                                if Config.Auth.Sms.TwilioVerify.AuthToken, err = maybeLoadEnv(Config.Auth.Sms.TwilioVerify.AuthToken); err != nil {
×
530
                                        return err
×
531
                                }
×
532
                        }
533
                        if Config.Auth.Sms.Messagebird.Enabled {
46✔
534
                                if len(Config.Auth.Sms.Messagebird.Originator) == 0 {
×
535
                                        return errors.New("Missing required field in config: auth.sms.messagebird.originator")
×
536
                                }
×
537
                                if len(Config.Auth.Sms.Messagebird.AccessKey) == 0 {
×
538
                                        return errors.New("Missing required field in config: auth.sms.messagebird.access_key")
×
539
                                }
×
540
                                if Config.Auth.Sms.Messagebird.AccessKey, err = maybeLoadEnv(Config.Auth.Sms.Messagebird.AccessKey); err != nil {
×
541
                                        return err
×
542
                                }
×
543
                        }
544
                        if Config.Auth.Sms.Textlocal.Enabled {
46✔
545
                                if len(Config.Auth.Sms.Textlocal.Sender) == 0 {
×
546
                                        return errors.New("Missing required field in config: auth.sms.textlocal.sender")
×
547
                                }
×
548
                                if len(Config.Auth.Sms.Textlocal.ApiKey) == 0 {
×
549
                                        return errors.New("Missing required field in config: auth.sms.textlocal.api_key")
×
550
                                }
×
551
                                if Config.Auth.Sms.Textlocal.ApiKey, err = maybeLoadEnv(Config.Auth.Sms.Textlocal.ApiKey); err != nil {
×
552
                                        return err
×
553
                                }
×
554
                        }
555
                        if Config.Auth.Sms.Vonage.Enabled {
46✔
556
                                if len(Config.Auth.Sms.Vonage.From) == 0 {
×
557
                                        return errors.New("Missing required field in config: auth.sms.vonage.from")
×
558
                                }
×
559
                                if len(Config.Auth.Sms.Vonage.ApiKey) == 0 {
×
560
                                        return errors.New("Missing required field in config: auth.sms.vonage.api_key")
×
561
                                }
×
562
                                if len(Config.Auth.Sms.Vonage.ApiSecret) == 0 {
×
563
                                        return errors.New("Missing required field in config: auth.sms.vonage.api_secret")
×
564
                                }
×
565
                                if Config.Auth.Sms.Vonage.ApiKey, err = maybeLoadEnv(Config.Auth.Sms.Vonage.ApiKey); err != nil {
×
566
                                        return err
×
567
                                }
×
568
                                if Config.Auth.Sms.Vonage.ApiSecret, err = maybeLoadEnv(Config.Auth.Sms.Vonage.ApiSecret); err != nil {
×
569
                                        return err
×
570
                                }
×
571
                        }
572
                        // Validate oauth config
573
                        for ext, provider := range Config.Auth.External {
828✔
574
                                if !provider.Enabled {
1,563✔
575
                                        continue
781✔
576
                                }
577
                                if provider.ClientId == "" {
1✔
578
                                        return fmt.Errorf("Missing required field in config: auth.external.%s.client_id", ext)
×
579
                                }
×
580
                                if provider.Secret == "" {
1✔
581
                                        return fmt.Errorf("Missing required field in config: auth.external.%s.secret", ext)
×
582
                                }
×
583
                                if provider.ClientId, err = maybeLoadEnv(provider.ClientId); err != nil {
1✔
584
                                        return err
×
585
                                }
×
586
                                if provider.Secret, err = maybeLoadEnv(provider.Secret); err != nil {
1✔
587
                                        return err
×
588
                                }
×
589
                                if provider.RedirectUri, err = maybeLoadEnv(provider.RedirectUri); err != nil {
1✔
590
                                        return err
×
591
                                }
×
592
                                if provider.Url, err = maybeLoadEnv(provider.Url); err != nil {
1✔
593
                                        return err
×
594
                                }
×
595
                                Config.Auth.External[ext] = provider
1✔
596
                        }
597
                }
598
        }
599
        // Validate functions config
600
        for name, functionConfig := range Config.Functions {
48✔
601
                if functionConfig.VerifyJWT == nil {
2✔
602
                        verifyJWT := true
×
603
                        functionConfig.VerifyJWT = &verifyJWT
×
604
                        Config.Functions[name] = functionConfig
×
605
                }
×
606
        }
607
        // Validate logflare config
608
        if Config.Analytics.Enabled {
46✔
609
                switch Config.Analytics.Backend {
×
610
                case LogflareBigQuery:
×
611
                        if len(Config.Analytics.GcpProjectId) == 0 {
×
612
                                return errors.New("Missing required field in config: analytics.gcp_project_id")
×
613
                        }
×
614
                        if len(Config.Analytics.GcpProjectNumber) == 0 {
×
615
                                return errors.New("Missing required field in config: analytics.gcp_project_number")
×
616
                        }
×
617
                        if len(Config.Analytics.GcpJwtPath) == 0 {
×
618
                                return errors.New("Path to GCP Service Account Key must be provided in config, relative to config.toml: analytics.gcp_jwt_path")
×
619
                        }
×
620
                case LogflarePostgres:
×
621
                        break
×
622
                default:
×
623
                        allowed := []LogflareBackend{LogflarePostgres, LogflareBigQuery}
×
624
                        return fmt.Errorf("Invalid config for analytics.backend. Must be one of: %v", allowed)
×
625
                }
626
        }
627
        return nil
46✔
628
}
629

630
func maybeLoadEnv(s string) (string, error) {
5✔
631
        matches := envPattern.FindStringSubmatch(s)
5✔
632
        if len(matches) == 0 {
7✔
633
                return s, nil
2✔
634
        }
2✔
635

636
        envName := matches[1]
3✔
637
        if value := os.Getenv(envName); value != "" {
6✔
638
                return value, nil
3✔
639
        }
3✔
640

641
        return "", fmt.Errorf(`Error evaluating "%s": environment variable %s is unset.`, s, envName)
×
642
}
643

644
func sanitizeProjectId(src string) string {
64✔
645
        // A valid project ID must only contain alphanumeric and special characters _.-
64✔
646
        sanitized := invalidProjectId.ReplaceAllString(src, "_")
64✔
647
        // It must also start with an alphanumeric character
64✔
648
        return strings.TrimLeft(sanitized, "_.-")
64✔
649
}
64✔
650

651
type InitParams struct {
652
        ProjectId   string
653
        UseOrioleDB bool
654
}
655

656
func InitConfig(params InitParams, fsys afero.Fs) error {
59✔
657
        // Defaults to current directory name as project id
59✔
658
        if len(params.ProjectId) == 0 {
114✔
659
                cwd, err := os.Getwd()
55✔
660
                if err != nil {
55✔
661
                        return err
×
662
                }
×
663
                params.ProjectId = filepath.Base(cwd)
55✔
664
        }
665
        params.ProjectId = sanitizeProjectId(params.ProjectId)
59✔
666
        // Create config file
59✔
667
        if err := MkdirIfNotExistFS(fsys, filepath.Dir(ConfigPath)); err != nil {
60✔
668
                return err
1✔
669
        }
1✔
670
        f, err := fsys.OpenFile(ConfigPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
58✔
671
        if err != nil {
58✔
672
                return err
×
673
        }
×
674
        defer f.Close()
58✔
675
        // Update from template
58✔
676
        return initConfigTemplate.Execute(f, params)
58✔
677
}
678

679
func WriteConfig(fsys afero.Fs, _test bool) error {
44✔
680
        return InitConfig(InitParams{}, fsys)
44✔
681
}
44✔
682

683
func removeDuplicates(slice []string) (result []string) {
94✔
684
        set := make(map[string]struct{})
94✔
685
        for _, item := range slice {
470✔
686
                if _, exists := set[item]; !exists {
611✔
687
                        set[item] = struct{}{}
235✔
688
                        result = append(result, item)
235✔
689
                }
235✔
690
        }
691
        return result
94✔
692
}
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