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

supabase / cli / 19699968033

26 Nov 2025 10:07AM UTC coverage: 54.995% (-0.4%) from 55.403%
19699968033

Pull #4368

github

web-flow
Merge b6a3e01eb into 6558d59e6
Pull Request #4368: feat: add deploy command to push all changes to linked project

45 of 181 new or added lines in 10 files covered. (24.86%)

15 existing lines in 2 files now uncovered.

6705 of 12192 relevant lines covered (55.0%)

6.2 hits per line

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

42.47
/internal/utils/flags/db_url.go
1
package flags
2

3
import (
4
        "context"
5
        "crypto/rand"
6
        _ "embed"
7
        "fmt"
8
        "math/big"
9
        "net"
10
        "net/http"
11
        "os"
12
        "strings"
13
        "time"
14

15
        "github.com/cenkalti/backoff/v4"
16
        "github.com/go-errors/errors"
17
        "github.com/jackc/pgconn"
18
        "github.com/spf13/afero"
19
        "github.com/spf13/pflag"
20
        "github.com/spf13/viper"
21
        "github.com/supabase/cli/internal/utils"
22
        "github.com/supabase/cli/internal/utils/credentials"
23
        "github.com/supabase/cli/pkg/api"
24
        "github.com/supabase/cli/pkg/config"
25
)
26

27
type connection int
28

29
const (
30
        unknown connection = iota
31
        direct
32
        local
33
        linked
34
        proxy
35
)
36

37
var DbConfig pgconn.Config
38

39
func ParseDatabaseConfig(ctx context.Context, flagSet *pflag.FlagSet, fsys afero.Fs) error {
3✔
40
        // Changed flags take precedence over default values
3✔
41
        var connType connection
3✔
42
        if flag := flagSet.Lookup("db-url"); flag != nil && flag.Changed {
4✔
43
                connType = direct
1✔
44
        } else if flag := flagSet.Lookup("local"); flag != nil && flag.Changed {
4✔
45
                connType = local
1✔
46
        } else if flag := flagSet.Lookup("linked"); flag != nil && flag.Changed {
3✔
47
                connType = linked
1✔
48
        } else if flag := flagSet.Lookup("proxy"); flag != nil && flag.Changed {
1✔
49
                connType = proxy
×
50
        } else if value, err := flagSet.GetBool("local"); err == nil && value {
×
51
                connType = local
×
52
        } else if value, err := flagSet.GetBool("linked"); err == nil && value {
×
53
                connType = linked
×
54
        } else if value, err := flagSet.GetBool("proxy"); err == nil && value {
×
55
                connType = proxy
×
56
        }
×
57
        // Update connection config
58
        switch connType {
3✔
59
        case direct:
1✔
60
                if err := LoadConfig(fsys); err != nil {
1✔
61
                        return err
×
62
                }
×
63
                if flag := flagSet.Lookup("db-url"); flag != nil {
2✔
64
                        config, err := pgconn.ParseConfig(flag.Value.String())
1✔
65
                        if err != nil {
1✔
66
                                return errors.Errorf("failed to parse connection string: %w", err)
×
67
                        }
×
68
                        DbConfig = *config
1✔
69
                }
70
        case local:
1✔
71
                if err := LoadConfig(fsys); err != nil {
1✔
72
                        return err
×
73
                }
×
74
                // Ignore other PG settings
75
                DbConfig.Host = utils.Config.Hostname
1✔
76
                DbConfig.Port = utils.Config.Db.Port
1✔
77
                DbConfig.User = "postgres"
1✔
78
                DbConfig.Password = utils.Config.Db.Password
1✔
79
                DbConfig.Database = "postgres"
1✔
80
        case linked:
1✔
81
                if err := LoadProjectRef(fsys); err != nil {
1✔
82
                        return err
×
83
                }
×
84
                if err := LoadConfig(fsys); err != nil {
1✔
85
                        return err
×
86
                }
×
87
                var err error
1✔
88
                if DbConfig, err = NewDbConfigWithPassword(ctx, ProjectRef); err != nil {
1✔
89
                        return err
×
90
                }
×
91
        case proxy:
×
92
                token, err := utils.LoadAccessTokenFS(fsys)
×
93
                if err != nil {
×
94
                        return err
×
95
                }
×
96
                if err := LoadProjectRef(fsys); err != nil {
×
97
                        return err
×
98
                }
×
99
                DbConfig.Host = utils.GetSupabaseAPIHost()
×
100
                DbConfig.Port = 443
×
101
                DbConfig.User = "postgres"
×
102
                DbConfig.Password = token
×
103
                DbConfig.Database = ProjectRef
×
104
        }
105
        return nil
3✔
106
}
107

