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

supabase / cli / 15239106693

25 May 2025 02:59PM UTC coverage: 60.201% (-0.02%) from 60.221%
15239106693

Pull #3609

github

web-flow
Merge 21c588ad3 into 09d487004
Pull Request #3609: Update mailpit label for supabase status in terminal and other reference to inbucket

22 of 26 new or added lines in 5 files covered. (84.62%)

9 existing lines in 4 files now uncovered.

9026 of 14993 relevant lines covered (60.2%)

509.14 hits per line

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

75.84
/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
        "sync"
15
        "time"
16

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

26
type CustomName struct {
27
        ApiURL                   string `env:"api.url,default=API_URL"`
28
        GraphqlURL               string `env:"api.graphql_url,default=GRAPHQL_URL"`
29
        StorageS3URL             string `env:"api.storage_s3_url,default=STORAGE_S3_URL"`
30
        DbURL                    string `env:"db.url,default=DB_URL"`
31
        StudioURL                string `env:"studio.url,default=STUDIO_URL"`
32
        MailpitURL              string `env:"mailpit.url,default=MAILPIT_URL"`
33
        JWTSecret                string `env:"auth.jwt_secret,default=JWT_SECRET"`
34
        AnonKey                  string `env:"auth.anon_key,default=ANON_KEY"`
35
        ServiceRoleKey           string `env:"auth.service_role_key,default=SERVICE_ROLE_KEY"`
36
        StorageS3AccessKeyId     string `env:"storage.s3_access_key_id,default=S3_PROTOCOL_ACCESS_KEY_ID"`
37
        StorageS3SecretAccessKey string `env:"storage.s3_secret_access_key,default=S3_PROTOCOL_ACCESS_KEY_SECRET"`
38
        StorageS3Region          string `env:"storage.s3_region,default=S3_PROTOCOL_REGION"`
39
}
40

41
func (c *CustomName) toValues(exclude ...string) map[string]string {
6✔
42
        values := map[string]string{
6✔
43
                c.DbURL: fmt.Sprintf("postgresql://%s@%s:%d/postgres", url.UserPassword("postgres", utils.Config.Db.Password), utils.Config.Hostname, utils.Config.Db.Port),
6✔
44
        }
6✔
45
        if utils.Config.Api.Enabled && !utils.SliceContains(exclude, utils.RestId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Api.Image)) {
6✔
46
                values[c.ApiURL] = utils.Config.Api.ExternalUrl
×
47
                values[c.GraphqlURL] = utils.GetApiUrl("/graphql/v1")
×
48
        }
×
49
        if utils.Config.Studio.Enabled && !utils.SliceContains(exclude, utils.StudioId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Studio.Image)) {
6✔
50
                values[c.StudioURL] = fmt.Sprintf("http://%s:%d", utils.Config.Hostname, utils.Config.Studio.Port)
×
51
        }
×
52
        if utils.Config.Auth.Enabled && !utils.SliceContains(exclude, utils.GotrueId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Auth.Image)) {
6✔
53
                values[c.JWTSecret] = utils.Config.Auth.JwtSecret.Value
×
54
                values[c.AnonKey] = utils.Config.Auth.AnonKey.Value
×
55
                values[c.ServiceRoleKey] = utils.Config.Auth.ServiceRoleKey.Value
×
56
        }
×
57
        if utils.Config.Mailpit.Enabled && !utils.SliceContains(exclude, utils.MailpitId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Mailpit.Image)) {
6✔
NEW
58
                values[c.MailpitURL] = fmt.Sprintf("http://%s:%d", utils.Config.Hostname, utils.Config.Mailpit.Port)
×
UNCOV
59
        }
×
60
        if utils.Config.Storage.Enabled && !utils.SliceContains(exclude, utils.StorageId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Storage.Image)) {
6✔
61
                values[c.StorageS3URL] = utils.GetApiUrl("/storage/v1/s3")
×
62
                values[c.StorageS3AccessKeyId] = utils.Config.Storage.S3Credentials.AccessKeyId
×
63
                values[c.StorageS3SecretAccessKey] = utils.Config.Storage.S3Credentials.SecretAccessKey
×
64
                values[c.StorageS3Region] = utils.Config.Storage.S3Credentials.Region
×
65
        }
