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  }