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

supabase / cli / 18483998815

14 Oct 2025 02:53AM UTC coverage: 54.633% (-0.05%) from 54.687%
18483998815

Pull #4305

github

web-flow
Merge 7e5a82b7c into 068afe697
Pull Request #4305: fix: transform pooler url from api response

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

5 existing lines in 1 file now uncovered.

6409 of 11731 relevant lines covered (54.63%)

6.09 hits per line

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

71.7
/internal/link/link.go
1
package link
2

3
import (
4
        "context"
5
        "fmt"
6
        "net/http"
7
        "os"
8
        "strconv"
9
        "strings"
10
        "sync"
11

12
        "github.com/go-errors/errors"
13
        "github.com/jackc/pgconn"
14
        "github.com/jackc/pgx/v4"
15
        "github.com/spf13/afero"
16
        "github.com/spf13/viper"
17
        "github.com/supabase/cli/internal/utils"
18
        "github.com/supabase/cli/internal/utils/flags"
19
        "github.com/supabase/cli/internal/utils/tenant"
20
        "github.com/supabase/cli/pkg/api"
21
        "github.com/supabase/cli/pkg/cast"
22
        cliConfig "github.com/supabase/cli/pkg/config"
23
        "github.com/supabase/cli/pkg/migration"
24
)
25

26
func Run(ctx context.Context, projectRef string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
3✔
27
        majorVersion := utils.Config.Db.MajorVersion
3✔
28
        if err := checkRemoteProjectStatus(ctx, projectRef, fsys); err != nil {
3✔
29
                return err
×
30
        }
×
31

32
        // 1. Check service config
33
        keys, err := tenant.GetApiKeys(ctx, projectRef)
3✔
34
        if err != nil {
3✔
35
                return err
×
36
        }
×
37
        LinkServices(ctx, projectRef, keys.ServiceRole, fsys)
3✔
38

3✔
39
        // 2. Check database connection
3✔
40
        config := flags.NewDbConfigWithPassword(ctx, projectRef)
3✔
41
        if err := linkDatabase(ctx, config, fsys, options...); err != nil {
4✔
42
                return err
1✔
43
        }
1✔
44

45
        // 3. Save project ref
46
        if err := utils.WriteFile(utils.ProjectRefPath, []byte(projectRef), fsys); err != nil {
3✔
47
                return err
1✔
48
        }
1✔
49
        fmt.Fprintln(os.Stdout, "Finished "+utils.Aqua("supabase link")+".")
1✔
50

1✔
51
        // 4. Suggest config update
1✔
52
        if utils.Config.Db.MajorVersion != majorVersion {
2✔
53
                fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "Local database version differs from the linked project.")
1✔
54
                fmt.Fprintf(os.Stderr, `Update your %s to fix it:
1✔
55
[db]
1✔
56
major_version = %d
1✔
57
`, utils.Bold(utils.ConfigPath), utils.Config.Db.MajorVersion)
1✔
58
        }
1✔
59
        return nil
1✔
60
}
61