108
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
109

110
func RandomString(size int) (string, error) {
×
111
        data := make([]byte, size)
×
112
        _, err := rand.Read(data)
×
113
        if err != nil {
×
114
                return "", errors.Errorf("failed to read random: %w", err)
×
115
        }
×
116
        for i := range data {
×
117
                n := int(data[i]) % len(letters)
×
118
                data[i] = letters[n]
×
119
        }
×
120
        return string(data), nil
×
121
}
122

123
const suggestEnvVar = "Connect to your database by setting the env var: SUPABASE_DB_PASSWORD"
124

125
func NewDbConfigWithPassword(ctx context.Context, projectRef string) (pgconn.Config, error) {
4✔
126
        config := pgconn.Config{
4✔
127
                Host:     utils.GetSupabaseDbHost(projectRef),
4✔
128
                Port:     5432,
4✔
129
                User:     "postgres",
4✔
130
                Password: viper.GetString("DB_PASSWORD"),
4✔
131
                Database: "postgres",
4✔
132
        }
4✔
133
        logger := utils.GetDebugLogger()
4✔
134
        // Use pooler if host is not reachable directly
4✔
135
        d := net.Dialer{Timeout: 5 * time.Second}
4✔
136
        addr := fmt.Sprintf("%s:%d", config.Host, config.Port)
4✔
137
        if conn, err := d.DialContext(ctx, "tcp", addr); err == nil {
4✔
138
                if err := conn.Close(); err != nil {
×
139
                        fmt.Fprintln(logger, err)
×
140
                }
×
UNCOV
141
                fmt.Fprintf(logger, "Resolved DNS: %v\n", conn.RemoteAddr())
×
142
        } else if poolerConfig := utils.GetPoolerConfig(projectRef); poolerConfig != nil {
5✔
143
                if len(config.Password) > 0 {
2✔
144
                        fmt.Fprintln(logger, "Using database password from env var...")
1✔
145
                        poolerConfig.Password = config.Password
1✔
146
                } else if err := initPoolerLogin(ctx, projectRef, poolerConfig); err != nil {
1✔
147
                        utils.CmdSuggestion = suggestEnvVar
×
148
                        return *poolerConfig, err
×
UNCOV
149
                }
×
150
                return *poolerConfig, nil
1✔
151
        } else {
3✔
152
                utils.CmdSuggestion = fmt.Sprintf("Run %s to setup IPv4 connection.", utils.Aqua("supabase link --project-ref "+projectRef))
3✔
153
                return config, errors.Errorf("IPv6 is not supported on your current network: %w", err)
3✔
154
        }
3✔
155
        // Connect via direct connection
156
        if len(config.Password) > 0 {
×
157
                fmt.Fprintln(logger, "Using database password from env var...")
×
158
        } else if err := initLoginRole(ctx, projectRef, &config); err != nil {
×
159
                // Do not prompt because reading masked input is buggy on windows
×
160
                utils.CmdSuggestion = suggestEnvVar
×
161
                return config, err
×
162
        }
×
UNCOV
163
        return config, nil
×
164
}
165

166
func initLoginRole(ctx context.Context, projectRef string, config *pgconn.Config) error {
×
167
        fmt.Fprintln(os.Stderr, "Initialising login role...")
×
168
        body := api.CreateRoleBody{ReadOnly: false}
×
169
        resp, err := utils.GetSupabase().V1CreateLoginRoleWithResponse(ctx, projectRef, body)
×
170
        if err != nil {
×
171
                return errors.Errorf("failed to initialise login role: %w", err)
×
172
        } else if resp.JSON201 == nil {
×
173
                return errors.Errorf("unexpected login role status %d: %s", resp.StatusCode(), string(resp.Body))
×
174
        }
×
175
        config.User = resp.JSON201.Role
×
176
        config.Password = resp.JSON201.Password
×
UNCOV
177
        return nil
×
178
}
179

