diff --git a/cli/cmd/turbo/main.go b/cli/cmd/turbo/main.go index d4155f5e0ffcb..326cb94888cba 100644 --- a/cli/cmd/turbo/main.go +++ b/cli/cmd/turbo/main.go @@ -15,14 +15,14 @@ func main() { os.Exit(1) } - argsString := os.Args[1] - var args turbostate.ParsedArgsFromRust - err := json.Unmarshal([]byte(argsString), &args) + executionStateString := os.Args[1] + var executionState turbostate.ExecutionState + err := json.Unmarshal([]byte(executionStateString), &executionState) if err != nil { - fmt.Printf("Error unmarshalling CLI args: %v\n Arg string: %v\n", err, argsString) + fmt.Printf("Error unmarshalling execution state: %v\n Execution state string: %v\n", err, executionStateString) os.Exit(1) } - exitCode := cmd.RunWithArgs(&args, turboVersion) + exitCode := cmd.RunWithExecutionState(&executionState, turboVersion) os.Exit(exitCode) } diff --git a/cli/internal/client/client.go b/cli/internal/client/client.go index 822b2df559885..d177a2da8caf0 100644 --- a/cli/internal/client/client.go +++ b/cli/internal/client/client.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-retryablehttp" "github.com/vercel/turbo/cli/internal/ci" + "github.com/vercel/turbo/cli/internal/turbostate" ) // APIClient is the main interface for making network requests to Vercel @@ -47,31 +48,14 @@ func (c *APIClient) SetToken(token string) { c.token = token } -// RemoteConfig holds the authentication and endpoint details for the API client -type RemoteConfig struct { - Token string - TeamID string - TeamSlug string - APIURL string -} - -// Opts holds values for configuring the behavior of the API client -type Opts struct { - UsePreflight bool - Timeout uint64 -} - -// ClientTimeout Exported ClientTimeout used in run.go -const ClientTimeout uint64 = 20 - // NewClient creates a new APIClient -func NewClient(remoteConfig RemoteConfig, logger hclog.Logger, turboVersion string, opts Opts) *APIClient { +func NewClient(config turbostate.APIClientConfig, logger hclog.Logger, turboVersion string) *APIClient { client := &APIClient{ - baseURL: remoteConfig.APIURL, + baseURL: config.APIURL, turboVersion: turboVersion, HTTPClient: &retryablehttp.Client{ HTTPClient: &http.Client{ - Timeout: time.Duration(opts.Timeout) * time.Second, + Timeout: time.Duration(config.Timeout) * time.Second, }, RetryWaitMin: 2 * time.Second, RetryWaitMax: 10 * time.Second, @@ -79,10 +63,10 @@ func NewClient(remoteConfig RemoteConfig, logger hclog.Logger, turboVersion stri Backoff: retryablehttp.DefaultBackoff, Logger: logger, }, - token: remoteConfig.Token, - teamID: remoteConfig.TeamID, - teamSlug: remoteConfig.TeamSlug, - usePreflight: opts.UsePreflight, + token: config.Token, + teamID: config.TeamID, + teamSlug: config.TeamSlug, + usePreflight: config.UsePreflight, } client.HTTPClient.CheckRetry = client.checkRetry return client diff --git a/cli/internal/client/client_test.go b/cli/internal/client/client_test.go index 36ff3fbc7eb87..455861b0d2b3e 100644 --- a/cli/internal/client/client_test.go +++ b/cli/internal/client/client_test.go @@ -12,6 +12,7 @@ import ( "github.com/google/uuid" "github.com/hashicorp/go-hclog" + "github.com/vercel/turbo/cli/internal/turbostate" "github.com/vercel/turbo/cli/internal/util" ) @@ -30,12 +31,12 @@ func Test_sendToServer(t *testing.T) { })) defer ts.Close() - remoteConfig := RemoteConfig{ + apiClientConfig := turbostate.APIClientConfig{ TeamSlug: "my-team-slug", APIURL: ts.URL, Token: "my-token", } - apiClient := NewClient(remoteConfig, hclog.Default(), "v1", Opts{}) + apiClient := NewClient(apiClientConfig, hclog.Default(), "v1") myUUID, err := uuid.NewUUID() if err != nil { @@ -85,12 +86,12 @@ func Test_PutArtifact(t *testing.T) { defer ts.Close() // Set up test expected values - remoteConfig := RemoteConfig{ + apiClientConfig := turbostate.APIClientConfig{ TeamSlug: "my-team-slug", APIURL: ts.URL, Token: "my-token", } - apiClient := NewClient(remoteConfig, hclog.Default(), "v1", Opts{}) + apiClient := NewClient(apiClientConfig, hclog.Default(), "v1") expectedArtifactBody := []byte("My string artifact") // Test Put Artifact @@ -111,12 +112,12 @@ func Test_PutWhenCachingDisabled(t *testing.T) { defer ts.Close() // Set up test expected values - remoteConfig := RemoteConfig{ + apiClientConfig := turbostate.APIClientConfig{ TeamSlug: "my-team-slug", APIURL: ts.URL, Token: "my-token", } - apiClient := NewClient(remoteConfig, hclog.Default(), "v1", Opts{}) + apiClient := NewClient(apiClientConfig, hclog.Default(), "v1") expectedArtifactBody := []byte("My string artifact") // Test Put Artifact err := apiClient.PutArtifact("hash", expectedArtifactBody, 500, "") @@ -138,12 +139,12 @@ func Test_FetchWhenCachingDisabled(t *testing.T) { defer ts.Close() // Set up test expected values - remoteConfig := RemoteConfig{ + apiClientConfig := turbostate.APIClientConfig{ TeamSlug: "my-team-slug", APIURL: ts.URL, Token: "my-token", } - apiClient := NewClient(remoteConfig, hclog.Default(), "v1", Opts{}) + apiClient := NewClient(apiClientConfig, hclog.Default(), "v1") // Test Put Artifact resp, err := apiClient.FetchArtifact("hash") cd := &util.CacheDisabledError{} diff --git a/cli/internal/cmd/root.go b/cli/internal/cmd/root.go index 314cb97c99a4b..7b637be86b2be 100644 --- a/cli/internal/cmd/root.go +++ b/cli/internal/cmd/root.go @@ -44,29 +44,29 @@ func initializeOutputFiles(helper *cmdutil.Helper, parsedArgs *turbostate.Parsed return nil } -// RunWithArgs runs turbo with the ParsedArgsFromRust that is passed from the Rust side. -func RunWithArgs(args *turbostate.ParsedArgsFromRust, turboVersion string) int { +// RunWithExecutionState runs turbo with the ParsedArgsFromRust that is passed from the Rust side. +func RunWithExecutionState(executionState *turbostate.ExecutionState, turboVersion string) int { util.InitPrintf() // TODO: replace this with a context signalWatcher := signals.NewWatcher() - helper := cmdutil.NewHelper(turboVersion, args) + helper := cmdutil.NewHelper(turboVersion, &executionState.CLIArgs) ctx := context.Background() - err := initializeOutputFiles(helper, args) + err := initializeOutputFiles(helper, &executionState.CLIArgs) if err != nil { fmt.Printf("%v", err) return 1 } - defer helper.Cleanup(args) + defer helper.Cleanup(&executionState.CLIArgs) doneCh := make(chan struct{}) var execErr error go func() { - command := args.Command + command := executionState.CLIArgs.Command if command.Prune != nil { - execErr = prune.ExecutePrune(helper, args) + execErr = prune.ExecutePrune(helper, executionState) } else if command.Run != nil { - execErr = run.ExecuteRun(ctx, helper, signalWatcher, args) + execErr = run.ExecuteRun(ctx, helper, signalWatcher, executionState) } else { execErr = fmt.Errorf("unknown command: %v", command) } diff --git a/cli/internal/cmdutil/cmdutil.go b/cli/internal/cmdutil/cmdutil.go index 0b02392a8a741..112d3964c10a8 100644 --- a/cli/internal/cmdutil/cmdutil.go +++ b/cli/internal/cmdutil/cmdutil.go @@ -7,7 +7,6 @@ import ( "io" "io/ioutil" "os" - "strconv" "sync" "github.com/hashicorp/go-hclog" @@ -39,8 +38,6 @@ type Helper struct { 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 @@ -74,12 +71,12 @@ func (h *Helper) Cleanup(cliConfig *turbostate.ParsedArgsFromRust) { } } -func (h *Helper) getUI(cliConfig *turbostate.ParsedArgsFromRust) cli.Ui { +func (h *Helper) getUI(cliArgs *turbostate.ParsedArgsFromRust) cli.Ui { colorMode := ui.GetColorModeFromEnv() - if cliConfig.GetNoColor() { + if cliArgs.NoColor { colorMode = ui.ColorModeSuppressed } - if cliConfig.GetColor() { + if cliArgs.Color { colorMode = ui.ColorModeForced } return ui.BuildColoredUi(colorMode) @@ -134,15 +131,15 @@ func NewHelper(turboVersion string, args *turbostate.ParsedArgsFromRust) *Helper // 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) { +func (h *Helper) GetCmdBase(executionState *turbostate.ExecutionState) (*CmdBase, error) { // terminal is for color/no-color output - terminal := h.getUI(cliConfig) + terminal := h.getUI(&executionState.CLIArgs) // 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() + cwdRaw := executionState.CLIArgs.CWD if err != nil { return nil, err } @@ -155,45 +152,12 @@ func (h *Helper) GetCmdBase(cliConfig *turbostate.ParsedArgsFromRust) (*CmdBase, 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 - } - } - } + apiClientConfig := executionState.APIClientConfig apiClient := client.NewClient( - remoteConfig, + apiClientConfig, logger, h.TurboVersion, - h.clientOpts, ) return &CmdBase{ @@ -201,9 +165,6 @@ func (h *Helper) GetCmdBase(cliConfig *turbostate.ParsedArgsFromRust) (*CmdBase, Logger: logger, RepoRoot: repoRoot, APIClient: apiClient, - RepoConfig: repoConfig, - UserConfig: userConfig, - RemoteConfig: remoteConfig, TurboVersion: h.TurboVersion, }, nil } @@ -214,9 +175,6 @@ type CmdBase struct { Logger hclog.Logger RepoRoot turbopath.AbsoluteSystemPath APIClient *client.APIClient - RepoConfig *config.RepoConfig - UserConfig *config.UserConfig - RemoteConfig client.RemoteConfig TurboVersion string } diff --git a/cli/internal/cmdutil/cmdutil_test.go b/cli/internal/cmdutil/cmdutil_test.go index 4e6cf7047bf16..bf54d82952b8d 100644 --- a/cli/internal/cmdutil/cmdutil_test.go +++ b/cli/internal/cmdutil/cmdutil_test.go @@ -5,75 +5,25 @@ import ( "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 TestRemoteCacheTimeoutFlag(t *testing.T) { + args := turbostate.ParsedArgsFromRust{ + CWD: "", } -} - -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, + executionState := turbostate.ExecutionState{ + APIClientConfig: turbostate.APIClientConfig{ + Timeout: 599, + }, + CLIArgs: args, } - h := NewHelper("test-version", args) - base, err := h.GetCmdBase(args) + h := NewHelper("test-version", &args) + + base, err := h.GetCmdBase(&executionState) if err != nil { t.Fatalf("failed to get command base %v", err) } @@ -89,18 +39,23 @@ func TestRemoteCacheTimeoutPrimacy(t *testing.T) { t.Cleanup(func() { _ = os.Unsetenv(key) }) - args := &turbostate.ParsedArgsFromRust{ - CWD: "", - RemoteCacheTimeout: 1, + args := turbostate.ParsedArgsFromRust{ + CWD: "", + } + executionState := turbostate.ExecutionState{ + APIClientConfig: turbostate.APIClientConfig{ + Timeout: 1, + }, + CLIArgs: args, } - h := NewHelper("test-version", args) + h := NewHelper("test-version", &args) err := os.Setenv(key, value) if err != nil { t.Fatalf("setenv %v", err) } - base, err := h.GetCmdBase(args) + base, err := h.GetCmdBase(&executionState) if err != nil { t.Fatalf("failed to get command base %v", err) } diff --git a/cli/internal/config/config_file.go b/cli/internal/config/config_file.go index d3118b8732da2..a5567436cb4d7 100644 --- a/cli/internal/config/config_file.go +++ b/cli/internal/config/config_file.go @@ -1,13 +1,9 @@ package config import ( - "os" - "github.com/spf13/viper" - "github.com/vercel/turbo/cli/internal/client" "github.com/vercel/turbo/cli/internal/fs" "github.com/vercel/turbo/cli/internal/turbopath" - "github.com/vercel/turbo/cli/internal/turbostate" ) // RepoConfig is a configuration object for the logged-in turborepo.com user @@ -21,172 +17,8 @@ func (rc *RepoConfig) LoginURL() string { return rc.repoViper.GetString("loginurl") } -// SetTeamID sets the teamID and clears the slug, since it may have been from an old team -func (rc *RepoConfig) SetTeamID(teamID string) error { - // Note that we can't use viper.Set to set a nil value, we have to merge it in - newVals := map[string]interface{}{ - "teamid": teamID, - "teamslug": nil, - } - if err := rc.repoViper.MergeConfigMap(newVals); err != nil { - return err - } - return rc.write() -} - -// GetRemoteConfig produces the necessary values for an API client configuration -func (rc *RepoConfig) GetRemoteConfig(token string) client.RemoteConfig { - return client.RemoteConfig{ - Token: token, - TeamID: rc.repoViper.GetString("teamid"), - TeamSlug: rc.repoViper.GetString("teamslug"), - APIURL: rc.repoViper.GetString("apiurl"), - } -} - -// Internal call to save this config data to the user config file. -func (rc *RepoConfig) write() error { - if err := rc.path.EnsureDir(); err != nil { - return err - } - return rc.repoViper.WriteConfig() -} - -// Delete deletes the config file. This repo config shouldn't be used -// afterwards, it needs to be re-initialized -func (rc *RepoConfig) Delete() error { - return rc.path.Remove() -} - -// UserConfig is a wrapper around the user-specific configuration values -// for Turborepo. -type UserConfig struct { - userViper *viper.Viper - path turbopath.AbsoluteSystemPath -} - -// Token returns the Bearer token for this user if it exists -func (uc *UserConfig) Token() string { - return uc.userViper.GetString("token") -} - -// SetToken saves a Bearer token for this user, writing it to the -// user config file, creating it if necessary -func (uc *UserConfig) SetToken(token string) error { - // Technically Set works here, due to how overrides work, but use merge for consistency - if err := uc.userViper.MergeConfigMap(map[string]interface{}{"token": token}); err != nil { - return err - } - return uc.write() -} - -// Internal call to save this config data to the user config file. -func (uc *UserConfig) write() error { - if err := uc.path.EnsureDir(); err != nil { - return err - } - return uc.userViper.WriteConfig() -} - -// Delete deletes the config file. This user config shouldn't be used -// afterwards, it needs to be re-initialized -func (uc *UserConfig) Delete() error { - return uc.path.Remove() -} - -// ReadUserConfigFile creates a UserConfig using the -// specified path as the user config file. Note that the path or its parents -// do not need to exist. On a write to this configuration, they will be created. -func ReadUserConfigFile(path turbopath.AbsoluteSystemPath, cliConfig *turbostate.ParsedArgsFromRust) (*UserConfig, error) { - userViper := viper.New() - userViper.SetConfigFile(path.ToString()) - userViper.SetConfigType("json") - userViper.SetEnvPrefix("turbo") - userViper.MustBindEnv("token") - - token, err := cliConfig.GetToken() - if err != nil { - return nil, err - } - if token != "" { - userViper.Set("token", token) - } - - if err := userViper.ReadInConfig(); err != nil && !os.IsNotExist(err) { - return nil, err - } - return &UserConfig{ - userViper: userViper, - path: path, - }, nil -} - // DefaultUserConfigPath returns the default platform-dependent place that // we store the user-specific configuration. func DefaultUserConfigPath() turbopath.AbsoluteSystemPath { return fs.GetUserConfigDir().UntypedJoin("config.json") } - -const ( - _defaultAPIURL = "https://vercel.com/api" - _defaultLoginURL = "https://vercel.com" -) - -// ReadRepoConfigFile creates a RepoConfig using the -// specified path as the repo config file. Note that the path or its -// parents do not need to exist. On a write to this configuration, they -// will be created. -func ReadRepoConfigFile(path turbopath.AbsoluteSystemPath, cliConfig *turbostate.ParsedArgsFromRust) (*RepoConfig, error) { - repoViper := viper.New() - repoViper.SetConfigFile(path.ToString()) - repoViper.SetConfigType("json") - repoViper.SetEnvPrefix("turbo") - repoViper.MustBindEnv("apiurl", "TURBO_API") - repoViper.MustBindEnv("loginurl", "TURBO_LOGIN") - repoViper.MustBindEnv("teamslug", "TURBO_TEAM") - repoViper.MustBindEnv("teamid") - repoViper.SetDefault("apiurl", _defaultAPIURL) - repoViper.SetDefault("loginurl", _defaultLoginURL) - - login, err := cliConfig.GetLogin() - if err != nil { - return nil, err - } - if login != "" { - repoViper.Set("loginurl", login) - } - - api, err := cliConfig.GetAPI() - if err != nil { - return nil, err - } - if api != "" { - repoViper.Set("apiurl", api) - } - - team, err := cliConfig.GetTeam() - if err != nil { - return nil, err - } - if team != "" { - repoViper.Set("teamslug", team) - } - - if err := repoViper.ReadInConfig(); err != nil && !os.IsNotExist(err) { - return nil, err - } - // If team was set via commandline, don't read the teamId from the config file, as it - // won't necessarily match. - if team != "" { - repoViper.Set("teamid", "") - } - return &RepoConfig{ - repoViper: repoViper, - path: path, - }, nil -} - -// GetRepoConfigPath reads the user-specific configuration values -func GetRepoConfigPath(repoRoot turbopath.AbsoluteSystemPath) turbopath.AbsoluteSystemPath { - return repoRoot.UntypedJoin(".turbo", "config.json") -} diff --git a/cli/internal/config/config_file_test.go b/cli/internal/config/config_file_test.go deleted file mode 100644 index 7a19108269e6e..0000000000000 --- a/cli/internal/config/config_file_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package config - -import ( - "fmt" - "testing" - - "github.com/vercel/turbo/cli/internal/fs" - "github.com/vercel/turbo/cli/internal/turbostate" - "gotest.tools/v3/assert" -) - -func TestReadRepoConfigWhenMissing(t *testing.T) { - testDir := fs.AbsoluteSystemPathFromUpstream(t.TempDir()).UntypedJoin("config.json") - args := &turbostate.ParsedArgsFromRust{ - CWD: "", - } - - config, err := ReadRepoConfigFile(testDir, args) - if err != nil { - t.Errorf("got error reading non-existent config file: %v, want ", err) - } - if config == nil { - t.Error("got , wanted config value") - } -} - -func TestReadRepoConfigSetTeamAndAPIFlag(t *testing.T) { - testConfigFile := fs.AbsoluteSystemPathFromUpstream(t.TempDir()).UntypedJoin("turborepo", "config.json") - - slug := "my-team-slug" - apiURL := "http://my-login-url" - args := &turbostate.ParsedArgsFromRust{ - CWD: "", - Team: slug, - API: apiURL, - } - - teamID := "some-id" - assert.NilError(t, testConfigFile.EnsureDir(), "EnsureDir") - assert.NilError(t, testConfigFile.WriteFile([]byte(fmt.Sprintf(`{"teamId":"%v"}`, teamID)), 0644), "WriteFile") - - config, err := ReadRepoConfigFile(testConfigFile, args) - if err != nil { - t.Errorf("ReadRepoConfigFile err got %v, want ", err) - } - remoteConfig := config.GetRemoteConfig("") - if remoteConfig.TeamID != "" { - t.Errorf("TeamID got %v, want ", remoteConfig.TeamID) - } - if remoteConfig.TeamSlug != slug { - t.Errorf("TeamSlug got %v, want %v", remoteConfig.TeamSlug, slug) - } - if remoteConfig.APIURL != apiURL { - t.Errorf("APIURL got %v, want %v", remoteConfig.APIURL, apiURL) - } -} - -func TestRepoConfigIncludesDefaults(t *testing.T) { - testConfigFile := fs.AbsoluteSystemPathFromUpstream(t.TempDir()).UntypedJoin("turborepo", "config.json") - args := &turbostate.ParsedArgsFromRust{ - CWD: "", - } - - expectedTeam := "my-team" - - assert.NilError(t, testConfigFile.EnsureDir(), "EnsureDir") - assert.NilError(t, testConfigFile.WriteFile([]byte(fmt.Sprintf(`{"teamSlug":"%v"}`, expectedTeam)), 0644), "WriteFile") - - config, err := ReadRepoConfigFile(testConfigFile, args) - if err != nil { - t.Errorf("ReadRepoConfigFile err got %v, want ", err) - } - - remoteConfig := config.GetRemoteConfig("") - if remoteConfig.APIURL != _defaultAPIURL { - t.Errorf("api url got %v, want %v", remoteConfig.APIURL, _defaultAPIURL) - } - if remoteConfig.TeamSlug != expectedTeam { - t.Errorf("team slug got %v, want %v", remoteConfig.TeamSlug, expectedTeam) - } -} - -func TestWriteRepoConfig(t *testing.T) { - repoRoot := fs.AbsoluteSystemPathFromUpstream(t.TempDir()) - testConfigFile := repoRoot.UntypedJoin(".turbo", "config.json") - args := &turbostate.ParsedArgsFromRust{ - CWD: "", - } - - expectedTeam := "my-team" - - assert.NilError(t, testConfigFile.EnsureDir(), "EnsureDir") - assert.NilError(t, testConfigFile.WriteFile([]byte(fmt.Sprintf(`{"teamSlug":"%v"}`, expectedTeam)), 0644), "WriteFile") - - initial, err := ReadRepoConfigFile(testConfigFile, args) - assert.NilError(t, err, "GetRepoConfig") - // setting the teamID should clear the slug, since it may have been from an old team - expectedTeamID := "my-team-id" - err = initial.SetTeamID(expectedTeamID) - assert.NilError(t, err, "SetTeamID") - - config, err := ReadRepoConfigFile(testConfigFile, args) - if err != nil { - t.Errorf("ReadRepoConfig err got %v, want ", err) - } - - remoteConfig := config.GetRemoteConfig("") - if remoteConfig.TeamSlug != "" { - t.Errorf("Expected TeamSlug to be cleared, got %v", remoteConfig.TeamSlug) - } - if remoteConfig.TeamID != expectedTeamID { - t.Errorf("TeamID got %v, want %v", remoteConfig.TeamID, expectedTeamID) - } -} - -func TestWriteUserConfig(t *testing.T) { - configPath := fs.AbsoluteSystemPathFromUpstream(t.TempDir()).UntypedJoin("turborepo", "config.json") - args := &turbostate.ParsedArgsFromRust{ - CWD: "", - } - - // Non-existent config file should get empty values - userConfig, err := ReadUserConfigFile(configPath, args) - assert.NilError(t, err, "readUserConfigFile") - assert.Equal(t, userConfig.Token(), "") - assert.Equal(t, userConfig.path, configPath) - - expectedToken := "my-token" - err = userConfig.SetToken(expectedToken) - assert.NilError(t, err, "SetToken") - - config, err := ReadUserConfigFile(configPath, args) - assert.NilError(t, err, "readUserConfigFile") - assert.Equal(t, config.Token(), expectedToken) - - err = config.Delete() - assert.NilError(t, err, "deleteConfigFile") - assert.Equal(t, configPath.FileExists(), false, "config file should be deleted") - - final, err := ReadUserConfigFile(configPath, args) - assert.NilError(t, err, "readUserConfigFile") - assert.Equal(t, final.Token(), "") - assert.Equal(t, configPath.FileExists(), false, "config file should be deleted") -} - -func TestUserConfigFlags(t *testing.T) { - configPath := fs.AbsoluteSystemPathFromUpstream(t.TempDir()).UntypedJoin("turborepo", "config.json") - args := &turbostate.ParsedArgsFromRust{ - CWD: "", - Token: "my-token", - } - - userConfig, err := ReadUserConfigFile(configPath, args) - assert.NilError(t, err, "readUserConfigFile") - assert.Equal(t, userConfig.Token(), "my-token") - assert.Equal(t, userConfig.path, configPath) -} diff --git a/cli/internal/prune/prune.go b/cli/internal/prune/prune.go index a82023f13bd14..2dc541e86b150 100644 --- a/cli/internal/prune/prune.go +++ b/cli/internal/prune/prune.go @@ -28,12 +28,12 @@ type opts struct { } // ExecutePrune executes the `prune` command. -func ExecutePrune(helper *cmdutil.Helper, args *turbostate.ParsedArgsFromRust) error { - base, err := helper.GetCmdBase(args) +func ExecutePrune(helper *cmdutil.Helper, executionState *turbostate.ExecutionState) error { + base, err := helper.GetCmdBase(executionState) if err != nil { return err } - if len(args.Command.Prune.Scope) == 0 { + if len(executionState.CLIArgs.Command.Prune.Scope) == 0 { err := errors.New("at least one target must be specified") base.LogError(err.Error()) return err @@ -41,7 +41,7 @@ func ExecutePrune(helper *cmdutil.Helper, args *turbostate.ParsedArgsFromRust) e p := &prune{ base, } - if err := p.prune(args.Command.Prune); err != nil { + if err := p.prune(executionState.CLIArgs.Command.Prune); err != nil { logError(p.base.Logger, p.base.UI, err) return err } diff --git a/cli/internal/run/run.go b/cli/internal/run/run.go index fb5dda5cb5b61..8575e85f6f047 100644 --- a/cli/internal/run/run.go +++ b/cli/internal/run/run.go @@ -32,18 +32,18 @@ import ( ) // ExecuteRun executes the run command -func ExecuteRun(ctx gocontext.Context, helper *cmdutil.Helper, signalWatcher *signals.Watcher, args *turbostate.ParsedArgsFromRust) error { - base, err := helper.GetCmdBase(args) +func ExecuteRun(ctx gocontext.Context, helper *cmdutil.Helper, signalWatcher *signals.Watcher, executionState *turbostate.ExecutionState) error { + base, err := helper.GetCmdBase(executionState) LogTag(base.Logger) if err != nil { return err } - tasks := args.Command.Run.Tasks - passThroughArgs := args.Command.Run.PassThroughArgs + tasks := executionState.CLIArgs.Command.Run.Tasks + passThroughArgs := executionState.CLIArgs.Command.Run.PassThroughArgs if len(tasks) == 0 { return errors.New("at least one task must be specified") } - opts, err := optsFromArgs(args) + opts, err := optsFromArgs(&executionState.CLIArgs) if err != nil { return err } @@ -66,8 +66,6 @@ func optsFromArgs(args *turbostate.ParsedArgsFromRust) (*Opts, error) { return nil, err } - // Cache flags - opts.clientOpts.Timeout = args.RemoteCacheTimeout opts.cacheOpts.SkipFilesystem = runPayload.RemoteOnly opts.cacheOpts.OverrideDir = runPayload.CacheDir opts.cacheOpts.Workers = runPayload.CacheWorkers diff --git a/cli/internal/run/run_spec.go b/cli/internal/run/run_spec.go index 14402d39b5c83..8350ee56f0c2b 100644 --- a/cli/internal/run/run_spec.go +++ b/cli/internal/run/run_spec.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/vercel/turbo/cli/internal/cache" - "github.com/vercel/turbo/cli/internal/client" "github.com/vercel/turbo/cli/internal/runcache" "github.com/vercel/turbo/cli/internal/scope" "github.com/vercel/turbo/cli/internal/util" @@ -42,7 +41,6 @@ func (rs *runSpec) ArgsForTask(task string) []string { type Opts struct { runOpts util.RunOpts cacheOpts cache.Opts - clientOpts client.Opts runcacheOpts runcache.Opts scopeOpts scope.Opts } @@ -83,8 +81,5 @@ func getDefaultOptions() *Opts { runOpts: util.RunOpts{ Concurrency: 10, }, - clientOpts: client.Opts{ - Timeout: client.ClientTimeout, - }, } } diff --git a/cli/internal/turbostate/turbostate.go b/cli/internal/turbostate/turbostate.go index 2ed23ddc0f537..3f34df7a45bd8 100644 --- a/cli/internal/turbostate/turbostate.go +++ b/cli/internal/turbostate/turbostate.go @@ -4,18 +4,9 @@ package turbostate import ( - "fmt" - "github.com/vercel/turbo/cli/internal/util" ) -// RepoState is the state for repository. Consists of the root for the repo -// along with the mode (single package or multi package) -type RepoState struct { - Root string `json:"root"` - Mode string `json:"mode"` -} - // DaemonPayload is the extra flags and command that are // passed for the `daemon` subcommand type DaemonPayload struct { @@ -96,45 +87,18 @@ type ParsedArgsFromRust struct { Command Command `json:"command"` } -// GetColor returns the value of the `color` flag. -func (a ParsedArgsFromRust) GetColor() bool { - return a.Color -} - -// GetNoColor returns the value of the `token` flag. -func (a ParsedArgsFromRust) GetNoColor() bool { - return a.NoColor -} - -// GetLogin returns the value of the `login` flag. -func (a ParsedArgsFromRust) GetLogin() (string, error) { - return a.Login, nil -} - -// GetAPI returns the value of the `api` flag. -func (a ParsedArgsFromRust) GetAPI() (string, error) { - return a.API, nil -} - -// GetTeam returns the value of the `team` flag. -func (a ParsedArgsFromRust) GetTeam() (string, error) { - return a.Team, nil -} - -// GetToken returns the value of the `token` flag. -func (a ParsedArgsFromRust) GetToken() (string, error) { - return a.Token, nil -} - -// GetCwd returns the value of the `cwd` flag. -func (a ParsedArgsFromRust) GetCwd() (string, error) { - return a.CWD, nil +// ExecutionState is the entire state of a turbo execution that is passed from the Rust shim. +type ExecutionState struct { + APIClientConfig APIClientConfig `json:"remote_config"` + CLIArgs ParsedArgsFromRust `json:"cli_args"` } -// GetRemoteCacheTimeout returns the value of the `remote-cache-timeout` flag. -func (a ParsedArgsFromRust) GetRemoteCacheTimeout() (uint64, error) { - if a.RemoteCacheTimeout != 0 { - return a.RemoteCacheTimeout, nil - } - return 0, fmt.Errorf("no remote cache timeout provided") +// APIClientConfig holds the authentication and endpoint details for the API client +type APIClientConfig struct { + Token string `json:"token"` + TeamID string `json:"team_id"` + TeamSlug string `json:"team_slug"` + APIURL string `json:"api_url"` + UsePreflight bool `json:"use_preflight"` + Timeout uint64 `json:"timeout"` } diff --git a/crates/turborepo-api-client/src/lib.rs b/crates/turborepo-api-client/src/lib.rs index ef242b7099178..30c50027713af 100644 --- a/crates/turborepo-api-client/src/lib.rs +++ b/crates/turborepo-api-client/src/lib.rs @@ -308,16 +308,13 @@ impl APIClient { false } - pub fn new( - base_url: impl AsRef, - timeout: Option, - version: &'static str, - ) -> Result { - let client = match timeout { - Some(timeout) => reqwest::Client::builder() + pub fn new(base_url: impl AsRef, timeout: u64, version: &'static str) -> Result { + let client = if timeout != 0 { + reqwest::Client::builder() .timeout(std::time::Duration::from_secs(timeout)) - .build()?, - None => reqwest::Client::builder().build()?, + .build()? + } else { + reqwest::Client::builder().build()? }; let user_agent = format!( diff --git a/crates/turborepo-lib/src/cli.rs b/crates/turborepo-lib/src/cli.rs index a2489183cbc8c..599ff14b45f65 100644 --- a/crates/turborepo-lib/src/cli.rs +++ b/crates/turborepo-lib/src/cli.rs @@ -434,12 +434,12 @@ pub enum LogPrefix { /// returns: Result #[tokio::main] pub async fn run(repo_state: Option) -> Result { - let mut clap_args = Args::new()?; + let mut cli_args = Args::new()?; // If there is no command, we set the command to `Command::Run` with // `self.parsed_args.run_args` as arguments. - if clap_args.command.is_none() { - if let Some(run_args) = mem::take(&mut clap_args.run_args) { - clap_args.command = Some(Command::Run(Box::new(run_args))); + if cli_args.command.is_none() { + if let Some(run_args) = mem::take(&mut cli_args.run_args) { + cli_args.command = Some(Command::Run(Box::new(run_args))); } else { return Err(anyhow!("No command specified")); } @@ -447,8 +447,8 @@ pub async fn run(repo_state: Option) -> Result { // If this is a run command, and we know the actual invocation path, set the // inference root, as long as the user hasn't overridden the cwd - if clap_args.cwd.is_none() { - if let Some(Command::Run(run_args)) = &mut clap_args.command { + if cli_args.cwd.is_none() { + if let Some(Command::Run(run_args)) = &mut cli_args.command { if let Ok(invocation_dir) = env::var(INVOCATION_DIR_ENV_VAR) { let invocation_path = Path::new(&invocation_dir); @@ -473,16 +473,16 @@ pub async fn run(repo_state: Option) -> Result { // Do this after the above, since we're now always setting cwd. if let Some(repo_state) = repo_state { - if let Some(Command::Run(run_args)) = &mut clap_args.command { + if let Some(Command::Run(run_args)) = &mut cli_args.command { run_args.single_package = matches!(repo_state.mode, RepoMode::SinglePackage); } - clap_args.cwd = Some(repo_state.root); + cli_args.cwd = Some(repo_state.root); } - let repo_root = if let Some(cwd) = &clap_args.cwd { + let repo_root = if let Some(cwd) = &cli_args.cwd { let canonical_cwd = fs_canonicalize(cwd)?; // Update on clap_args so that Go gets a canonical path. - clap_args.cwd = Some(canonical_cwd.clone()); + cli_args.cwd = Some(canonical_cwd.clone()); canonical_cwd } else { current_dir()? @@ -490,27 +490,27 @@ pub async fn run(repo_state: Option) -> Result { let version = get_version(); - match clap_args.command.as_ref().unwrap() { + match cli_args.command.as_ref().unwrap() { Command::Bin { .. } => { bin::run()?; Ok(Payload::Rust(Ok(0))) } Command::Logout { .. } => { - let mut base = CommandBase::new(clap_args, repo_root, version)?; + let mut base = CommandBase::new(cli_args, repo_root, version)?; logout::logout(&mut base)?; Ok(Payload::Rust(Ok(0))) } Command::Login { sso_team } => { - if clap_args.test_run { + if cli_args.test_run { println!("Login test run successful"); return Ok(Payload::Rust(Ok(0))); } let sso_team = sso_team.clone(); - let mut base = CommandBase::new(clap_args, repo_root, version)?; + let mut base = CommandBase::new(cli_args, repo_root, version)?; if let Some(sso_team) = sso_team { login::sso_login(&mut base, &sso_team).await?; @@ -524,36 +524,36 @@ pub async fn run(repo_state: Option) -> Result { no_gitignore, target, } => { - if clap_args.test_run { + if cli_args.test_run { println!("Link test run successful"); return Ok(Payload::Rust(Ok(0))); } let modify_gitignore = !*no_gitignore; let to = *target; - let mut base = CommandBase::new(clap_args, repo_root, version)?; + let mut base = CommandBase::new(cli_args, repo_root, version)?; if let Err(err) = link::link(&mut base, modify_gitignore, to).await { error!("error: {}", err.to_string()) - }; + } Ok(Payload::Rust(Ok(0))) } Command::Unlink { target } => { - if clap_args.test_run { + if cli_args.test_run { println!("Unlink test run successful"); return Ok(Payload::Rust(Ok(0))); } let from = *target; - let mut base = CommandBase::new(clap_args, repo_root, version)?; + let mut base = CommandBase::new(cli_args, repo_root, version)?; unlink::unlink(&mut base, from)?; Ok(Payload::Rust(Ok(0))) } Command::Daemon { command, idle_time } => { - let base = CommandBase::new(clap_args.clone(), repo_root, version)?; + let base = CommandBase::new(cli_args.clone(), repo_root, version)?; match command { Some(command) => daemon::daemon_client(command, &base).await, @@ -562,7 +562,10 @@ pub async fn run(repo_state: Option) -> Result { Ok(Payload::Rust(Ok(0))) } - Command::Prune { .. } | Command::Run(_) => Ok(Payload::Go(Box::new(clap_args))), + Command::Prune { .. } | Command::Run(_) => { + let base = CommandBase::new(cli_args, repo_root, version)?; + Ok(Payload::Go(Box::new(base))) + } Command::Completion { shell } => { generate(*shell, &mut Args::command(), "turbo", &mut io::stdout()); diff --git a/crates/turborepo-lib/src/commands/mod.rs b/crates/turborepo-lib/src/commands/mod.rs index db5383c3ee63b..c55bde422648e 100644 --- a/crates/turborepo-lib/src/commands/mod.rs +++ b/crates/turborepo-lib/src/commands/mod.rs @@ -128,6 +128,10 @@ impl CommandBase { Ok(self.client_config.get().unwrap()) } + pub fn args(&self) -> &Args { + &self.args + } + pub fn api_client(&mut self) -> Result { let repo_config = self.repo_config()?; let client_config = self.client_config()?; diff --git a/crates/turborepo-lib/src/config/client.rs b/crates/turborepo-lib/src/config/client.rs index 567b2e0373065..ae77a21cff0b3 100644 --- a/crates/turborepo-lib/src/config/client.rs +++ b/crates/turborepo-lib/src/config/client.rs @@ -13,7 +13,7 @@ pub struct ClientConfig { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] struct ClientConfigValue { - remote_cache_timeout: Option, + remote_cache_timeout: u64, } #[derive(Debug, Clone)] @@ -23,24 +23,13 @@ pub struct ClientConfigLoader { } impl ClientConfig { - #[allow(dead_code)] - pub fn remote_cache_timeout(&self) -> Option { - match self.config.remote_cache_timeout { - // Pass 0 to get no timeout. - Some(0) => None, - - // Pass any non-zero uint64 to get a timeout of that duration measured in seconds. - Some(other) => Some(other), - - // If the _config_ doesn't have a remote_cache_timeout, give them the default. - None => Some(DEFAULT_TIMEOUT), - } + pub fn remote_cache_timeout(&self) -> u64 { + self.config.remote_cache_timeout } } impl ClientConfigLoader { /// Creates a loader that will load the client config - #[allow(dead_code)] pub fn new() -> Self { Self { remote_cache_timeout: None, @@ -49,7 +38,6 @@ impl ClientConfigLoader { } /// Set an override for token that the user provided via the command line - #[allow(dead_code)] pub fn with_remote_cache_timeout(mut self, remote_cache_timeout: Option) -> Self { self.remote_cache_timeout = remote_cache_timeout; self @@ -61,7 +49,6 @@ impl ClientConfigLoader { self } - #[allow(dead_code)] pub fn load(self) -> Result { let Self { remote_cache_timeout, @@ -79,7 +66,7 @@ impl ClientConfigLoader { match config_attempt { Err(_) => Ok(ClientConfig { config: ClientConfigValue { - remote_cache_timeout: None, + remote_cache_timeout: DEFAULT_TIMEOUT, }, }), Ok(config) => Ok(ClientConfig { config }), @@ -107,16 +94,16 @@ mod test { fn test_client_default() -> Result<()> { let config = ClientConfigLoader::new().load()?; - assert_eq!(config.remote_cache_timeout(), Some(DEFAULT_TIMEOUT)); + assert_eq!(config.remote_cache_timeout(), DEFAULT_TIMEOUT); Ok(()) } fn test_client_arg_variable() -> Result<()> { - let arg_value = Some(1); + let arg_value: u64 = 1; let config = ClientConfigLoader::new() - .with_remote_cache_timeout(arg_value) + .with_remote_cache_timeout(Some(arg_value)) .load()?; assert_eq!(config.remote_cache_timeout(), arg_value); @@ -137,7 +124,7 @@ mod test { assert_eq!( config.remote_cache_timeout(), - Some(env_value.parse::().unwrap()) + env_value.parse::().unwrap() ); Ok(()) @@ -145,76 +132,78 @@ mod test { #[test] fn test_client_arg_env_variable() -> Result<()> { + #[derive(Debug)] struct TestCase { arg: Option, env: String, - output: Option, + output: u64, } let tests = [ TestCase { arg: Some(0), env: String::from("0"), - output: None, + output: 0, }, TestCase { arg: Some(0), env: String::from("2"), - output: None, + output: 0, }, TestCase { arg: Some(0), env: String::from("garbage"), - output: None, + output: 0, }, TestCase { arg: Some(0), env: String::from(""), - output: None, + output: 0, }, TestCase { arg: Some(1), env: String::from("0"), - output: Some(1), + output: 1, }, TestCase { arg: Some(1), env: String::from("2"), - output: Some(1), + output: 1, }, TestCase { arg: Some(1), env: String::from("garbage"), - output: Some(1), + output: 1, }, TestCase { arg: Some(1), env: String::from(""), - output: Some(1), + output: 1, }, TestCase { arg: None, env: String::from("0"), - output: None, + output: 0, }, TestCase { arg: None, env: String::from("2"), - output: Some(2), + output: 2, }, TestCase { arg: None, env: String::from("garbage"), - output: Some(DEFAULT_TIMEOUT), + output: DEFAULT_TIMEOUT, }, TestCase { arg: None, env: String::from(""), - output: Some(DEFAULT_TIMEOUT), + output: DEFAULT_TIMEOUT, }, ]; for test in &tests { + println!("{:?}", test); let config = ClientConfigLoader::new() .with_remote_cache_timeout(test.arg) .with_environment({ diff --git a/crates/turborepo-lib/src/config/repo.rs b/crates/turborepo-lib/src/config/repo.rs index f960e57dba564..3d4615bf4c0d9 100644 --- a/crates/turborepo-lib/src/config/repo.rs +++ b/crates/turborepo-lib/src/config/repo.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + env, path::{Path, PathBuf}, }; @@ -21,10 +22,14 @@ pub struct RepoConfig { #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Default)] struct RepoConfigValue { - apiurl: Option, - loginurl: Option, - teamslug: Option, - teamid: Option, + #[serde(rename = "apiurl")] + api_url: Option, + #[serde(rename = "loginurl")] + login_url: Option, + #[serde(rename = "teamslug")] + team_slug: Option, + #[serde(rename = "teamid")] + team_id: Option, } #[derive(Debug, Clone)] @@ -32,39 +37,42 @@ pub struct RepoConfigLoader { path: PathBuf, api: Option, login: Option, - teamslug: Option, + team_slug: Option, environment: Option>, } impl RepoConfig { #[allow(dead_code)] pub fn api_url(&self) -> &str { - self.config.apiurl.as_deref().unwrap_or(DEFAULT_API_URL) + self.config.api_url.as_deref().unwrap_or(DEFAULT_API_URL) } #[allow(dead_code)] pub fn login_url(&self) -> &str { - self.config.loginurl.as_deref().unwrap_or(DEFAULT_LOGIN_URL) + self.config + .login_url + .as_deref() + .unwrap_or(DEFAULT_LOGIN_URL) } #[allow(dead_code)] pub fn team_slug(&self) -> Option<&str> { - self.config.teamslug.as_deref() + self.config.team_slug.as_deref() } #[allow(dead_code)] pub fn team_id(&self) -> Option<&str> { - self.config.teamid.as_deref() + self.config.team_id.as_deref() } /// Sets the team id and clears the team slug, since it may have been from /// an old team #[allow(dead_code)] pub fn set_team_id(&mut self, team_id: Option) -> Result<()> { - self.disk_config.teamslug = None; - self.config.teamslug = None; - self.disk_config.teamid = team_id.clone(); - self.config.teamid = team_id; + self.disk_config.team_slug = None; + self.config.team_slug = None; + self.disk_config.team_id = team_id.clone(); + self.config.team_id = team_id; self.write_to_disk() } @@ -85,7 +93,7 @@ impl RepoConfigLoader { path, api: None, login: None, - teamslug: None, + team_slug: None, environment: None, } } @@ -104,7 +112,7 @@ impl RepoConfigLoader { #[allow(dead_code)] pub fn with_team_slug(mut self, team_slug: Option) -> Self { - self.teamslug = team_slug; + self.team_slug = team_slug; self } @@ -120,7 +128,7 @@ impl RepoConfigLoader { path, api, login, - teamslug, + team_slug, environment, } = self; let raw_disk_config = Config::builder() @@ -131,7 +139,7 @@ impl RepoConfigLoader { ) .build()?; - let has_teamslug_override = teamslug.is_some(); + let has_team_slug_override = team_slug.is_some(); let mut config: RepoConfigValue = Config::builder() .add_source(raw_disk_config.clone()) @@ -144,7 +152,7 @@ impl RepoConfigLoader { ) .set_override_option("apiurl", api)? .set_override_option("loginurl", login)? - .set_override_option("teamslug", teamslug)? + .set_override_option("teamslug", team_slug)? // set teamid to none if teamslug present .build()? .try_deserialize()?; @@ -153,8 +161,13 @@ impl RepoConfigLoader { // If teamid was passed via command line flag we ignore team slug as it // might not match. - if has_teamslug_override { - config.teamid = None; + if has_team_slug_override { + config.team_id = None; + } + + // We don't set this above because it's specific to team_id + if let Ok(vercel_artifacts_owner) = env::var("VERCEL_ARTIFACTS_OWNER") { + config.team_id = Some(vercel_artifacts_owner); } Ok(RepoConfig { @@ -173,6 +186,14 @@ mod test { use super::*; + #[test] + fn test_repo_config_when_missing() -> Result<()> { + let config = RepoConfigLoader::new(PathBuf::from("missing")).load(); + assert!(config.is_ok()); + + Ok(()) + } + #[test] fn test_repo_config_with_team_and_api_flags() -> Result<()> { let mut config_file = NamedTempFile::new()?; @@ -190,6 +211,17 @@ mod test { Ok(()) } + #[test] + fn test_repo_config_includes_defaults() { + let config = RepoConfigLoader::new(PathBuf::from("missing")) + .load() + .unwrap(); + assert_eq!(config.api_url(), DEFAULT_API_URL); + assert_eq!(config.login_url(), DEFAULT_LOGIN_URL); + assert_eq!(config.team_slug(), None); + assert_eq!(config.team_id(), None); + } + #[test] fn test_team_override_clears_id() -> Result<()> { let mut config_file = NamedTempFile::new()?; diff --git a/crates/turborepo-lib/src/config/user.rs b/crates/turborepo-lib/src/config/user.rs index aa03910249096..487bb1b3485fc 100644 --- a/crates/turborepo-lib/src/config/user.rs +++ b/crates/turborepo-lib/src/config/user.rs @@ -92,7 +92,8 @@ impl UserConfigLoader { let config = Config::builder() .add_source(raw_disk_config.clone()) - .add_source(Environment::with_prefix("turbo").source(environment)) + .add_source(Environment::with_prefix("TURBO").source(environment.clone())) + .add_source(Environment::with_prefix("VERCEL_ARTIFACTS").source(environment)) .set_override_option("token", token)? .build()? .try_deserialize()?; @@ -144,19 +145,28 @@ mod test { Ok(()) } + static TOKEN_ENV_VARS: [&'static str; 2] = ["TURBO_TOKEN", "VERCEL_ARTIFACTS_TOKEN"]; + #[test] fn test_env_var_trumps_disk() -> Result<()> { let mut config_file = NamedTempFile::new()?; writeln!(&mut config_file, "{{\"token\": \"foo\"}}")?; - let env = { - let mut map = HashMap::new(); - map.insert("TURBO_TOKEN".into(), "bar".into()); - map - }; - let config = UserConfigLoader::new(config_file.path().to_path_buf()) - .with_environment(Some(env)) - .load()?; - assert_eq!(config.token(), Some("bar")); + + for (idx, env_var) in TOKEN_ENV_VARS.into_iter().enumerate() { + let env_var_value = format!("bar{}", idx); + + let env = { + let mut map = HashMap::new(); + map.insert(env_var.into(), env_var_value.clone()); + map + }; + let config = UserConfigLoader::new(config_file.path().to_path_buf()) + .with_environment(Some(env)) + .load()?; + + assert_eq!(config.token(), Some(env_var_value.as_str())); + } + Ok(()) } } diff --git a/crates/turborepo-lib/src/execution_state.rs b/crates/turborepo-lib/src/execution_state.rs new file mode 100644 index 0000000000000..9f6558e8f8c3d --- /dev/null +++ b/crates/turborepo-lib/src/execution_state.rs @@ -0,0 +1,46 @@ +use serde::Serialize; + +use crate::{cli::Args, commands::CommandBase}; + +#[derive(Debug, Serialize)] +pub struct ExecutionState<'a> { + pub api_client_config: APIClientConfig<'a>, + pub cli_args: &'a Args, +} + +#[derive(Debug, Serialize, Default)] +pub struct APIClientConfig<'a> { + // Comes from user config, i.e. $XDG_CONFIG_HOME/turborepo/config.json + pub token: Option<&'a str>, + // Comes from repo config, i.e. ./.turbo/config.json + pub team_id: Option<&'a str>, + pub team_slug: Option<&'a str>, + pub api_url: &'a str, + pub use_preflight: bool, + pub timeout: u64, +} + +impl<'a> TryFrom<&'a CommandBase> for ExecutionState<'a> { + type Error = anyhow::Error; + + fn try_from(base: &'a CommandBase) -> Result { + let repo_config = base.repo_config()?; + let user_config = base.user_config()?; + let client_config = base.client_config()?; + let args = base.args(); + + let remote_config = APIClientConfig { + token: user_config.token(), + team_id: repo_config.team_id(), + team_slug: repo_config.team_slug(), + api_url: repo_config.api_url(), + use_preflight: args.preflight, + timeout: client_config.remote_cache_timeout(), + }; + + Ok(ExecutionState { + api_client_config: remote_config, + cli_args: base.args(), + }) + } +} diff --git a/crates/turborepo-lib/src/lib.rs b/crates/turborepo-lib/src/lib.rs index 7aa29855de0ae..baa0d97e9c0a1 100644 --- a/crates/turborepo-lib/src/lib.rs +++ b/crates/turborepo-lib/src/lib.rs @@ -5,6 +5,7 @@ mod cli; mod commands; mod config; mod daemon; +mod execution_state; pub(crate) mod globwatcher; mod package_manager; mod shim; @@ -14,15 +15,15 @@ use anyhow::Result; pub use child::spawn_child; use log::error; -pub use crate::cli::Args; -use crate::package_manager::PackageManager; +pub use crate::{cli::Args, execution_state::ExecutionState}; +use crate::{commands::CommandBase, package_manager::PackageManager}; /// The payload from running main, if the program can complete without using Go -/// the Rust variant will be returned. If Go is needed then the args that -/// should be passed to Go will be returned. +/// the Rust variant will be returned. If Go is needed then the execution state +/// that should be passed to Go will be returned. pub enum Payload { Rust(Result), - Go(Box), + Go(Box), } pub fn get_version() -> &'static str { diff --git a/crates/turborepo/src/main.rs b/crates/turborepo/src/main.rs index 84ee07a7bde3c..c917dd0b50010 100644 --- a/crates/turborepo/src/main.rs +++ b/crates/turborepo/src/main.rs @@ -7,9 +7,9 @@ use std::{ use anyhow::Result; use dunce::canonicalize as fs_canonicalize; use log::{debug, error, trace}; -use turborepo_lib::{spawn_child, Args, Payload}; +use turborepo_lib::{spawn_child, ExecutionState, Payload}; -fn run_go_binary(args: Args) -> Result { +fn run_go_binary(execution_state: ExecutionState) -> Result { // canonicalize the binary path to ensure we can find go-turbo let turbo_path = fs_canonicalize(current_exe()?)?; let mut go_binary_path = turbo_path.clone(); @@ -41,7 +41,13 @@ fn run_go_binary(args: Args) -> Result { )); } - let serialized_args = serde_json::to_string(&args)?; + if execution_state.cli_args.test_run { + let serialized_args = serde_json::to_string_pretty(&execution_state)?; + println!("{}", serialized_args); + return Ok(0); + } + + let serialized_args = serde_json::to_string(&execution_state)?; trace!("Invoking go binary with {}", serialized_args); let mut command = process::Command::new(go_binary_path); command @@ -60,7 +66,7 @@ fn run_go_binary(args: Args) -> Result { fn main() -> Result<()> { let exit_code = match turborepo_lib::main() { Payload::Rust(res) => res.unwrap_or(1), - Payload::Go(state) => run_go_binary(*state)?, + Payload::Go(base) => run_go_binary((&*base).try_into()?)?, }; process::exit(exit_code) diff --git a/turborepo-tests/integration/tests/api-client-config.t b/turborepo-tests/integration/tests/api-client-config.t new file mode 100644 index 0000000000000..a53706b9e5202 --- /dev/null +++ b/turborepo-tests/integration/tests/api-client-config.t @@ -0,0 +1,42 @@ +Setup + $ . ${TESTDIR}/../../helpers/setup.sh + $ . ${TESTDIR}/_helpers/setup_monorepo.sh $(pwd) + +Run test run + $ ${TURBO} run build --__test-run | jq .remote_config + { + "token": null, + "team_id": null, + "team_slug": null, + "api_url": "https://vercel.com/api", + "use_preflight": false, + "timeout": 20 + } + +Run test run with api overloaded + $ ${TURBO} run build --__test-run --api http://localhost:8000 | jq .remote_config.api_url + null + +Run test run with token overloaded + $ ${TURBO} run build --__test-run --token 1234567890 | jq .remote_config.token + "1234567890" + +Run test run with token overloaded from both TURBO_TOKEN and VERCEL_ARTIFACTS_TOKEN + $ TURBO_TOKEN=turbo VERCEL_ARTIFACTS_TOKEN=vercel ${TURBO} run build --__test-run | jq .remote_config.token + "vercel" + +Run test run with team overloaded + $ ${TURBO} run build --__test-run --team vercel | jq .remote_config.team_slug + "vercel" + +Run test run with team overloaded from both env and flag (flag should take precedence) + $ TURBO_TEAM=vercel ${TURBO} run build --__test-run --team turbo | jq .remote_config.team_slug + "turbo" + +Run test run with remote cache timeout env variable set + $ TURBO_REMOTE_CACHE_TIMEOUT=123 ${TURBO} run build --__test-run | jq .remote_config.timeout + 123 + +Run test run with remote cache timeout from both env and flag (flag should take precedence) + $ TURBO_REMOTE_CACHE_TIMEOUT=123 ${TURBO} run build --__test-run --remote-cache-timeout 456 | jq .remote_config.timeout + 456