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

supabase / cli / 18558080767

16 Oct 2025 10:20AM UTC coverage: 54.579% (-0.008%) from 54.587%
18558080767

Pull #4319

github

web-flow
Merge 73845860d into af399f974
Pull Request #4319: feat: add flag to skip linking pooler

23 of 29 new or added lines in 4 files covered. (79.31%)

7 existing lines in 2 files now uncovered.

6389 of 11706 relevant lines covered (54.58%)

6.09 hits per line

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

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

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

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

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

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

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

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

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

61
func LinkServices(ctx context.Context, projectRef, serviceKey string, skipPooler bool, fsys afero.Fs) {
3✔
62
        jq := queue.NewJobQueue(5)
3✔
63
        api := tenant.NewTenantAPI(ctx, projectRef, serviceKey)
3✔
64
        jobs := []func() error{
3✔
65
                func() error { return linkDatabaseSettings(ctx, projectRef) },
6✔
66
                func() error { return linkNetworkRestrictions(ctx, projectRef) },
3✔
67
                func() error { return linkPostgrest(ctx, projectRef) },
3✔
68
                func() error { return linkGotrue(ctx, projectRef) },
3✔
69
                func() error { return linkStorage(ctx, projectRef) },
3✔
70
                func() error {
3✔
71
                        if skipPooler {
3✔
NEW
72
                                return fsys.RemoveAll(utils.PoolerUrlPath)
×
NEW
73
                        }
×
74
                        return linkPooler(ctx, projectRef, fsys)
3✔
75
                },
76
                func() error { return linkPostgrestVersion(ctx, api, fsys) },
3✔
77
                func() error { return linkGotrueVersion(ctx, api, fsys) },
3✔
78
                func() error { return linkStorageVersion(ctx, api, fsys) },
3✔
79
        }
80
        // Ignore non-fatal errors linking services
81
        logger := utils.GetDebugLogger()
3✔
82
        for _, job := range jobs {
30✔
83
                if err := jq.Put(job); err != nil {
35✔
84
                        fmt.Fprintln(logger, err)
8✔
85
                }
8✔
86
        }
87
        if err := jq.Collect(); err != nil {
6✔
88
                fmt.Fprintln(logger, err)
3✔
89
        }
3✔
90
}
91

92
func linkPostgrest(ctx context.Context, projectRef string) error {
7✔
93
        resp, err := utils.GetSupabase().V1GetPostgrestServiceConfigWithResponse(ctx, projectRef)
7✔
94
        if err != nil {
10✔
95
                return errors.Errorf("failed to read API config: %w", err)
3✔
96
        } else if resp.JSON200 == nil {
8✔
97
                return errors.Errorf("unexpected API config status %d: %s", resp.StatusCode(), string(resp.Body))
1✔
98
        }
1✔
99
        utils.Config.Api.FromRemoteApiConfig(*resp.JSON200)
3✔
100
        return nil
3✔
101
}
102

103
func linkPostgrestVersion(ctx context.Context, api tenant.TenantAPI, fsys afero.Fs) error {
3✔
104
        version, err := api.GetPostgrestVersion(ctx)
3✔
105
        if err != nil {
5✔
106
                return err
2✔
107
        }
2✔
108
        return utils.WriteFile(utils.RestVersionPath, []byte(version), fsys)
1✔
109
}
110

111
func linkGotrue(ctx context.Context, projectRef string) error {
3✔
112
        resp, err := utils.GetSupabase().V1GetAuthServiceConfigWithResponse(ctx, projectRef)
3✔
113
        if err != nil {
6✔
114
                return errors.Errorf("failed to read Auth config: %w", err)
3✔
115
        } else if resp.JSON200 == nil {
3✔
116
                return errors.Errorf("unexpected Auth config status %d: %s", resp.StatusCode(), string(resp.Body))
×
117
        }
×
118
        utils.Config.Auth.FromRemoteAuthConfig(*resp.JSON200)
×
119
        return nil
×
120
}
121

122
func linkGotrueVersion(ctx context.Context, api tenant.TenantAPI, fsys afero.Fs) error {
3✔
123
        version, err := api.GetGotrueVersion(ctx)
3✔
124
        if err != nil {
5✔
125
                return err
2✔
126
        }
2✔
127
        return utils.WriteFile(utils.GotrueVersionPath, []byte(version), fsys)
1✔
128
}
129

