From dd84b9d64fb98746a230cd24233ff50a562c39c9 Mon Sep 17 00:00:00 2001 From: 简律纯 Date: Fri, 28 Apr 2023 01:36:44 +0800 Subject: --- cli/internal/cmdutil/cmdutil.go | 245 +++++++++++++++++++++++++++++++++++ cli/internal/cmdutil/cmdutil_test.go | 109 ++++++++++++++++ 2 files changed, 354 insertions(+) create mode 100644 cli/internal/cmdutil/cmdutil.go create mode 100644 cli/internal/cmdutil/cmdutil_test.go (limited to 'cli/internal/cmdutil') diff --git a/cli/internal/cmdutil/cmdutil.go b/cli/internal/cmdutil/cmdutil.go new file mode 100644 index 0000000..0b02392 --- /dev/null +++ b/cli/internal/cmdutil/cmdutil.go @@ -0,0 +1,245 @@ +// Package cmdutil holds functionality to run turbo via cobra. That includes flag parsing and configuration +// of components common to all subcommands +package cmdutil + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "strconv" + "sync" + + "github.com/hashicorp/go-hclog" + + "github.com/fatih/color" + "github.com/mitchellh/cli" + "github.com/vercel/turbo/cli/internal/client" + "github.com/vercel/turbo/cli/internal/config" + "github.com/vercel/turbo/cli/internal/fs" + "github.com/vercel/turbo/cli/internal/turbopath" + "github.com/vercel/turbo/cli/internal/turbostate" + "github.com/vercel/turbo/cli/internal/ui" +) + +const ( + // _envLogLevel is the environment log level + _envLogLevel = "TURBO_LOG_LEVEL" +) + +// Helper is a struct used to hold configuration values passed via flag, env vars, +// config files, etc. It is not intended for direct use by turbo commands, it drives +// the creation of CmdBase, which is then used by the commands themselves. +type Helper struct { + // TurboVersion is the version of turbo that is currently executing + TurboVersion string + + // for logging + verbosity int + + rawRepoRoot string + + clientOpts client.Opts + + // UserConfigPath is the path to where we expect to find + // a user-specific config file, if one is present. Public + // to allow overrides in tests + UserConfigPath turbopath.AbsoluteSystemPath + + cleanupsMu sync.Mutex + cleanups []io.Closer +} + +// RegisterCleanup saves a function to be run after turbo execution, +// even if the command that runs returns an error +func (h *Helper) RegisterCleanup(cleanup io.Closer) { + h.cleanupsMu.Lock() + defer h.cleanupsMu.Unlock() + h.cleanups = append(h.cleanups, cleanup) +} + +// Cleanup runs the register cleanup handlers. It requires the flags +// to the root command so that it can construct a UI if necessary +func (h *Helper) Cleanup(cliConfig *turbostate.ParsedArgsFromRust) { + h.cleanupsMu.Lock() + defer h.cleanupsMu.Unlock() + var ui cli.Ui + for _, cleanup := range h.cleanups { + if err := cleanup.Close(); err != nil { + if ui == nil { + ui = h.getUI(cliConfig) + } + ui.Warn(fmt.Sprintf("failed cleanup: %v", err)) + } + } +} + +func (h *Helper) getUI(cliConfig *turbostate.ParsedArgsFromRust) cli.Ui { + colorMode := ui.GetColorModeFromEnv() + if cliConfig.GetNoColor() { + colorMode = ui.ColorModeSuppressed + } + if cliConfig.GetColor() { + colorMode = ui.ColorModeForced + } + return ui.BuildColoredUi(colorMode) +} + +func (h *Helper) getLogger() (hclog.Logger, error) { + var level hclog.Level + switch h.verbosity { + case 0: + if v := os.Getenv(_envLogLevel); v != "" { + level = hclog.LevelFromString(v) + if level == hclog.NoLevel { + return nil, fmt.Errorf("%s value %q is not a valid log level", _envLogLevel, v) + } + } else { + level = hclog.NoLevel + } + case 1: + level = hclog.Info + case 2: + level = hclog.Debug + case 3: + level = hclog.Trace + default: + level = hclog.Trace + } + // Default output is nowhere unless we enable logging. + output := ioutil.Discard + color := hclog.ColorOff + if level != hclog.NoLevel { + output = os.Stderr + color = hclog.AutoColor + } + + return hclog.New(&hclog.LoggerOptions{ + Name: "turbo", + Level: level, + Color: color, + Output: output, + }), nil +} + +// NewHelper returns a new helper instance to hold configuration values for the root +// turbo command. +func NewHelper(turboVersion string, args *turbostate.ParsedArgsFromRust) *Helper { + return &Helper{ + TurboVersion: turboVersion, + UserConfigPath: config.DefaultUserConfigPath(), + verbosity: args.Verbosity, + } +} + +// GetCmdBase returns a CmdBase instance configured with values from this helper. +// It additionally returns a mechanism to set an error, so +func (h *Helper) GetCmdBase(cliConfig *turbostate.ParsedArgsFromRust) (*CmdBase, error) { + // terminal is for color/no-color output + terminal := h.getUI(cliConfig) + // logger is configured with verbosity level using --verbosity flag from end users + logger, err := h.getLogger() + if err != nil { + return nil, err + } + cwdRaw, err := cliConfig.GetCwd() + if err != nil { + return nil, err + } + cwd, err := fs.GetCwd(cwdRaw) + if err != nil { + return nil, err + } + repoRoot := fs.ResolveUnknownPath(cwd, h.rawRepoRoot) + repoRoot, err = repoRoot.EvalSymlinks() + if err != nil { + return nil, err + } + repoConfig, err := config.ReadRepoConfigFile(config.GetRepoConfigPath(repoRoot), cliConfig) + if err != nil { + return nil, err + } + userConfig, err := config.ReadUserConfigFile(h.UserConfigPath, cliConfig) + if err != nil { + return nil, err + } + remoteConfig := repoConfig.GetRemoteConfig(userConfig.Token()) + if remoteConfig.Token == "" && ui.IsCI { + vercelArtifactsToken := os.Getenv("VERCEL_ARTIFACTS_TOKEN") + vercelArtifactsOwner := os.Getenv("VERCEL_ARTIFACTS_OWNER") + if vercelArtifactsToken != "" { + remoteConfig.Token = vercelArtifactsToken + } + if vercelArtifactsOwner != "" { + remoteConfig.TeamID = vercelArtifactsOwner + } + } + + // Primacy: Arg > Env + timeout, err := cliConfig.GetRemoteCacheTimeout() + if err == nil { + h.clientOpts.Timeout = timeout + } else { + val, ok := os.LookupEnv("TURBO_REMOTE_CACHE_TIMEOUT") + if ok { + number, err := strconv.ParseUint(val, 10, 64) + if err == nil { + h.clientOpts.Timeout = number + } + } + } + + apiClient := client.NewClient( + remoteConfig, + logger, + h.TurboVersion, + h.clientOpts, + ) + + return &CmdBase{ + UI: terminal, + Logger: logger, + RepoRoot: repoRoot, + APIClient: apiClient, + RepoConfig: repoConfig, + UserConfig: userConfig, + RemoteConfig: remoteConfig, + TurboVersion: h.TurboVersion, + }, nil +} + +// CmdBase encompasses configured components common to all turbo commands. +type CmdBase struct { + UI cli.Ui + Logger hclog.Logger + RepoRoot turbopath.AbsoluteSystemPath + APIClient *client.APIClient + RepoConfig *config.RepoConfig + UserConfig *config.UserConfig + RemoteConfig client.RemoteConfig + TurboVersion string +} + +// LogError prints an error to the UI +func (b *CmdBase) LogError(format string, args ...interface{}) { + err := fmt.Errorf(format, args...) + b.Logger.Error("error", err) + b.UI.Error(fmt.Sprintf("%s%s", ui.ERROR_PREFIX, color.RedString(" %v", err))) +} + +// LogWarning logs an error and outputs it to the UI. +func (b *CmdBase) LogWarning(prefix string, err error) { + b.Logger.Warn(prefix, "warning", err) + + if prefix != "" { + prefix = " " + prefix + ": " + } + + b.UI.Warn(fmt.Sprintf("%s%s%s", ui.WARNING_PREFIX, prefix, color.YellowString(" %v", err))) +} + +// LogInfo logs an message and outputs it to the UI. +func (b *CmdBase) LogInfo(msg string) { + b.Logger.Info(msg) + b.UI.Info(fmt.Sprintf("%s%s", ui.InfoPrefix, color.WhiteString(" %v", msg))) +} diff --git a/cli/internal/cmdutil/cmdutil_test.go b/cli/internal/cmdutil/cmdutil_test.go new file mode 100644 index 0000000..4e6cf70 --- /dev/null +++ b/cli/internal/cmdutil/cmdutil_test.go @@ -0,0 +1,109 @@ +package cmdutil + +import ( + "os" + "testing" + "time" + + "github.com/vercel/turbo/cli/internal/fs" + "github.com/vercel/turbo/cli/internal/turbostate" + "gotest.tools/v3/assert" +) + +func TestTokenEnvVar(t *testing.T) { + // Set up an empty config so we're just testing environment variables + userConfigPath := fs.AbsoluteSystemPathFromUpstream(t.TempDir()).UntypedJoin("turborepo", "config.json") + expectedPrefix := "my-token" + vars := []string{"TURBO_TOKEN", "VERCEL_ARTIFACTS_TOKEN"} + for _, v := range vars { + t.Run(v, func(t *testing.T) { + t.Cleanup(func() { + _ = os.Unsetenv(v) + }) + args := &turbostate.ParsedArgsFromRust{ + CWD: "", + } + h := NewHelper("test-version", args) + h.UserConfigPath = userConfigPath + + expectedToken := expectedPrefix + v + err := os.Setenv(v, expectedToken) + if err != nil { + t.Fatalf("setenv %v", err) + } + + base, err := h.GetCmdBase(args) + if err != nil { + t.Fatalf("failed to get command base %v", err) + } + assert.Equal(t, base.RemoteConfig.Token, expectedToken) + }) + } +} + +func TestRemoteCacheTimeoutEnvVar(t *testing.T) { + key := "TURBO_REMOTE_CACHE_TIMEOUT" + expectedTimeout := "600" + t.Run(key, func(t *testing.T) { + t.Cleanup(func() { + _ = os.Unsetenv(key) + }) + args := &turbostate.ParsedArgsFromRust{ + CWD: "", + } + h := NewHelper("test-version", args) + + err := os.Setenv(key, expectedTimeout) + if err != nil { + t.Fatalf("setenv %v", err) + } + + base, err := h.GetCmdBase(args) + if err != nil { + t.Fatalf("failed to get command base %v", err) + } + assert.Equal(t, base.APIClient.HTTPClient.HTTPClient.Timeout, time.Duration(600)*time.Second) + }) +} + +func TestRemoteCacheTimeoutFlag(t *testing.T) { + args := &turbostate.ParsedArgsFromRust{ + CWD: "", + RemoteCacheTimeout: 599, + } + h := NewHelper("test-version", args) + + base, err := h.GetCmdBase(args) + if err != nil { + t.Fatalf("failed to get command base %v", err) + } + + assert.Equal(t, base.APIClient.HTTPClient.HTTPClient.Timeout, time.Duration(599)*time.Second) +} + +func TestRemoteCacheTimeoutPrimacy(t *testing.T) { + key := "TURBO_REMOTE_CACHE_TIMEOUT" + value := "2" + + t.Run(key, func(t *testing.T) { + t.Cleanup(func() { + _ = os.Unsetenv(key) + }) + args := &turbostate.ParsedArgsFromRust{ + CWD: "", + RemoteCacheTimeout: 1, + } + h := NewHelper("test-version", args) + + err := os.Setenv(key, value) + if err != nil { + t.Fatalf("setenv %v", err) + } + + base, err := h.GetCmdBase(args) + if err != nil { + t.Fatalf("failed to get command base %v", err) + } + assert.Equal(t, base.APIClient.HTTPClient.HTTPClient.Timeout, time.Duration(1)*time.Second) + }) +} -- cgit v1.2.3-70-g09d2