diff options
| author | 2023-04-28 01:36:44 +0800 | |
|---|---|---|
| committer | 2023-04-28 01:36:44 +0800 | |
| commit | dd84b9d64fb98746a230cd24233ff50a562c39c9 (patch) | |
| tree | b583261ef00b3afe72ec4d6dacb31e57779a6faf /cli/internal/config | |
| parent | 0b46fcd72ac34382387b2bcf9095233efbcc52f4 (diff) | |
| download | HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.tar.gz HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.zip | |
Diffstat (limited to 'cli/internal/config')
| -rw-r--r-- | cli/internal/config/config_file.go | 192 | ||||
| -rw-r--r-- | cli/internal/config/config_file_test.go | 157 |
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) +} |