×
66
        return values
6✔
67
}
68

69
func Run(ctx context.Context, names CustomName, format string, fsys afero.Fs) error {
4✔
70
        // Sanity checks.
4✔
71
        if err := flags.LoadConfig(fsys); err != nil {
5✔
72
                return err
1✔
73
        }
1✔
74
        if err := assertContainerHealthy(ctx, utils.DbId); err != nil {
4✔
75
                return err
1✔
76
        }
1✔
77
        stopped, err := checkServiceHealth(ctx)
2✔
78
        if err != nil {
2✔
79
                return err
×
80
        }
×
81
        if len(stopped) > 0 {
4✔
82
                fmt.Fprintln(os.Stderr, "Stopped services:", stopped)
2✔
83
        }
2✔
84
        if format == utils.OutputPretty {
4✔
85
                fmt.Fprintf(os.Stderr, "%s local development setup is running.\n\n", utils.Aqua("supabase"))
2✔
86
                PrettyPrint(os.Stdout, stopped...)
2✔
87
                return nil
2✔
88
        }
2✔
89
        return printStatus(names, format, os.Stdout, stopped...)
×
90
}
91

92
func checkServiceHealth(ctx context.Context) ([]string, error) {
5✔
93
        resp, err := utils.Docker.ContainerList(ctx, container.ListOptions{
5✔
94
                Filters: utils.CliProjectFilter(utils.Config.ProjectId),
5✔
95
        })
5✔
96
        if err != nil {
6✔
97
                return nil, errors.Errorf("failed to list running containers: %w", err)
1✔
98
        }
1✔
99
        running := make(map[string]struct{}, len(resp))
4✔
100
        for _, c := range resp {
43✔
101
                for _, n := range c.Names {
78✔
102
                        running[n] = struct{}{}
39✔
103
                }
39✔
104
        }
105
        var stopped []string
4✔
106
        for _, containerId := range utils.GetDockerIds() {
56✔
107
                if _, ok := running["/"+containerId]; !ok {
91✔
108
                        stopped = append(stopped, containerId)
39✔
109
                }
39✔
110
        }
111
        return stopped, nil
4✔
112
}
113

114
func assertContainerHealthy(ctx context.Context, container string) error {
30✔
115
        if resp, err := utils.Docker.ContainerInspect(ctx, container); err != nil {
31✔
116
                return errors.Errorf("failed to inspect container health: %w", err)
1✔
117
        } else if !resp.State.Running {
33✔
118
                return errors.Errorf("%s container is not running: %s", container, resp.State.Status)
3✔
119
        } else if resp.State.Health != nil && resp.State.Health.Status != types.Healthy {
29✔
120
                return errors.Errorf("%s container is not ready: %s", container, resp.State.Health.Status)
×
121
        }
×
122
        return nil
26✔
123
}
124

125
func IsServiceReady(ctx context.Context, container string) error {
29✔
126
        if container == utils.RestId {
30✔
127
                // PostgREST does not support native health checks
1✔
128
                return checkHTTPHead(ctx, "/rest-admin/v1/ready")
1✔
129
        }
1✔
130
        if container == utils.EdgeRuntimeId {
29✔
131
                // Native health check logs too much hyper::Error(IncompleteMessage)
1✔
132
                return checkHTTPHead(ctx, "/functions/v1/_internal/health")
1✔
133
        }
1✔
134
        return assertContainerHealthy(ctx, container)
27✔
135
}
136

137
var (
138
        //go:embed kong.local.crt
139
        KongCert string
140
        //go:embed kong.local.key
141
        KongKey string
142
)
143

