package status import ( "context" "crypto/tls" "crypto/x509" _ "embed" "fmt" "io" "net/http" "net/url" "os" "reflect" "sync" "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/go-errors/errors" "github.com/spf13/afero" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" "github.com/supabase/cli/pkg/fetcher" ) type CustomName struct { ApiURL string `env:"api.url,default=API_URL"` GraphqlURL string `env:"api.graphql_url,default=GRAPHQL_URL"` StorageS3URL string `env:"api.storage_s3_url,default=STORAGE_S3_URL"` DbURL string `env:"db.url,default=DB_URL"` StudioURL string `env:"studio.url,default=STUDIO_URL"` InbucketURL string `env:"inbucket.url,default=INBUCKET_URL"` JWTSecret string `env:"auth.jwt_secret,default=JWT_SECRET"` AnonKey string `env:"auth.anon_key,default=ANON_KEY"` ServiceRoleKey string `env:"auth.service_role_key,default=SERVICE_ROLE_KEY"` StorageS3AccessKeyId string `env:"storage.s3_access_key_id,default=S3_PROTOCOL_ACCESS_KEY_ID"` StorageS3SecretAccessKey string `env:"storage.s3_secret_access_key,default=S3_PROTOCOL_ACCESS_KEY_SECRET"` StorageS3Region string `env:"storage.s3_region,default=S3_PROTOCOL_REGION"` } func (c *CustomName) toValues(exclude ...string) map[string]string { values := map[string]string{ c.DbURL: fmt.Sprintf("postgresql://%s@%s:%d/postgres", url.UserPassword("postgres", utils.Config.Db.Password), utils.Config.Hostname, utils.Config.Db.Port), } if utils.Config.Api.Enabled && !utils.SliceContains(exclude, utils.RestId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Api.Image)) { values[c.ApiURL] = utils.Config.Api.ExternalUrl values[c.GraphqlURL] = utils.GetApiUrl("/graphql/v1") } if utils.Config.Studio.Enabled && !utils.SliceContains(exclude, utils.StudioId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Studio.Image)) { values[c.StudioURL] = fmt.Sprintf("http://%s:%d", utils.Config.Hostname, utils.Config.Studio.Port) } if utils.Config.Auth.Enabled && !utils.SliceContains(exclude, utils.GotrueId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Auth.Image)) { values[c.JWTSecret] = utils.Config.Auth.JwtSecret values[c.AnonKey] = utils.Config.Auth.AnonKey values[c.ServiceRoleKey] = utils.Config.Auth.ServiceRoleKey } if utils.Config.Inbucket.Enabled && !utils.SliceContains(exclude, utils.InbucketId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Inbucket.Image)) { values[c.InbucketURL] = fmt.Sprintf("http://%s:%d", utils.Config.Hostname, utils.Config.Inbucket.Port) } if utils.Config.Storage.Enabled && !utils.SliceContains(exclude, utils.StorageId) && !utils.SliceContains(exclude, utils.ShortContainerImageName(utils.Config.Storage.Image)) { values[c.StorageS3URL] = utils.GetApiUrl("/storage/v1/s3") values[c.StorageS3AccessKeyId] = utils.Config.Storage.S3Credentials.AccessKeyId values[c.StorageS3SecretAccessKey] = utils.Config.Storage.S3Credentials.SecretAccessKey values[c.StorageS3Region] = utils.Config.Storage.S3Credentials.Region } return values } func Run(ctx context.Context, names CustomName, format string, fsys afero.Fs) error { // Sanity checks. if err := flags.LoadConfig(fsys); err != nil { return err } if err := assertContainerHealthy(ctx, utils.DbId); err != nil { return err } stopped, err := checkServiceHealth(ctx) if err != nil { return err } if len(stopped) > 0 { fmt.Fprintln(os.Stderr, "Stopped services:", stopped) } if format == utils.OutputPretty { fmt.Fprintf(os.Stderr, "%s local development setup is running.\n\n", utils.Aqua("supabase")) PrettyPrint(os.Stdout, stopped...) return nil } return printStatus(names, format, os.Stdout, stopped...) } func checkServiceHealth(ctx context.Context) ([]string, error) { resp, err := utils.Docker.ContainerList(ctx, container.ListOptions{ Filters: utils.CliProjectFilter(utils.Config.ProjectId), }) if err != nil { return nil, errors.Errorf("failed to list running containers: %w", err) } running := make(map[string]struct{}, len(resp)) for _, c := range resp { for _, n := range c.Names { running[n] = struct{}{} } } var stopped []string for _, containerId := range utils.GetDockerIds() { if _, ok := running["/"+containerId]; !ok { stopped = append(stopped, containerId) } } return stopped, nil } func assertContainerHealthy(ctx context.Context, container string) error { if resp, err := utils.Docker.ContainerInspect(ctx, container); err != nil { return errors.Errorf("failed to inspect container health: %w", err) } else if !resp.State.Running { return errors.Errorf("%s container is not running: %s", container, resp.State.Status) } else if resp.State.Health != nil && resp.State.Health.Status != types.Healthy { return errors.Errorf("%s container is not ready: %s", container, resp.State.Health.Status) } return nil } func IsServiceReady(ctx context.Context, container string) error { if container == utils.RestId { // PostgREST does not support native health checks return checkHTTPHead(ctx, "/rest-admin/v1/ready") } if container == utils.EdgeRuntimeId { // Native health check logs too much hyper::Error(IncompleteMessage) return checkHTTPHead(ctx, "/functions/v1/_internal/health") } return assertContainerHealthy(ctx, container) } var ( //go:embed kong.local.crt KongCert string //go:embed kong.local.key KongKey string ) // To regenerate local certificate pair: // // openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 \ // -nodes -keyout kong.local.key -out kong.local.crt -subj "/CN=localhost" \ // -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" func NewKongClient() *http.Client { client := &http.Client{ Timeout: 10 * time.Second, } if t, ok := http.DefaultTransport.(*http.Transport); ok { pool, err := x509.SystemCertPool() if err != nil { fmt.Fprintln(utils.GetDebugLogger(), err) pool = x509.NewCertPool() } // No need to replace TLS config if we fail to append cert if pool.AppendCertsFromPEM([]byte(KongCert)) { rt := t.Clone() rt.TLSClientConfig = &tls.Config{ MinVersion: tls.VersionTLS12, RootCAs: pool, } client.Transport = rt } } return client } var ( healthClient *fetcher.Fetcher healthOnce sync.Once ) func checkHTTPHead(ctx context.Context, path string) error { healthOnce.Do(func() { server := utils.Config.Api.ExternalUrl header := func(req *http.Request) { req.Header.Add("apikey", utils.Config.Auth.AnonKey) } client := NewKongClient() healthClient = fetcher.NewFetcher( server, fetcher.WithHTTPClient(client), fetcher.WithRequestEditor(header), fetcher.WithExpectedStatus(http.StatusOK), ) }) // HEAD method does not return response body resp, err := healthClient.Send(ctx, http.MethodHead, path, nil) if err != nil { return err } defer resp.Body.Close() return nil } func printStatus(names CustomName, format string, w io.Writer, exclude ...string) (err error) { values := names.toValues(exclude...) return utils.EncodeOutput(format, w, values) } func PrettyPrint(w io.Writer, exclude ...string) { names := CustomName{ ApiURL: " " + utils.Aqua("API URL"), GraphqlURL: " " + utils.Aqua("GraphQL URL"), StorageS3URL: " " + utils.Aqua("S3 Storage URL"), DbURL: " " + utils.Aqua("DB URL"), StudioURL: " " + utils.Aqua("Studio URL"), InbucketURL: " " + utils.Aqua("Inbucket URL"), JWTSecret: " " + utils.Aqua("JWT secret"), AnonKey: " " + utils.Aqua("anon key"), ServiceRoleKey: "" + utils.Aqua("service_role key"), StorageS3AccessKeyId: " " + utils.Aqua("S3 Access Key"), StorageS3SecretAccessKey: " " + utils.Aqua("S3 Secret Key"), StorageS3Region: " " + utils.Aqua("S3 Region"), } values := names.toValues(exclude...) // Iterate through map in order of declared struct fields val := reflect.ValueOf(names) for i := 0; i < val.NumField(); i++ { k := val.Field(i).String() if v, ok := values[k]; ok { fmt.Fprintf(w, "%s: %s\n", k, v) } } }