• 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

76.17
/internal/status/status.go
1
package status
2

3
import (
4
        "context"
5
        "crypto/tls"
6
        "crypto/x509"
7
        _ "embed"
8
        "fmt"
9
        "io"
10
        "net/http"
11
        "net/url"
12
        "os"
13
        "reflect"
14
        "slices"
15
        "strings"
16
        "sync"
17
        "time"
18

19
        "github.com/docker/docker/api/types"
20
        "github.com/docker/docker/api/types/container"
21
        "github.com/go-errors/errors"
22
        "github.com/spf13/afero"
23
        "github.com/supabase/cli/internal/utils"
24
        "github.com/supabase/cli/internal/utils/flags"
25
        "github.com/supabase/cli/pkg/api"
26
        "github.com/supabase/cli/pkg/fetcher"
27
)
28

29
type CustomName struct {
30
        ApiURL                   string `env:"api.url,default=API_URL"`
31
        GraphqlURL               string `env:"api.graphql_url,default=GRAPHQL_URL"`
32
        StorageS3URL             string `env:"api.storage_s3_url,default=STORAGE_S3_URL"`
33
        McpURL                   string `env:"api.mcp_url,default=MCP_URL"`
34
        DbURL                    string `env:"db.url,default=DB_URL"`
35
        StudioURL                string `env:"studio.url,default=STUDIO_URL"`
36
        InbucketURL              string `env:"inbucket.url,default=INBUCKET_URL,deprecated"`
37
        MailpitURL               string `env:"mailpit.url,default=MAILPIT_URL"`
38
        PublishableKey           string `env:"auth.publishable_key,default=PUBLISHABLE_KEY"`
39
        SecretKey                string `env:"auth.secret_key,default=SECRET_KEY"`
40
        JWTSecret                string `env:"auth.jwt_secret,default=JWT_SECRET,deprecated"`
41
        AnonKey                  string `env:"auth.anon_key,default=ANON_KEY,deprecated"`
42
        ServiceRoleKey           string `env:"auth.service_role_key,default=SERVICE_ROLE_KEY,deprecated"`
43
        StorageS3AccessKeyId     string `env:"storage.s3_access_key_id,default=S3_PROTOCOL_ACCESS_KEY_ID"`
44
        StorageS3SecretAccessKey string `env:"storage.s3_secret_access_key,default=S3_PROTOCOL_ACCESS_KEY_SECRET"`
45
        StorageS3Region          string `env:"storage.s3_region,default=S3_PROTOCOL_REGION"`
46
}
47

48
func (c *CustomName) toValues(exclude ...string) map[string]string {
6✔
49
        values := map[string]string{
6✔
50
                c.DbURL: fmt.Sprintf("postgresql://%s@%s:%d/postgres", url.UserPassword("postgres", utils.Config.Db.Password), utils.Config.Hostname, utils.Config.Db.Port),
6✔
51
        }
6✔
52

6✔
53
        apiEnabled := utils.Config.Api.Enabled && !slices.Contains(exclude, utils.RestId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Api.Image))
6✔
54
        studioEnabled := utils.Config.Studio.Enabled && !slices.Contains(exclude, utils.StudioId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Studio.Image))
6✔
55
        authEnabled := utils.Config.Auth.Enabled && !slices.Contains(exclude, utils.GotrueId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Auth.Image))
6✔
56
        inbucketEnabled := utils.Config.Inbucket.Enabled && !slices.Contains(exclude, utils.InbucketId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Inbucket.Image))
6✔
57
        storageEnabled := utils.Config.Storage.Enabled && !slices.Contains(exclude, utils.StorageId) && !slices.Contains(exclude, utils.ShortContainerImageName(utils.Config.Storage.Image))
6✔
58

6✔
59
        if apiEnabled {
6✔
60
                values[c.ApiURL] = utils.Config.Api.ExternalUrl
×
61
                values[c.GraphqlURL] = utils.GetApiUrl("/graphql/v1")
×
62
                if studioEnabled {
×
63
                        values[c.McpURL] = utils.GetApiUrl("/mcp")
×
64
                }
×
65
        }
