github.com/supabase/cli@v1.168.1/internal/status/status.go (about) 1 package status 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "net/http" 8 "net/url" 9 "os" 10 "reflect" 11 12 "github.com/docker/docker/client" 13 "github.com/go-errors/errors" 14 "github.com/spf13/afero" 15 "github.com/supabase/cli/internal/utils" 16 ) 17 18 type CustomName struct { 19 ApiURL string `env:"api.url,default=API_URL"` 20 GraphqlURL string `env:"api.graphql_url,default=GRAPHQL_URL"` 21 StorageS3URL string `env:"api.storage_s3_url,default=STORAGE_S3_URL"` 22 DbURL string `env:"db.url,default=DB_URL"` 23 StudioURL string `env:"studio.url,default=STUDIO_URL"` 24 InbucketURL string `env:"inbucket.url,default=INBUCKET_URL"` 25 JWTSecret string `env:"auth.jwt_secret,default=JWT_SECRET"` 26 AnonKey string `env:"auth.anon_key,default=ANON_KEY"` 27 ServiceRoleKey string `env:"auth.service_role_key,default=SERVICE_ROLE_KEY"` 28 StorageS3AccessKeyId string `env:"storage.s3_access_key_id,default=S3_PROTOCOL_ACCESS_KEY_ID"` 29 StorageS3SecretAccessKey string `env:"storage.s3_secret_access_key,default=S3_PROTOCOL_ACCESS_KEY_SECRET"` 30 StorageS3Region string `env:"storage.s3_region,default=S3_PROTOCOL_REGION"` 31 } 32 33 func (c *CustomName) toValues(exclude ...string) map[string]string { 34 values := map[string]string{ 35 c.DbURL: fmt.Sprintf("postgresql://%s@%s:%d/postgres", url.UserPassword("postgres", utils.Config.Db.Password), utils.Config.Hostname, utils.Config.Db.Port), 36 } 37 if utils.Config.Api.Enabled && !utils.SliceContains(exclude, utils.RestId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Api.Image)) { 38 values[c.ApiURL] = fmt.Sprintf("http://%s:%d", utils.Config.Hostname, utils.Config.Api.Port) 39 values[c.GraphqlURL] = fmt.Sprintf("http://%s:%d/graphql/v1", utils.Config.Hostname, utils.Config.Api.Port) 40 } 41 if utils.Config.Studio.Enabled && !utils.SliceContains(exclude, utils.StudioId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.StudioImage)) { 42 values[c.StudioURL] = fmt.Sprintf("http://%s:%d", utils.Config.Hostname, utils.Config.Studio.Port) 43 } 44 if utils.Config.Auth.Enabled && !utils.SliceContains(exclude, utils.GotrueId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Auth.Image)) { 45 values[c.JWTSecret] = utils.Config.Auth.JwtSecret 46 values[c.AnonKey] = utils.Config.Auth.AnonKey 47 values[c.ServiceRoleKey] = utils.Config.Auth.ServiceRoleKey 48 } 49 if utils.Config.Inbucket.Enabled && !utils.SliceContains(exclude, utils.InbucketId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.InbucketImage)) { 50 values[c.InbucketURL] = fmt.Sprintf("http://%s:%d", utils.Config.Hostname, utils.Config.Inbucket.Port) 51 } 52 if utils.Config.Storage.Enabled && !utils.SliceContains(exclude, utils.StorageId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Storage.Image)) { 53 values[c.StorageS3URL] = fmt.Sprintf("http://%s:%d/storage/v1/s3", utils.Config.Hostname, utils.Config.Api.Port) 54 values[c.StorageS3AccessKeyId] = utils.Config.Storage.S3Credentials.AccessKeyId 55 values[c.StorageS3SecretAccessKey] = utils.Config.Storage.S3Credentials.SecretAccessKey 56 values[c.StorageS3Region] = utils.Config.Storage.S3Credentials.Region 57 } 58 return values 59 } 60 61 func Run(ctx context.Context, names CustomName, format string, fsys afero.Fs) error { 62 // Sanity checks. 63 { 64 if err := utils.LoadConfigFS(fsys); err != nil { 65 return err 66 } 67 if err := AssertContainerHealthy(ctx, utils.DbId); err != nil { 68 return err 69 } 70 } 71 72 services := []string{ 73 utils.KongId, 74 utils.GotrueId, 75 utils.InbucketId, 76 utils.RealtimeId, 77 utils.RestId, 78 utils.StorageId, 79 utils.ImgProxyId, 80 utils.PgmetaId, 81 utils.StudioId, 82 utils.LogflareId, 83 } 84 stopped := checkServiceHealth(ctx, services, os.Stderr) 85 if len(stopped) > 0 { 86 fmt.Fprintln(os.Stderr, "Stopped services:", stopped) 87 } 88 if format == utils.OutputPretty { 89 fmt.Fprintf(os.Stderr, "%s local development setup is running.\n\n", utils.Aqua("supabase")) 90 PrettyPrint(os.Stdout, stopped...) 91 return nil 92 } 93 return printStatus(names, format, os.Stdout, stopped...) 94 } 95 96 func checkServiceHealth(ctx context.Context, services []string, w io.Writer) (stopped []string) { 97 for _, name := range services { 98 if err := AssertContainerHealthy(ctx, name); err != nil { 99 if client.IsErrNotFound(err) { 100 stopped = append(stopped, name) 101 } else { 102 // Log unhealthy containers instead of failing 103 fmt.Fprintln(w, err) 104 } 105 } 106 } 107 return stopped 108 } 109 110 func AssertContainerHealthy(ctx context.Context, container string) error { 111 if resp, err := utils.Docker.ContainerInspect(ctx, container); err != nil { 112 return err 113 } else if !resp.State.Running { 114 return errors.Errorf("%s container is not running: %s", container, resp.State.Status) 115 } else if resp.State.Health != nil && resp.State.Health.Status != "healthy" { 116 return errors.Errorf("%s container is not ready: %s", container, resp.State.Health.Status) 117 } 118 return nil 119 } 120 121 func IsServiceReady(ctx context.Context, container string) bool { 122 if container == utils.RestId { 123 return isPostgRESTHealthy(ctx) 124 } 125 if container == utils.EdgeRuntimeId { 126 return isEdgeRuntimeHealthy(ctx) 127 } 128 return AssertContainerHealthy(ctx, container) == nil 129 } 130 131 func isPostgRESTHealthy(ctx context.Context) bool { 132 // PostgREST does not support native health checks 133 restUrl := fmt.Sprintf("http://%s:%d/rest-admin/v1/ready", utils.Config.Hostname, utils.Config.Api.Port) 134 return checkHTTPHead(ctx, restUrl) 135 } 136 137 func isEdgeRuntimeHealthy(ctx context.Context) bool { 138 // Native health check logs too much hyper::Error(IncompleteMessage) 139 restUrl := fmt.Sprintf("http://%s:%d/functions/v1/_internal/health", utils.Config.Hostname, utils.Config.Api.Port) 140 return checkHTTPHead(ctx, restUrl) 141 } 142 143 func checkHTTPHead(ctx context.Context, url string) bool { 144 req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) 145 if err != nil { 146 return false 147 } 148 req.Header.Add("apikey", utils.Config.Auth.AnonKey) 149 resp, err := http.DefaultClient.Do(req) 150 return err == nil && resp.StatusCode == http.StatusOK 151 } 152 153 func printStatus(names CustomName, format string, w io.Writer, exclude ...string) (err error) { 154 values := names.toValues(exclude...) 155 return utils.EncodeOutput(format, w, values) 156 } 157 158 func PrettyPrint(w io.Writer, exclude ...string) { 159 names := CustomName{ 160 ApiURL: " " + utils.Aqua("API URL"), 161 GraphqlURL: " " + utils.Aqua("GraphQL URL"), 162 StorageS3URL: " " + utils.Aqua("S3 Storage URL"), 163 DbURL: " " + utils.Aqua("DB URL"), 164 StudioURL: " " + utils.Aqua("Studio URL"), 165 InbucketURL: " " + utils.Aqua("Inbucket URL"), 166 JWTSecret: " " + utils.Aqua("JWT secret"), 167 AnonKey: " " + utils.Aqua("anon key"), 168 ServiceRoleKey: "" + utils.Aqua("service_role key"), 169 StorageS3AccessKeyId: " " + utils.Aqua("S3 Access Key"), 170 StorageS3SecretAccessKey: " " + utils.Aqua("S3 Secret Key"), 171 StorageS3Region: " " + utils.Aqua("S3 Region"), 172 } 173 values := names.toValues(exclude...) 174 // Iterate through map in order of declared struct fields 175 val := reflect.ValueOf(names) 176 for i := 0; i < val.NumField(); i++ { 177 k := val.Field(i).String() 178 if v, ok := values[k]; ok { 179 fmt.Fprintf(w, "%s: %s\n", k, v) 180 } 181 } 182 }