aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/cli/internal/config
diff options
context:
space:
mode:
Diffstat (limited to 'cli/internal/config')
-rw-r--r--cli/internal/config/config_file.go192
-rw-r--r--cli/internal/config/config_file_test.go157
2 files changed, 349 insertions, 0 deletions
diff --git a/cli/internal/config/config_file.go b/cli/internal/config/config_file.go
new file mode 100644
index 0000000..d3118b8
--- /dev/null
+++ b/cli/internal/config/config_file.go
@@ -0,0 +1,192 @@
+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
+type RepoConfig struct {
+ repoViper *viper.Viper
+ path turbopath.AbsoluteSystemPath
+}
+
+// LoginURL returns the configured URL for authenticating the user
+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
new file mode 100644
index 0000000..7a19108
--- /dev/null
+++ b/cli/internal/config/config_file_test.go
@@ -0,0 +1,157 @@
+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 <nil>", err)
+ }
+ if config == nil {
+ t.Error("got <nil>, 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 <nil>", err)
+ }
+ remoteConfig := config.GetRemoteConfig("")
+ if remoteConfig.TeamID != "" {
+ t.Errorf("TeamID got %v, want <empty string>", 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 <nil>", 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 <nil>", 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)
+}