66
        if studioEnabled {
6✔
67
                values[c.StudioURL] = fmt.Sprintf("http://%s:%d", utils.Config.Hostname, utils.Config.Studio.Port)
×
68
        }
×
69
        if authEnabled {
6✔
70
                values[c.PublishableKey] = utils.Config.Auth.PublishableKey.Value
×
71
                values[c.SecretKey] = utils.Config.Auth.SecretKey.Value
×
72
                values[c.JWTSecret] = utils.Config.Auth.JwtSecret.Value
×
73
                values[c.AnonKey] = utils.Config.Auth.AnonKey.Value
×
74
                values[c.ServiceRoleKey] = utils.Config.Auth.ServiceRoleKey.Value
×
75
        }
×
76
        if inbucketEnabled {
6✔
77
                values[c.MailpitURL] = fmt.Sprintf("http://%s:%d", utils.Config.Hostname, utils.Config.Inbucket.Port)
×
78
                values[c.InbucketURL] = fmt.Sprintf("http://%s:%d", utils.Config.Hostname, utils.Config.Inbucket.Port)
×
79
        }
×
80
        if storageEnabled {
6✔
81
                values[c.StorageS3URL] = utils.GetApiUrl("/storage/v1/s3")
×
82
                values[c.StorageS3AccessKeyId] = utils.Config.Storage.S3Credentials.AccessKeyId
×
83
                values[c.StorageS3SecretAccessKey] = utils.Config.Storage.S3Credentials.SecretAccessKey
×
84
                values[c.StorageS3Region] = utils.Config.Storage.S3Credentials.Region
×
85
        }
×
86
        return values
6✔
87
}
88

89
func Run(ctx context.Context, names CustomName, format string, fsys afero.Fs) error {
4✔
90
        // Sanity checks.
4✔
91
        if err := flags.LoadConfig(fsys); err != nil {
5✔
92
                return err
1✔
93
        }
1✔
94
        if err := assertContainerHealthy(ctx, utils.DbId); err != nil {
4✔
95
                return err
1✔
96
        }
1✔
97
        stopped, err := checkServiceHealth(ctx)
2✔
98
        if err != nil {
2✔
99
                return err
×
100
        }
×
101
        if len(stopped) > 0 {
4✔
102
                fmt.Fprintln(os.Stderr, "Stopped services:", stopped)
2✔
103
        }
2✔
104
        if format == utils.OutputPretty {
4✔
105
                fmt.Fprintf(os.Stderr, "%s local development setup is running.\n\n", utils.Aqua("supabase"))
2✔
106
                PrettyPrint(os.Stdout, stopped...)
2✔
107
                return nil
2✔
108
        }
2✔
109
        return printStatus(names, format, os.Stdout, stopped...)
×
110
}
111

112
func checkServiceHealth(ctx context.Context) ([]string, error) {
5✔
113
        resp, err := utils.Docker.ContainerList(ctx, container.ListOptions{
5✔
114
                Filters: utils.CliProjectFilter(utils.Config.ProjectId),
5✔
115
        })
5✔
116
        if err != nil {
6✔
117
                return nil, errors.Errorf("failed to list running containers: %w", err)
1✔
118
        }
1✔
119
        running := make(map[string]struct{}, len(resp))
4✔
120
        for _, c := range resp {
43✔
121
                for _, n := range c.Names {
78✔
122
                        running[n] = struct{}{}
39✔
123
                }
39✔
124
        }
125
        var stopped []string
4✔
126
        for _, containerId := range utils.GetDockerIds() {
56✔
127
                if _, ok := running["/"+containerId]; !ok {
91✔
128
                        stopped = append(stopped, containerId)
39✔
129
                }
39✔
130
        }
131
        return stopped, nil
4✔
132
}
133

134
func assertContainerHealthy(ctx context.Context, container string) error {
30✔
135
        if resp, err := utils.Docker.ContainerInspect(ctx, container); err != nil {
31✔
136
                return errors.Errorf("failed to inspect container health: %w", err)
1✔
137
        } else if !resp.State.Running {
33✔
138
                return errors.Errorf("%s container is not running: %s", container, resp.State.Status)
3✔
139
        } else if resp.State.Health != nil && resp.State.Health.Status != types.Healthy {
29✔
140
                return errors.Errorf("%s container is not ready: %s", container, resp.State.Health.Status)
×
141
        }
×
142
        return nil
26✔
143
}
144