144
// To regenerate local certificate pair:
145
//
146
//        openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 \
147
//          -nodes -keyout kong.local.key -out kong.local.crt -subj "/CN=localhost" \
148
//          -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
149
func NewKongClient() *http.Client {
5✔
150
        client := &http.Client{
5✔
151
                Timeout: 10 * time.Second,
5✔
152
        }
5✔
153
        if t, ok := http.DefaultTransport.(*http.Transport); ok {
5✔
154
                pool, err := x509.SystemCertPool()
×
155
                if err != nil {
×
156
                        fmt.Fprintln(utils.GetDebugLogger(), err)
×
157
                        pool = x509.NewCertPool()
×
158
                }
×
159
                // No need to replace TLS config if we fail to append cert
160
                if pool.AppendCertsFromPEM([]byte(KongCert)) {
×
161
                        rt := t.Clone()
×
162
                        rt.TLSClientConfig = &tls.Config{
×
163
                                MinVersion: tls.VersionTLS12,
×
164
                                RootCAs:    pool,
×
165
                        }
×
166
                        client.Transport = rt
×
167
                }
×
168
        }
169
        return client
5✔
170
}
171

172
var (
173
        healthClient *fetcher.Fetcher
174
        healthOnce   sync.Once
175
)
176

177
func checkHTTPHead(ctx context.Context, path string) error {
2✔
178
        healthOnce.Do(func() {
3✔
179
                server := utils.Config.Api.ExternalUrl
1✔
180
                header := func(req *http.Request) {
3✔
181
                        req.Header.Add("apikey", utils.Config.Auth.AnonKey.Value)
2✔
182
                }
2✔
183
                client := NewKongClient()
1✔
184
                healthClient = fetcher.NewFetcher(
1✔
185
                        server,
1✔
186
                        fetcher.WithHTTPClient(client),
1✔
187
                        fetcher.WithRequestEditor(header),
1✔
188
                        fetcher.WithExpectedStatus(http.StatusOK),
1✔
189
                )
1✔
190
        })
191
        // HEAD method does not return response body
192
        resp, err := healthClient.Send(ctx, http.MethodHead, path, nil)
2✔
193
        if err != nil {
2✔
194
                return err
×
195
        }
×
196
        defer resp.Body.Close()
2✔
197
        return nil
2✔
198
}
199

200
func printStatus(names CustomName, format string, w io.Writer, exclude ...string) (err error) {
4✔
201
        values := names.toValues(exclude...)
4✔
202
        return utils.EncodeOutput(format, w, values)
4✔
203
}
4✔
204

205
func PrettyPrint(w io.Writer, exclude ...string) {
2✔
206
        names := CustomName{
2✔
207
                ApiURL:                   "         " + utils.Aqua("API URL"),
2✔
208
                GraphqlURL:               "     " + utils.Aqua("GraphQL URL"),
2✔
209
                StorageS3URL:             "  " + utils.Aqua("S3 Storage URL"),
2✔
210
                DbURL:                    "          " + utils.Aqua("DB URL"),
2✔
211
                StudioURL:                "      " + utils.Aqua("Studio URL"),
2✔
212
                MailpitURL:              "    " + utils.Aqua("Mailpit URL"),
2✔
213
                JWTSecret:                "      " + utils.Aqua("JWT secret"),
2✔
214
                AnonKey:                  "        " + utils.Aqua("anon key"),
2✔
215
                ServiceRoleKey:           "" + utils.Aqua("service_role key"),
2✔
216
                StorageS3AccessKeyId:     "   " + utils.Aqua("S3 Access Key"),
2✔
217
                StorageS3SecretAccessKey: "   " + utils.Aqua("S3 Secret Key"),
2✔
218
                StorageS3Region:          "       " + utils.Aqua("S3 Region"),
2✔
219
        }
2✔
220
        values := names.toValues(exclude...)
2✔
221
        // Iterate through map in order of declared struct fields
2✔
222
        val := reflect.ValueOf(names)
2✔
223
        for i := 0; i < val.NumField(); i++ {
26✔
224
                k := val.Field(i).String()
24✔
225
                if v, ok := values[k]; ok {
26✔
226
                        fmt.Fprintf(w, "%s: %s\n", k, v)
2✔
227
                }
2✔
228
        }
229
}
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