130
func linkStorage(ctx context.Context, projectRef string) error {
3✔
131
        resp, err := utils.GetSupabase().V1GetStorageConfigWithResponse(ctx, projectRef)
3✔
132
        if err != nil {
5✔
133
                return errors.Errorf("failed to read Storage config: %w", err)
2✔
134
        } else if resp.JSON200 == nil {
3✔
135
                return errors.Errorf("unexpected Storage config status %d: %s", resp.StatusCode(), string(resp.Body))
×
136
        }
×
137
        utils.Config.Storage.FromRemoteStorageConfig(*resp.JSON200)
1✔
138
        return nil
1✔
139
}
140

141
func linkStorageVersion(ctx context.Context, api tenant.TenantAPI, fsys afero.Fs) error {
3✔
142
        version, err := api.GetStorageVersion(ctx)
3✔
143
        if err != nil {
5✔
144
                return err
2✔
145
        }
2✔
146
        return utils.WriteFile(utils.StorageVersionPath, []byte(version), fsys)
1✔
147
}
148

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

151
func linkStorageMigration(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) error {
5✔
152
        var name string
5✔
153
        if err := conn.QueryRow(ctx, GET_LATEST_STORAGE_MIGRATION).Scan(&name); err != nil {
6✔
154
                return errors.Errorf("failed to fetch storage migration: %w", err)
1✔
155
        }
1✔
156
        return utils.WriteFile(utils.StorageMigrationPath, []byte(name), fsys)
4✔
157
}
158

159
func linkDatabaseSettings(ctx context.Context, projectRef string) error {
3✔
160
        resp, err := utils.GetSupabase().V1GetPostgresConfigWithResponse(ctx, projectRef)
3✔
161
        if err != nil {
5✔
162
                return errors.Errorf("failed to read DB config: %w", err)
2✔
163
        } else if resp.JSON200 == nil {
3✔
164
                return errors.Errorf("unexpected DB config status %d: %s", resp.StatusCode(), string(resp.Body))
×
165
        }
×
166
        utils.Config.Db.Settings.FromRemotePostgresConfig(*resp.JSON200)
1✔
167
        return nil
1✔
168
}
169

170
func linkNetworkRestrictions(ctx context.Context, projectRef string) error {
3✔
171
        resp, err := utils.GetSupabase().V1GetNetworkRestrictionsWithResponse(ctx, projectRef)
3✔
172
        if err != nil {
3✔
173
                return errors.Errorf("failed to read network restrictions: %w", err)
×
174
        } else if resp.JSON200 == nil {
3✔
175
                return errors.Errorf("unexpected network restrictions status %d: %s", resp.StatusCode(), string(resp.Body))
×
176
        }
×
177
        utils.Config.Db.NetworkRestrictions.FromRemoteNetworkRestrictions(*resp.JSON200)
3✔
178
        return nil
3✔
179
}
180

181
func linkDatabase(ctx context.Context, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
7✔
182
        conn, err := utils.ConnectByConfig(ctx, config, options...)
7✔
183
        if err != nil {
9✔
184
                return err
2✔
185
        }
2✔
186
        defer conn.Close(context.Background())
5✔
187
        updatePostgresConfig(conn)
5✔
188
        if err := linkStorageMigration(ctx, conn, fsys); err != nil {
7✔
189
                fmt.Fprintln(os.Stderr, err)
2✔
190
        }
2✔
191
        // If `schema_migrations` doesn't exist on the remote database, create it.
192
        if err := migration.CreateMigrationTable(ctx, conn); err != nil {
6✔
193
                return err
1✔
194
        }
1✔
195
        return migration.CreateSeedTable(ctx, conn)
4✔
196
}
197

198
func updatePostgresConfig(conn *pgx.Conn) {
5✔
199
        serverVersion := conn.PgConn().ParameterStatus("server_version")
5✔
200
        // Safe to assume that supported Postgres version is 10.0 <= n < 100.0
5✔
201
        majorDigits := min(len(serverVersion), 2)
5✔
202
        // Treat error as unchanged
5✔
203
        if dbMajorVersion, err := strconv.ParseUint(serverVersion[:majorDigits], 10, 7); err == nil {
9✔
204
                utils.Config.Db.MajorVersion = uint(dbMajorVersion)
4✔
205
        }
4✔
206
}
207