145
func IsServiceReady(ctx context.Context, container string) error {
29✔
146
        if container == utils.RestId {
30✔
147
                // PostgREST does not support native health checks
1✔
148
                return checkHTTPHead(ctx, "/rest-admin/v1/ready")
1✔
149
        }
1✔
150
        if container == utils.EdgeRuntimeId {
29✔
151
                // Native health check logs too much hyper::Error(IncompleteMessage)
1✔
152
                return checkHTTPHead(ctx, "/functions/v1/_internal/health")
1✔
153
        }
1✔
154
        return assertContainerHealthy(ctx, container)
27✔
155
}
156

157
// To regenerate local certificate pair:
158
//
159
//        openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 \
160
//          -nodes -keyout kong.local.key -out kong.local.crt -subj "/CN=localhost" \
161
//          -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
162
func NewKongClient() *http.Client {
5✔
163
        client := &http.Client{
5✔
164
                Timeout: 10 * time.Second,
5✔
165
        }
5✔
166
        if t, ok := http.DefaultTransport.(*http.Transport); ok {
5✔
167
                pool, err := x509.SystemCertPool()
×
168
                if err != nil {
×
169
                        fmt.Fprintln(utils.GetDebugLogger(), err)
×
170
                        pool = x509.NewCertPool()
×
171
                }
×
172
                // No need to replace TLS config if we fail to append cert
173
                if pool.AppendCertsFromPEM(utils.Config.Api.Tls.CertContent) {
×
174
                        rt := t.Clone()
×
175
                        rt.TLSClientConfig = &tls.Config{
×
176
                                MinVersion: tls.VersionTLS12,
×
177
                                RootCAs:    pool,
×
178
                        }
×
179
                        client.Transport = rt
×
180
                }
×
181
        }
182
        return client
5✔
183
}
184

185
var (
186
        healthClient *fetcher.Fetcher
187
        healthOnce   sync.Once
188
)
189

190
func checkHTTPHead(ctx context.Context, path string) error {
2✔
191
        healthOnce.Do(func() {
3✔
192
                healthClient = fetcher.NewServiceGateway(
1✔
193
                        utils.Config.Api.ExternalUrl,
1✔
194
                        utils.Config.Auth.SecretKey.Value,
1✔
195
                        fetcher.WithHTTPClient(NewKongClient()),
1✔
196
                        fetcher.WithUserAgent("SupabaseCLI/"+utils.Version),
1✔
197
                )
1✔
198
        })
1✔
199
        // HEAD method does not return response body
200
        resp, err := healthClient.Send(ctx, http.MethodHead, path, nil)
2✔
201
        if err != nil {
2✔
202
                return err
×
203
        }
×
204
        defer resp.Body.Close()
2✔
205
        return nil
2✔
206
}
207

208
func printStatus(names CustomName, format string, w io.Writer, exclude ...string) (err error) {
4✔
209
        values := names.toValues(exclude...)
4✔
210
        return utils.EncodeOutput(format, w, values)
4✔
211
}
4✔
212