62
func LinkServices(ctx context.Context, projectRef, serviceKey string, fsys afero.Fs) {
3✔
63
        // Ignore non-fatal errors linking services
3✔
64
        var wg sync.WaitGroup
3✔
65
        wg.Add(8)
3✔
66
        go func() {
6✔
67
                defer wg.Done()
3✔
68
                if err := linkDatabaseSettings(ctx, projectRef); err != nil && viper.GetBool("DEBUG") {
3✔
69
                        fmt.Fprintln(os.Stderr, err)
×
70
                }
×
71
        }()
72
        go func() {
6✔
73
                defer wg.Done()
3✔
74
                if err := linkNetworkRestrictions(ctx, projectRef); err != nil && viper.GetBool("DEBUG") {
3✔
75
                        fmt.Fprintln(os.Stderr, err)
×
76
                }
×
77
        }()
78
        go func() {
6✔
79
                defer wg.Done()
3✔
80
                if err := linkPostgrest(ctx, projectRef); err != nil && viper.GetBool("DEBUG") {
3✔
81
                        fmt.Fprintln(os.Stderr, err)
×
82
                }
×
83
        }()
84
        go func() {
6✔
85
                defer wg.Done()
3✔
86
                if err := linkGotrue(ctx, projectRef); err != nil && viper.GetBool("DEBUG") {
3✔
87
                        fmt.Fprintln(os.Stderr, err)
×
88
                }
×
89
        }()
90
        go func() {
6✔
91
                defer wg.Done()
3✔
92
                if err := linkStorage(ctx, projectRef); err != nil && viper.GetBool("DEBUG") {
3✔
93
                        fmt.Fprintln(os.Stderr, err)
×
94
                }
×
95
        }()
96
        go func() {
6✔
97
                defer wg.Done()
3✔
98
                if err := linkPooler(ctx, projectRef, fsys); err != nil && viper.GetBool("DEBUG") {
3✔
99
                        fmt.Fprintln(os.Stderr, err)
×
100
                }
×
101
        }()
102
        api := tenant.NewTenantAPI(ctx, projectRef, serviceKey)
3✔
103
        go func() {
6✔
104
                defer wg.Done()
3✔
105
                if err := linkPostgrestVersion(ctx, api, fsys); err != nil && viper.GetBool("DEBUG") {
3✔
106
                        fmt.Fprintln(os.Stderr, err)
×
107
                }
×
108
        }()
109
        go func() {
6✔
110
                defer wg.Done()
3✔
111
                if err := linkGotrueVersion(ctx, api, fsys); err != nil && viper.GetBool("DEBUG") {
3✔
112
                        fmt.Fprintln(os.Stderr, err)
×
113
                }
×
114
        }()
115
        wg.Wait()
3✔
116
}
117

118
func linkPostgrest(ctx context.Context, projectRef string) error {
7✔
119
        resp, err := utils.GetSupabase().V1GetPostgrestServiceConfigWithResponse(ctx, projectRef)
7✔
120
        if err != nil {
10✔
121
                return errors.Errorf("failed to read API config: %w", err)
3✔
122
        } else if resp.JSON200 == nil {
8✔
123
                return errors.Errorf("unexpected API config status %d: %s", resp.StatusCode(), string(resp.Body))
1✔
124
        }
1✔
125
        utils.Config.Api.FromRemoteApiConfig(*resp.JSON200)
3✔
126
        return nil
3✔
127
}
128

129
func linkPostgrestVersion(ctx context.Context, api tenant.TenantAPI, fsys afero.Fs) error {
3✔
130
        version, err := api.GetPostgrestVersion(ctx)
3✔
131
        if err != nil {
5✔
132
                return err
2✔
133
        }
2✔
134
        return utils.WriteFile(utils.RestVersionPath, []byte(version), fsys)
1✔
135
}
136

137
func linkGotrue(ctx context.Context, projectRef string) error {
3✔
138
        resp, err := utils.GetSupabase().V1GetAuthServiceConfigWithResponse(ctx, projectRef)
3✔
139
        if err != nil {
6✔
140
                return errors.Errorf("failed to read Auth config: %w", err)
3✔
141
        } else if resp.JSON200 == nil {
3✔
142
                return errors.Errorf("unexpected Auth config status %d: %s", resp.StatusCode(), string(resp.Body))
×
143
        }
×
144
        utils.Config.Auth.FromRemoteAuthConfig(*resp.JSON200)
×
145
        return nil
×
146
}
147

148
func linkGotrueVersion(ctx context.Context, api tenant.TenantAPI, fsys afero.Fs) error {
3✔
149
        version, err := api.GetGotrueVersion(ctx)
3✔
150
        if err != nil {
5✔
151
                return err
2✔
152
        }
2✔
153
        return utils.WriteFile(utils.GotrueVersionPath, []byte(version), fsys)
1✔
154
}
155