208
func linkPooler(ctx context.Context, projectRef string, fsys afero.Fs) error {
3✔
209
        resp, err := utils.GetSupabase().V1GetPoolerConfigWithResponse(ctx, projectRef)
3✔
210
        if err != nil {
6✔
211
                return errors.Errorf("failed to get pooler config: %w", err)
3✔
212
        }
3✔
213
        if resp.JSON200 == nil {
×
214
                return errors.Errorf("%w: %s", tenant.ErrAuthToken, string(resp.Body))
×
215
        }
×
216
        for _, config := range *resp.JSON200 {
×
217
                if config.DatabaseType == api.PRIMARY {
×
218
                        updatePoolerConfig(config)
×
219
                }
×
220
        }
221
        return utils.WriteFile(utils.PoolerUrlPath, []byte(utils.Config.Db.Pooler.ConnectionString), fsys)
×
222
}
223

224
func updatePoolerConfig(config api.SupavisorConfigResponse) {
×
225
        // Remove password from pooler connection string because the placeholder text
×
226
        // [YOUR-PASSWORD] messes up pgconn.ParseConfig. The password must be percent
×
227
        // escaped so we cannot simply call strings.Replace with actual password.
×
228
        utils.Config.Db.Pooler.ConnectionString = strings.ReplaceAll(config.ConnectionString, ":[YOUR-PASSWORD]", "")
×
229
        // Always use session mode for running migrations
×
230
        if utils.Config.Db.Pooler.PoolMode = cliConfig.SessionMode; config.PoolMode != api.SupavisorConfigResponsePoolModeSession {
×
231
                utils.Config.Db.Pooler.ConnectionString = strings.ReplaceAll(utils.Config.Db.Pooler.ConnectionString, ":6543/", ":5432/")
×
232
        }
×
233
        if value, err := config.DefaultPoolSize.Get(); err == nil {
×
234
                utils.Config.Db.Pooler.DefaultPoolSize = cast.IntToUint(value)
×
235
        }
×
236
        if value, err := config.MaxClientConn.Get(); err == nil {
×
237
                utils.Config.Db.Pooler.MaxClientConn = cast.IntToUint(value)
×
238
        }
×
239
}
240

241
var errProjectPaused = errors.New("project is paused")
242

243
func checkRemoteProjectStatus(ctx context.Context, projectRef string, fsys afero.Fs) error {
6✔
244
        resp, err := utils.GetSupabase().V1GetProjectWithResponse(ctx, projectRef)
6✔
245
        if err != nil {
6✔
246
                return errors.Errorf("failed to retrieve remote project status: %w", err)
×
247
        }
×
248
        switch resp.StatusCode() {
6✔
249
        case http.StatusNotFound:
1✔
250
                // Ignore not found error to support linking branch projects
1✔
251
                return nil
1✔
252
        case http.StatusOK:
5✔
253
                // resp.JSON200 is not nil, proceed
254
        default:
×
255
                return errors.New("Unexpected error retrieving remote project status: " + string(resp.Body))
×
256
        }
257

258
        switch resp.JSON200.Status {
5✔
259
        case api.V1ProjectWithDatabaseResponseStatusINACTIVE:
1✔
260
                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✔
261
                return errors.New(errProjectPaused)
1✔
262
        case api.V1ProjectWithDatabaseResponseStatusACTIVEHEALTHY:
4✔
263
                // Project is in the desired state, do nothing
264
        default:
×
265
                fmt.Fprintf(os.Stderr, "%s: Project status is %s instead of Active Healthy. Some operations might fail.\n", utils.Yellow("WARNING"), resp.JSON200.Status)
×
266
        }
267

268
        // Update postgres image version to match the remote project
269
        if version := resp.JSON200.Database.Version; len(version) > 0 {
6✔
270
                return utils.WriteFile(utils.PostgresVersionPath, []byte(version), fsys)
2✔
271
        }
2✔
272
        return nil
2✔
273
}
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