github.com/Redstoneguy129/cli@v0.0.0-20230211220159-15dca4e91917/internal/status/status.go (about) 1 package status 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "os" 10 "reflect" 11 12 "github.com/BurntSushi/toml" 13 "github.com/Redstoneguy129/cli/internal/utils" 14 "github.com/docker/docker/client" 15 "github.com/joho/godotenv" 16 "github.com/spf13/afero" 17 "gopkg.in/yaml.v3" 18 ) 19 20 const ( 21 OutputEnv = "env" 22 OutputJson = "json" 23 OutputPretty = "pretty" 24 OutputToml = "toml" 25 OutputYaml = "yaml" 26 ) 27 28 type CustomName struct { 29 ApiURL string `env:"api.url,default=API_URL"` 30 DbURL string `env:"db.url,default=DB_URL"` 31 StudioURL string `env:"studio.url,default=STUDIO_URL"` 32 InbucketURL string `env:"inbucket.url,default=INBUCKET_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 } 37 38 func (c *CustomName) toValues(exclude ...string) map[string]string { 39 values := map[string]string{ 40 c.DbURL: fmt.Sprintf("postgresql://postgres:postgres@"+utils.Config.Hostname+":%d/postgres", utils.Config.Db.Port), 41 } 42 if !sliceContains(exclude, utils.RestId) && !sliceContains(exclude, utils.ShortContainerImageName(utils.PostgrestImage)) { 43 values[c.ApiURL] = fmt.Sprintf("http://"+utils.Config.Hostname+":%d", utils.Config.Api.Port) 44 } 45 if !sliceContains(exclude, utils.StudioId) && !sliceContains(exclude, utils.ShortContainerImageName(utils.StudioImage)) { 46 values[c.StudioURL] = fmt.Sprintf("http://"+utils.Config.Hostname+":%d", utils.Config.Studio.Port) 47 } 48 if !sliceContains(exclude, utils.GotrueId) && !sliceContains(exclude, utils.ShortContainerImageName(utils.GotrueImage)) { 49 values[c.JWTSecret] = utils.JWTSecret 50 values[c.AnonKey] = utils.AnonKey 51 values[c.ServiceRoleKey] = utils.ServiceRoleKey 52 } 53 if !sliceContains(exclude, utils.InbucketId) && !sliceContains(exclude, utils.ShortContainerImageName(utils.InbucketImage)) { 54 values[c.InbucketURL] = fmt.Sprintf("http://"+utils.Config.Hostname+":%d", utils.Config.Inbucket.Port) 55 } 56 return values 57 } 58 59 func sliceContains(s []string, e string) bool { 60 for _, a := range s { 61 if a == e { 62 return true 63 } 64 } 65 return false 66 } 67 68 func Run(ctx context.Context, names CustomName, format string, fsys afero.Fs) error { 69 // Sanity checks. 70 { 71 if err := utils.LoadConfigFS(fsys); err != nil { 72 return err 73 } 74 if err := AssertContainerHealthy(ctx, utils.DbId); err != nil { 75 return err 76 } 77 } 78 79 services := []string{ 80 utils.KongId, 81 utils.GotrueId, 82 utils.InbucketId, 83 utils.RealtimeId, 84 utils.RestId, 85 utils.StorageId, 86 utils.ImgProxyId, 87 utils.PgmetaId, 88 utils.StudioId, 89 } 90 stopped := checkServiceHealth(ctx, services, os.Stderr) 91 if len(stopped) > 0 { 92 fmt.Fprintln(os.Stderr, "Stopped services:", stopped) 93 } 94 if format == OutputPretty { 95 fmt.Fprintf(os.Stderr, "%s local development setup is running.\n\n", utils.Aqua("supabase")) 96 PrettyPrint(os.Stdout, stopped...) 97 return nil 98 } 99 return printStatus(names, format, os.Stdout, stopped...) 100 } 101 102 func checkServiceHealth(ctx context.Context, services []string, w io.Writer) (stopped []string) { 103 for _, name := range services { 104 if err := AssertContainerHealthy(ctx, name); err != nil { 105 if client.IsErrNotFound(err) { 106 stopped = append(stopped, name) 107 } else { 108 // Log unhealthy containers instead of failing 109 fmt.Fprintln(w, err) 110 } 111 } 112 } 113 return stopped 114 } 115 116 func AssertContainerHealthy(ctx context.Context, container string) error { 117 if resp, err := utils.Docker.ContainerInspect(ctx, container); err != nil { 118 return err 119 } else if !resp.State.Running { 120 return fmt.Errorf("%s container is not running: %s", container, resp.State.Status) 121 } else if resp.State.Health != nil && resp.State.Health.Status != "healthy" { 122 return fmt.Errorf("%s container is not ready: %s", container, resp.State.Health.Status) 123 } 124 return nil 125 } 126 127 func IsServiceReady(ctx context.Context, container string) bool { 128 if container == utils.RestId { 129 return isPostgRESTHealthy(ctx) 130 } 131 return AssertContainerHealthy(ctx, container) == nil 132 } 133 134 func isPostgRESTHealthy(ctx context.Context) bool { 135 // PostgREST does not support native health checks 136 restUrl := fmt.Sprintf("http://"+utils.Config.Hostname+":%d/rest/v1/", utils.Config.Api.Port) 137 req, err := http.NewRequestWithContext(ctx, http.MethodHead, restUrl, nil) 138 if err != nil { 139 return false 140 } 141 req.Header.Add("apikey", utils.AnonKey) 142 resp, err := http.DefaultClient.Do(req) 143 return err == nil && resp.StatusCode == http.StatusOK 144 } 145 146 func printStatus(names CustomName, format string, w io.Writer, exclude ...string) (err error) { 147 values := names.toValues(exclude...) 148 switch format { 149 case OutputEnv: 150 var out string 151 out, err = godotenv.Marshal(values) 152 fmt.Fprintln(w, out) 153 case OutputJson: 154 enc := json.NewEncoder(w) 155 err = enc.Encode(values) 156 case OutputYaml: 157 enc := yaml.NewEncoder(w) 158 err = enc.Encode(values) 159 case OutputToml: 160 enc := toml.NewEncoder(w) 161 err = enc.Encode(values) 162 } 163 return err 164 } 165 166 func PrettyPrint(w io.Writer, exclude ...string) { 167 names := CustomName{ 168 ApiURL: " " + utils.Aqua("API URL"), 169 DbURL: " " + utils.Aqua("DB URL"), 170 StudioURL: " " + utils.Aqua("Studio URL"), 171 InbucketURL: " " + utils.Aqua("Inbucket URL"), 172 JWTSecret: " " + utils.Aqua("JWT secret"), 173 AnonKey: " " + utils.Aqua("anon key"), 174 ServiceRoleKey: "" + utils.Aqua("service_role key"), 175 } 176 values := names.toValues(exclude...) 177 // Iterate through map in order of declared struct fields 178 val := reflect.ValueOf(names) 179 for i := 0; i < val.NumField(); i++ { 180 k := val.Field(i).String() 181 if v, ok := values[k]; ok { 182 fmt.Fprintf(w, "%s: %s\n", k, v) 183 } 184 } 185 }