269 lines
7.6 KiB
Go
269 lines
7.6 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/getsentry/sentry-go"
|
|
"github.com/go-errors/errors"
|
|
"github.com/spf13/afero"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
"github.com/supabase/cli/internal/utils"
|
|
"github.com/supabase/cli/internal/utils/flags"
|
|
"golang.org/x/mod/semver"
|
|
)
|
|
|
|
const (
|
|
groupQuickStart = "quick-start"
|
|
groupLocalDev = "local-dev"
|
|
groupManagementAPI = "management-api"
|
|
)
|
|
|
|
func IsManagementAPI(cmd *cobra.Command) bool {
|
|
for cmd != cmd.Root() {
|
|
if cmd.GroupID == groupManagementAPI {
|
|
return true
|
|
}
|
|
// Find the last assigned group
|
|
if len(cmd.GroupID) > 0 {
|
|
break
|
|
}
|
|
cmd = cmd.Parent()
|
|
}
|
|
return false
|
|
}
|
|
|
|
func promptLogin(fsys afero.Fs) error {
|
|
if _, err := utils.LoadAccessTokenFS(fsys); err == utils.ErrMissingToken {
|
|
utils.CmdSuggestion = fmt.Sprintf("Run %s first.", utils.Aqua("supabase login"))
|
|
return errors.New("You need to be logged-in in order to use Management API commands.")
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var experimental = []*cobra.Command{
|
|
bansCmd,
|
|
restrictionsCmd,
|
|
vanityCmd,
|
|
sslEnforcementCmd,
|
|
genKeysCmd,
|
|
postgresCmd,
|
|
branchesCmd,
|
|
storageCmd,
|
|
}
|
|
|
|
func IsExperimental(cmd *cobra.Command) bool {
|
|
for _, exp := range experimental {
|
|
if cmd == exp || cmd.Parent() == exp {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
var (
|
|
sentryOpts = sentry.ClientOptions{
|
|
Dsn: utils.SentryDsn,
|
|
Release: utils.Version,
|
|
ServerName: "<redacted>",
|
|
// Set TracesSampleRate to 1.0 to capture 100%
|
|
// of transactions for performance monitoring.
|
|
// We recommend adjusting this value in production,
|
|
TracesSampleRate: 1.0,
|
|
}
|
|
|
|
createTicket bool
|
|
|
|
rootCmd = &cobra.Command{
|
|
Use: "supabase",
|
|
Short: "Supabase CLI " + utils.Version,
|
|
Version: utils.Version,
|
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
|
if IsExperimental(cmd) && !viper.GetBool("EXPERIMENTAL") {
|
|
return errors.New("must set the --experimental flag to run this command")
|
|
}
|
|
cmd.SilenceUsage = true
|
|
// Change workdir
|
|
fsys := afero.NewOsFs()
|
|
if err := utils.ChangeWorkDir(fsys); err != nil {
|
|
return err
|
|
}
|
|
// Add common flags
|
|
ctx := cmd.Context()
|
|
if IsManagementAPI(cmd) {
|
|
if err := promptLogin(fsys); err != nil {
|
|
return err
|
|
}
|
|
ctx, _ = signal.NotifyContext(ctx, os.Interrupt)
|
|
if cmd.Flags().Lookup("project-ref") != nil {
|
|
if err := flags.ParseProjectRef(ctx, fsys); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if err := flags.ParseDatabaseConfig(cmd.Flags(), fsys); err != nil {
|
|
return err
|
|
}
|
|
// Prepare context
|
|
if viper.GetBool("DEBUG") {
|
|
ctx = utils.WithTraceContext(ctx)
|
|
fmt.Fprintln(os.Stderr, cmd.Root().Short)
|
|
}
|
|
cmd.SetContext(ctx)
|
|
// Setup sentry last to ignore errors from parsing cli flags
|
|
apiHost, err := url.Parse(utils.GetSupabaseAPIHost())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sentryOpts.Environment = apiHost.Host
|
|
return sentry.Init(sentryOpts)
|
|
},
|
|
SilenceErrors: true,
|
|
}
|
|
)
|
|
|
|
func Execute() {
|
|
defer recoverAndExit()
|
|
if err := rootCmd.Execute(); err != nil {
|
|
panic(err)
|
|
}
|
|
// Check upgrade last because --version flag is initialised after execute
|
|
version, err := checkUpgrade(rootCmd.Context(), afero.NewOsFs())
|
|
if err != nil {
|
|
fmt.Fprintln(utils.GetDebugLogger(), err)
|
|
}
|
|
if semver.Compare(version, "v"+utils.Version) > 0 {
|
|
fmt.Fprintln(os.Stderr, suggestUpgrade(version))
|
|
}
|
|
if len(utils.CmdSuggestion) > 0 {
|
|
fmt.Fprintln(os.Stderr, utils.CmdSuggestion)
|
|
}
|
|
}
|
|
|
|
func checkUpgrade(ctx context.Context, fsys afero.Fs) (string, error) {
|
|
if shouldFetchRelease(fsys) {
|
|
version, err := utils.GetLatestRelease(ctx)
|
|
if exists, _ := afero.DirExists(fsys, utils.SupabaseDirPath); exists {
|
|
// If user is offline, write an empty file to skip subsequent checks
|
|
err = utils.WriteFile(utils.CliVersionPath, []byte(version), fsys)
|
|
}
|
|
return version, err
|
|
}
|
|
version, err := afero.ReadFile(fsys, utils.CliVersionPath)
|
|
if err != nil {
|
|
return "", errors.Errorf("failed to read cli version: %w", err)
|
|
}
|
|
return string(version), nil
|
|
}
|
|
|
|
func shouldFetchRelease(fsys afero.Fs) bool {
|
|
// Always fetch latest release when using --version flag
|
|
if vf := rootCmd.Flag("version"); vf != nil && vf.Changed {
|
|
return true
|
|
}
|
|
if fi, err := fsys.Stat(utils.CliVersionPath); err == nil {
|
|
expiry := fi.ModTime().Add(time.Hour * 10)
|
|
// Skip if last checked is less than 10 hours ago
|
|
return time.Now().After(expiry)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func suggestUpgrade(version string) string {
|
|
const guide = "https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli"
|
|
return fmt.Sprintf(`A new version of Supabase CLI is available: %s (currently installed v%s)
|
|
We recommend updating regularly for new features and bug fixes: %s`, utils.Yellow(version), utils.Version, utils.Bold(guide))
|
|
}
|
|
|
|
func recoverAndExit() {
|
|
err := recover()
|
|
if err == nil {
|
|
return
|
|
}
|
|
var msg string
|
|
switch err := err.(type) {
|
|
case string:
|
|
msg = err
|
|
case error:
|
|
if !errors.Is(err, context.Canceled) &&
|
|
len(utils.CmdSuggestion) == 0 &&
|
|
!viper.GetBool("DEBUG") {
|
|
utils.CmdSuggestion = utils.SuggestDebugFlag
|
|
}
|
|
msg = err.Error()
|
|
default:
|
|
msg = fmt.Sprintf("%#v", err)
|
|
}
|
|
// Log error to console
|
|
fmt.Fprintln(os.Stderr, utils.Red(msg))
|
|
if len(utils.CmdSuggestion) > 0 {
|
|
fmt.Fprintln(os.Stderr, utils.CmdSuggestion)
|
|
}
|
|
// Report error to sentry
|
|
if createTicket && len(utils.SentryDsn) > 0 {
|
|
sentry.ConfigureScope(addSentryScope)
|
|
eventId := sentry.CurrentHub().Recover(err)
|
|
if eventId != nil && sentry.Flush(2*time.Second) {
|
|
fmt.Fprintln(os.Stderr, "Sent crash report:", *eventId)
|
|
fmt.Fprintln(os.Stderr, "Quote the crash ID above when filing a bug report: https://github.com/supabase/cli/issues/new/choose")
|
|
}
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
|
|
func init() {
|
|
cobra.OnInitialize(func() {
|
|
viper.SetEnvPrefix("SUPABASE")
|
|
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
|
viper.AutomaticEnv()
|
|
})
|
|
|
|
flags := rootCmd.PersistentFlags()
|
|
flags.Bool("debug", false, "output debug logs to stderr")
|
|
flags.String("workdir", "", "path to a Supabase project directory")
|
|
flags.Bool("experimental", false, "enable experimental features")
|
|
flags.String("network-id", "", "use the specified docker network instead of a generated one")
|
|
flags.Var(&utils.OutputFormat, "output", "output format of status variables")
|
|
flags.Var(&utils.DNSResolver, "dns-resolver", "lookup domain names using the specified resolver")
|
|
flags.BoolVar(&createTicket, "create-ticket", false, "create a support ticket for any CLI error")
|
|
cobra.CheckErr(viper.BindPFlags(flags))
|
|
|
|
rootCmd.SetVersionTemplate("{{.Version}}\n")
|
|
rootCmd.AddGroup(&cobra.Group{ID: groupQuickStart, Title: "Quick Start:"})
|
|
rootCmd.AddGroup(&cobra.Group{ID: groupLocalDev, Title: "Local Development:"})
|
|
rootCmd.AddGroup(&cobra.Group{ID: groupManagementAPI, Title: "Management APIs:"})
|
|
}
|
|
|
|
// instantiate new rootCmd is a bit tricky with cobra, but it can be done later with the following
|
|
// approach for example: https://github.com/portworx/pxc/tree/master/cmd
|
|
func GetRootCmd() *cobra.Command {
|
|
return rootCmd
|
|
}
|
|
|
|
func addSentryScope(scope *sentry.Scope) {
|
|
serviceImages := utils.Config.GetServiceImages()
|
|
imageToVersion := make(map[string]interface{}, len(serviceImages))
|
|
for _, image := range serviceImages {
|
|
parts := strings.Split(image, ":")
|
|
// Bypasses sentry's IP sanitization rule, ie. 15.1.0.147
|
|
if net.ParseIP(parts[1]) != nil {
|
|
imageToVersion[parts[0]] = "v" + parts[1]
|
|
} else {
|
|
imageToVersion[parts[0]] = parts[1]
|
|
}
|
|
}
|
|
scope.SetContext("Services", imageToVersion)
|
|
scope.SetContext("Config", map[string]interface{}{
|
|
"Image Registry": utils.GetRegistry(),
|
|
"Project ID": flags.ProjectRef,
|
|
})
|
|
}
|