213
func PrettyPrint(w io.Writer, exclude ...string) {
2✔
214
        names := CustomName{
2✔
215
                ApiURL:                   "         " + utils.Aqua("API URL"),
2✔
216
                GraphqlURL:               "     " + utils.Aqua("GraphQL URL"),
2✔
217
                StorageS3URL:             "  " + utils.Aqua("S3 Storage URL"),
2✔
218
                McpURL:                   "         " + utils.Aqua("MCP URL"),
2✔
219
                DbURL:                    "    " + utils.Aqua("Database URL"),
2✔
220
                StudioURL:                "      " + utils.Aqua("Studio URL"),
2✔
221
                InbucketURL:              "    " + utils.Aqua("Inbucket URL"),
2✔
222
                MailpitURL:               "     " + utils.Aqua("Mailpit URL"),
2✔
223
                PublishableKey:           " " + utils.Aqua("Publishable key"),
2✔
224
                SecretKey:                "      " + utils.Aqua("Secret key"),
2✔
225
                JWTSecret:                "      " + utils.Aqua("JWT secret"),
2✔
226
                AnonKey:                  "        " + utils.Aqua("anon key"),
2✔
227
                ServiceRoleKey:           "" + utils.Aqua("service_role key"),
2✔
228
                StorageS3AccessKeyId:     "   " + utils.Aqua("S3 Access Key"),
2✔
229
                StorageS3SecretAccessKey: "   " + utils.Aqua("S3 Secret Key"),
2✔
230
                StorageS3Region:          "       " + utils.Aqua("S3 Region"),
2✔
231
        }
2✔
232
        values := names.toValues(exclude...)
2✔
233
        // Iterate through map in order of declared struct fields
2✔
234
        t := reflect.TypeOf(names)
2✔
235
        val := reflect.ValueOf(names)
2✔
236
        for i := 0; i < val.NumField(); i++ {
34✔
237
                k := val.Field(i).String()
32✔
238
                if tag := t.Field(i).Tag.Get("env"); isDeprecated(tag) {
40✔
239
                        continue
8✔
240
                }
241
                if v, ok := values[k]; ok {
26✔
242
                        fmt.Fprintf(w, "%s: %s\n", k, v)
2✔
243
                }
2✔
244
        }
245
}
246

247
func isDeprecated(tag string) bool {
32✔
248
        for part := range strings.SplitSeq(tag, ",") {
104✔
249
                if strings.EqualFold(part, "deprecated") {
80✔
250
                        return true
8✔
251
                }
8✔
252
        }
253
        return false
24✔
254
}
255

256
func RunRemote(ctx context.Context, format string, fsys afero.Fs) error {
2✔
257
        // Parse project ref
2✔
258
        if err := flags.ParseProjectRef(ctx, fsys); err != nil {
3✔
259
                return err
1✔
260
        }
1✔
261

262
        // Define services to check
263
        services := []api.V1GetServicesHealthParamsServices{
1✔
264
                api.Auth,
1✔
265
                api.Realtime,
1✔
266
                api.Rest,
1✔
267
                api.Storage,
1✔
268
                api.Db,
1✔
269
        }
1✔
270

1✔
271
        // Call health check API
1✔
272
        resp, err := utils.GetSupabase().V1GetServicesHealthWithResponse(ctx, flags.ProjectRef, &api.V1GetServicesHealthParams{
1✔
273
                Services: services,
1✔
274
        })
1✔
275
        if err != nil {
1✔
NEW
276
                return errors.Errorf("failed to check remote health: %w", err)
×
NEW
277
        }
×
278
        if resp.JSON200 == nil {
1✔
NEW
279
                return errors.New("Unexpected error checking remote health: " + string(resp.Body))
×
NEW
280
        }
×
281

282
        // Print results
283
        if format == utils.OutputPretty {
2✔
284
                return prettyPrintRemoteHealth(os.Stdout, *resp.JSON200)
1✔
285
        }
1✔
NEW
286
        return utils.EncodeOutput(format, os.Stdout, resp.JSON200)
×
287
}
288

289
func prettyPrintRemoteHealth(w io.Writer, health []api.V1ServiceHealthResponse) error {
1✔
290
        fmt.Fprintf(w, "\n")
1✔
291
        for _, service := range health {
6✔
292
                statusSymbol := "✓"
5✔
293
                statusColor := utils.Green
5✔
294
                if !service.Healthy {
5✔
NEW
295
                        statusSymbol = "✗"
×
NEW
296
                        statusColor = utils.Red
×
NEW
297
                }
×
298

299
                fmt.Fprintf(w, "%s %s %s\n", statusColor(statusSymbol), utils.Aqua(string(service.Name)), utils.Dim(string(service.Status)))
5✔
300

5✔
301
                if service.Error != nil && *service.Error != "" {
5✔
NEW
302
                        fmt.Fprintf(w, "  Error: %s\n", utils.Red(*service.Error))
×
NEW
303
                }
×
304
        }
305
        fmt.Fprintf(w, "\n")
1✔
306

1✔
307
        return nil
1✔
308
}
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