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  }