156
func linkStorage(ctx context.Context, projectRef string) error {
3✔
157
        resp, err := utils.GetSupabase().V1GetStorageConfigWithResponse(ctx, projectRef)
3✔
158
        if err != nil {
5✔
159
                return errors.Errorf("failed to read Storage config: %w", err)
2✔
160
        } else if resp.JSON200 == nil {
3✔
161
                return errors.Errorf("unexpected Storage config status %d: %s", resp.StatusCode(), string(resp.Body))
×
162
        }
×
163
        utils.Config.Storage.FromRemoteStorageConfig(*resp.JSON200)
1✔
164
        return nil
1✔
165
}
166

167
const GET_LATEST_STORAGE_MIGRATION = "SELECT name FROM storage.migrations ORDER BY id DESC LIMIT 1"
168

169
func linkStorageVersion(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) error {
5✔
170
        var name string
5✔
171
        if err := conn.QueryRow(ctx, GET_LATEST_STORAGE_MIGRATION).Scan(&name); err != nil {
6✔
172
                return errors.Errorf("failed to fetch storage migration: %w", err)
1✔
173
        }
1✔
174
        return utils.WriteFile(utils.StorageVersionPath, []byte(name), fsys)
4✔
175
}
176

177
func linkDatabaseSettings(ctx context.Context, projectRef string) error {
3✔
178
        resp, err := utils.GetSupabase().V1GetPostgresConfigWithResponse(ctx, projectRef)
3✔
179
        if err != nil {
5✔
180
                return errors.Errorf("failed to read DB config: %w", err)
2✔
181
        } else if resp.JSON200 == nil {
3✔
182
                return errors.Errorf("unexpected DB config status %d: %s", resp.StatusCode(), string(resp.Body))
×
183
        }
×
184
        utils.Config.Db.Settings.FromRemotePostgresConfig(*resp.JSON200)
1✔
185
        return nil
1✔
186
}
187

188
func linkNetworkRestrictions(ctx context.Context, projectRef string) error {
3✔
189
        resp, err := utils.GetSupabase().V1GetNetworkRestrictionsWithResponse(ctx, projectRef)
3✔
190
        if err != nil {
3✔
191
                return errors.Errorf("failed to read network restrictions: %w", err)
×
192
        } else if resp.JSON200 == nil {
3✔
193
                return errors.Errorf("unexpected network restrictions status %d: %s", resp.StatusCode(), string(resp.Body))
×
194
        }
×
195
        utils.Config.Db.NetworkRestrictions.FromRemoteNetworkRestrictions(*resp.JSON200)
3✔
196
        return nil
3✔
197
}
198

199
func linkDatabase(ctx context.Context, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
7✔
200
        conn, err := utils.ConnectByConfig(ctx, config, options...)
7✔
201
        if err != nil {
9✔
202
                return err
2✔
203
        }
2✔
204
        defer conn.Close(context.Background())
5✔
205
        updatePostgresConfig(conn)
5✔
206
        if err := linkStorageVersion(ctx, conn, fsys); err != nil {
7✔
207
                fmt.Fprintln(os.Stderr, err)
2✔
208
        }
2✔
209
        // If `schema_migrations` doesn't exist on the remote database, create it.
210
        if err := migration.CreateMigrationTable(ctx, conn); err != nil {
6✔
211
                return err
1✔
212
        }
1✔
213
        return migration.CreateSeedTable(ctx, conn)
4✔
214
}
215

216
func updatePostgresConfig(conn *pgx.Conn) {
5✔
217
        serverVersion := conn.PgConn().ParameterStatus("server_version")
5✔
218
        // Safe to assume that supported Postgres version is 10.0 <= n < 100.0
5✔
219
        majorDigits := min(len(serverVersion), 2)
5✔
220
        // Treat error as unchanged
5✔
221
        if dbMajorVersion, err := strconv.ParseUint(serverVersion[:majorDigits], 10, 7); err == nil {
9✔
222
                utils.Config.Db.MajorVersion = uint(dbMajorVersion)
4✔
223
        }
4✔
224
}
225