180
func initPoolerLogin(ctx context.Context, projectRef string, poolerConfig *pgconn.Config) error {
×
181
        poolerUser := poolerConfig.User
×
182
        if err := initLoginRole(ctx, projectRef, poolerConfig); err != nil {
×
183
                return err
×
184
        }
×
185
        suffix := "." + projectRef
×
186
        if strings.HasSuffix(poolerUser, suffix) {
×
187
                poolerConfig.User += suffix
×
UNCOV
188
        }
×
189
        // Wait for pooler to refresh password
190
        login := func() error {
×
191
                conn, err := pgconn.ConnectConfig(ctx, poolerConfig)
×
192
                if err != nil {
×
193
                        return errors.Errorf("failed to connect as temp role: %w", err)
×
194
                }
×
UNCOV
195
                return conn.Close(ctx)
×
196
        }
197
        notify := utils.NewErrorCallback(func(attempt uint) error {
×
198
                if attempt < 3 {
×
199
                        return nil
×
200
                }
×
201
                if ips, err := ListNetworkBans(ctx, projectRef); err != nil {
×
202
                        return err
×
203
                } else if len(ips) > 0 {
×
204
                        return UnbanIP(ctx, projectRef, ips...)
×
205
                }
×
UNCOV
206
                return nil
×
207
        })
UNCOV
208
        return backoff.RetryNotify(login, utils.NewBackoffPolicy(ctx), notify)
×
209
}
210

211
func ListNetworkBans(ctx context.Context, projectRef string) ([]string, error) {
×
212
        resp, err := utils.GetSupabase().V1ListAllNetworkBansWithResponse(ctx, projectRef)
×
213
        if err != nil {
×
214
                return nil, errors.Errorf("failed to list network bans: %w", err)
×
215
        } else if resp.JSON201 == nil {
×
216
                return nil, errors.Errorf("unexpected list bans status %d: %s", resp.StatusCode(), string(resp.Body))
×
217
        }
×
UNCOV
218
        return resp.JSON201.BannedIpv4Addresses, nil
×
219
}
220

221
func UnbanIP(ctx context.Context, projectRef string, addrs ...string) error {
3✔
222
        includeSelf := len(addrs) == 0
3✔
223
        body := api.RemoveNetworkBanRequest{
3✔
224
                Ipv4Addresses: append([]string{}, addrs...),
3✔
225
                RequesterIp:   &includeSelf,
3✔
226
        }
3✔
227
        if resp, err := utils.GetSupabase().V1DeleteNetworkBansWithResponse(ctx, projectRef, body); err != nil {
4✔
228
                return errors.Errorf("failed to remove network bans: %w", err)
1✔
229
        } else if resp.StatusCode() != http.StatusOK {
4✔
230
                return errors.Errorf("unexpected unban status %d: %s", resp.StatusCode(), string(resp.Body))
1✔
231
        }
1✔
232
        return nil
1✔
233
}
234

235
const PASSWORD_LENGTH = 16
236

237
func PromptPassword(stdin *os.File) string {
2✔
238
        fmt.Fprint(os.Stderr, "Enter your database password (or leave blank to generate one): ")
2✔
239
        if input := credentials.PromptMasked(stdin); len(input) > 0 {
3✔
240
                return input
1✔
241
        }
1✔
242
        // Generate a password, see ./Settings/Database/DatabaseSettings/ResetDbPassword.tsx#L83
243
        var password []byte
1✔
244
        charset := string(config.LowerUpperLettersDigits.ToChar())
1✔
245
        charset = strings.ReplaceAll(charset, ":", "")
1✔
246
        maxRange := big.NewInt(int64(len(charset)))
1✔
247
        for range PASSWORD_LENGTH {
17✔
248
                random, err := rand.Int(rand.Reader, maxRange)
16✔
249
                if err != nil {
16✔
250
                        fmt.Fprintln(os.Stderr, "Failed to randomise password:", err)
×
UNCOV
251
                        continue
×
252
                }
253
                password = append(password, charset[random.Int64()])
16✔
254
        }
255
        return string(password)
1✔
256
}
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