226
func linkPooler(ctx context.Context, projectRef string, fsys afero.Fs) error {
3✔
227
        resp, err := utils.GetSupabase().V1GetPoolerConfigWithResponse(ctx, projectRef)
3✔
228
        if err != nil {
6✔
229
                return errors.Errorf("failed to get pooler config: %w", err)
3✔
230
        }
3✔
231
        if resp.JSON200 == nil {
×
232
                return errors.Errorf("%w: %s", tenant.ErrAuthToken, string(resp.Body))
×
233
        }
×
234
        for _, config := range *resp.JSON200 {
×
235
                if config.DatabaseType == api.PRIMARY {
×
236
                        updatePoolerConfig(config)
×
237
                }
×
238
        }
239
        return utils.WriteFile(utils.PoolerUrlPath, []byte(utils.Config.Db.Pooler.ConnectionString), fsys)
×
240
}
241

242
func updatePoolerConfig(config api.SupavisorConfigResponse) {
×
NEW
243
        // Remove password from pooler connection string because the placeholder text
×
NEW
244
        // [YOUR-PASSWORD] messes up pgconn.ParseConfig. The password must be percent
×
NEW
245
        // escaped so we cannot simply call strings.Replace with actual password.
×
NEW
246
        utils.Config.Db.Pooler.ConnectionString = strings.ReplaceAll(config.ConnectionString, ":[YOUR-PASSWORD]", "")
×
NEW
247
        // Always use session mode for running migrations
×
NEW
248
        if utils.Config.Db.Pooler.PoolMode = cliConfig.SessionMode; config.PoolMode != api.SupavisorConfigResponsePoolModeSession {
×
NEW
249
                utils.Config.Db.Pooler.ConnectionString = strings.ReplaceAll(utils.Config.Db.Pooler.ConnectionString, ":6543/", ":5432/")
×
NEW
250
        }
×
251
        if value, err := config.DefaultPoolSize.Get(); err == nil {
×
252
                utils.Config.Db.Pooler.DefaultPoolSize = cast.IntToUint(value)
×
253
        }
×
254
        if value, err := config.MaxClientConn.Get(); err == nil {
×
255
                utils.Config.Db.Pooler.MaxClientConn = cast.IntToUint(value)
×
256
        }
×
257
}
258

259
var errProjectPaused = errors.New("project is paused")
260

261
func checkRemoteProjectStatus(ctx context.Context, projectRef string, fsys afero.Fs) error {
6✔
262
        resp, err := utils.GetSupabase().V1GetProjectWithResponse(ctx, projectRef)
6✔
263
        if err != nil {
6✔
264
                return errors.Errorf("failed to retrieve remote project status: %w", err)
×
265
        }
×
266
        switch resp.StatusCode() {
6✔
267
        case http.StatusNotFound:
1✔
268
                // Ignore not found error to support linking branch projects
1✔
269
                return nil
1✔
270
        case http.StatusOK:
5✔
271
                // resp.JSON200 is not nil, proceed
272
        default:
×
273
                return errors.New("Unexpected error retrieving remote project status: " + string(resp.Body))
×
274
        }
275

276
        switch resp.JSON200.Status {
5✔
277
        case api.V1ProjectWithDatabaseResponseStatusINACTIVE:
1✔
278
                utils.CmdSuggestion = fmt.Sprintf("An admin must unpause it from the Supabase dashboard at %s", utils.Aqua(fmt.Sprintf("%s/project/%s", utils.GetSupabaseDashboardURL(), projectRef)))
1✔
279
                return errors.New(errProjectPaused)
1✔
280
        case api.V1ProjectWithDatabaseResponseStatusACTIVEHEALTHY:
4✔
281
                // Project is in the desired state, do nothing
282
        default:
×
283
                fmt.Fprintf(os.Stderr, "%s: Project status is %s instead of Active Healthy. Some operations might fail.\n", utils.Yellow("WARNING"), resp.JSON200.Status)
×
284
        }
285

286
        // Update postgres image version to match the remote project
287
        if version := resp.JSON200.Database.Version; len(version) > 0 {
6✔
288
                return utils.WriteFile(utils.PostgresVersionPath, []byte(version), fsys)
2✔
289
        }
2✔
290
        return nil
2✔
291
}
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