-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add afero to Config, Refactor Config file access #1156
Changes from 1 commit
5a89151
673093d
d617746
18e9706
799e507
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,10 +2,12 @@ package config | |
|
||
import ( | ||
"encoding/json" | ||
"io/ioutil" | ||
"errors" | ||
"os" | ||
"path/filepath" | ||
|
||
"github.com/adrg/xdg" | ||
"github.com/spf13/afero" | ||
"github.com/vercel/turborepo/cli/internal/fs" | ||
) | ||
|
||
|
@@ -23,13 +25,27 @@ type TurborepoConfig struct { | |
TeamSlug string `json:"teamSlug,omitempty" envconfig:"team"` | ||
} | ||
|
||
func defaultUserConfig() *TurborepoConfig { | ||
return &TurborepoConfig{ | ||
ApiUrl: "https://vercel.com/api", | ||
LoginUrl: "https://vercel.com", | ||
} | ||
} | ||
|
||
func defaultRepoConfig() *TurborepoConfig { | ||
return &TurborepoConfig{ | ||
ApiUrl: "https://vercel.com/api", | ||
LoginUrl: "https://vercel.com", | ||
} | ||
} | ||
|
||
// writeConfigFile writes config file at a path | ||
func writeConfigFile(path string, config *TurborepoConfig) error { | ||
func writeConfigFile(fsys afero.Fs, path fs.AbsolutePath, config *TurborepoConfig) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This looks like it clobbers whatever is there? Should we make this a |
||
jsonBytes, marshallError := json.Marshal(config) | ||
if marshallError != nil { | ||
return marshallError | ||
} | ||
writeFilErr := ioutil.WriteFile(path, jsonBytes, 0644) | ||
writeFilErr := fs.WriteFile(fsys, path, jsonBytes, 0644) | ||
if writeFilErr != nil { | ||
return writeFilErr | ||
} | ||
|
@@ -38,38 +54,51 @@ func writeConfigFile(path string, config *TurborepoConfig) error { | |
|
||
// WriteRepoConfigFile is used to write the portion of the config file that is saved | ||
// within the repository itself. | ||
func WriteRepoConfigFile(config *TurborepoConfig) error { | ||
fs.EnsureDir(filepath.Join(".turbo", "config.json")) | ||
path := filepath.Join(".turbo", "config.json") | ||
return writeConfigFile(path, config) | ||
func WriteRepoConfigFile(fsys afero.Fs, repoRoot fs.AbsolutePath, toWrite *TurborepoConfig) error { | ||
path := repoRoot.Join(".turbo", "config.json") | ||
err := fs.EnsureDirFS(fsys, path) | ||
if err != nil { | ||
return err | ||
} | ||
return writeConfigFile(fsys, path, toWrite) | ||
} | ||
|
||
// WriteUserConfigFile writes a user config file. This may contain a token and so should | ||
// not be saved within the repository to avoid committing sensitive data | ||
func WriteUserConfigFile(config *TurborepoConfig) error { | ||
func userConfigPath(fsys afero.Fs) (fs.AbsolutePath, error) { | ||
path, err := xdg.ConfigFile(filepath.Join("turborepo", "config.json")) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
And the It isn't fully-required as it isn't a code path we'd necessarily care to push through a custom FS, but it'd be nice to be able to use a consistent filesystem abstraction. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, unfortunate. Looks like upstream is open to a patch though, which is good. |
||
if err != nil { | ||
return err | ||
return "", err | ||
} | ||
return writeConfigFile(path, config) | ||
absPath, err := fs.CheckedToAbsolutePath(path) | ||
if err != nil { | ||
return "", err | ||
} | ||
return absPath, nil | ||
} | ||
|
||
// ReadConfigFile reads a config file at a path | ||
func ReadConfigFile(path string) (*TurborepoConfig, error) { | ||
var config = &TurborepoConfig{ | ||
Token: "", | ||
TeamId: "", | ||
ApiUrl: "https://vercel.com/api", | ||
LoginUrl: "https://vercel.com", | ||
TeamSlug: "", | ||
// WriteUserConfigFile writes the given configuration to a user-specific | ||
// configuration file. This is for values that are not shared with a team, such | ||
// as credentials. | ||
func WriteUserConfigFile(fsys afero.Fs, config *TurborepoConfig) error { | ||
path, err := userConfigPath(fsys) | ||
if err != nil { | ||
return err | ||
} | ||
b, err := ioutil.ReadFile(path) | ||
return writeConfigFile(fsys, path, config) | ||
} | ||
|
||
// readConfigFile reads a config file at a path | ||
func readConfigFile(fsys afero.Fs, path fs.AbsolutePath, defaults func() *TurborepoConfig) (*TurborepoConfig, error) { | ||
b, err := fs.ReadFile(fsys, path) | ||
if err != nil { | ||
return config, err | ||
if errors.Is(err, os.ErrNotExist) { | ||
return nil, nil | ||
} | ||
return nil, err | ||
} | ||
config := defaults() | ||
jsonErr := json.Unmarshal(b, config) | ||
if jsonErr != nil { | ||
return config, jsonErr | ||
return nil, jsonErr | ||
} | ||
if config.ApiUrl == "https://api.vercel.com" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An optional nice-to-have cleanup for this PR would be to go ahead and rewrite this to the correct value on disk or spit out a log message. |
||
config.ApiUrl = "https://vercel.com/api" | ||
|
@@ -78,21 +107,25 @@ func ReadConfigFile(path string) (*TurborepoConfig, error) { | |
} | ||
|
||
// ReadUserConfigFile reads a user config file | ||
func ReadUserConfigFile() (*TurborepoConfig, error) { | ||
path, err := xdg.ConfigFile(filepath.Join("turborepo", "config.json")) | ||
func ReadUserConfigFile(fsys afero.Fs) (*TurborepoConfig, error) { | ||
path, err := userConfigPath(fsys) | ||
if err != nil { | ||
return &TurborepoConfig{ | ||
Token: "", | ||
TeamId: "", | ||
ApiUrl: "https://vercel.com/api", | ||
LoginUrl: "https://vercel.com", | ||
TeamSlug: "", | ||
}, err | ||
return nil, err | ||
} | ||
return ReadConfigFile(path) | ||
return readConfigFile(fsys, path, defaultUserConfig) | ||
} | ||
|
||
// ReadRepoConfigFile reads the user-specific configuration values | ||
func ReadRepoConfigFile(fsys afero.Fs, repoRoot fs.AbsolutePath) (*TurborepoConfig, error) { | ||
path := repoRoot.Join(".turbo", "config.json") | ||
return readConfigFile(fsys, path, defaultRepoConfig) | ||
} | ||
|
||
// DeleteUserConfigFile deletes a user config file | ||
func DeleteUserConfigFile() error { | ||
return WriteUserConfigFile(&TurborepoConfig{}) | ||
func DeleteUserConfigFile(fsys afero.Fs) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a significant improvement, but I'm not sure that we ever actually want this behavior? The call site is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did a little digging, and the only value we use from the user config file is At the risk of pushing a significant change down the road, this area of the codebase might be best handled post- I'm not opposed to splitting out the struct now, but I'm a bit hesitant to tackle any more intelligent configuration merging or rewriting, given that I'm hoping most of this code is not [too] long for this world. |
||
path, err := userConfigPath(fsys) | ||
if err != nil { | ||
return err | ||
} | ||
return fs.RemoveFile(fsys, path) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
package config | ||
gsoltis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
import ( | ||
"encoding/json" | ||
"os" | ||
"testing" | ||
|
||
"github.com/spf13/afero" | ||
"github.com/vercel/turborepo/cli/internal/fs" | ||
) | ||
|
||
func TestReadRepoConfigWhenMissing(t *testing.T) { | ||
fsys := afero.NewMemMapFs() | ||
cwd, err := fs.GetCwd() | ||
if err != nil { | ||
t.Fatalf("getting cwd: %v", err) | ||
} | ||
|
||
config, err := ReadRepoConfigFile(fsys, cwd) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, because of If we ever did a multi-layer FS this sort of weirdness could be an issue. |
||
if err != nil { | ||
t.Errorf("got error reading non-existent config file: %v, want <nil>", err) | ||
} | ||
if config != nil { | ||
t.Errorf("got config value %v, wanted <nil>", config) | ||
} | ||
} | ||
|
||
func writePartialInitialConfig(t *testing.T, fsys afero.Fs, repoRoot fs.AbsolutePath) *TurborepoConfig { | ||
path := repoRoot.Join(".turbo", "config.json") | ||
initial := &TurborepoConfig{ | ||
TeamSlug: "my-team", | ||
} | ||
toWrite, err := json.Marshal(initial) | ||
if err != nil { | ||
t.Fatalf("marshal config: %v", err) | ||
} | ||
err = fs.WriteFile(fsys, path, toWrite, os.ModePerm) | ||
if err != nil { | ||
t.Fatalf("writing config file: %v", err) | ||
} | ||
return initial | ||
} | ||
|
||
func TestRepoConfigIncludesDefaults(t *testing.T) { | ||
fsys := afero.NewMemMapFs() | ||
cwd, err := fs.GetCwd() | ||
if err != nil { | ||
t.Fatalf("getting cwd: %v", err) | ||
} | ||
|
||
initial := writePartialInitialConfig(t, fsys, cwd) | ||
|
||
config, err := ReadRepoConfigFile(fsys, cwd) | ||
if err != nil { | ||
t.Errorf("ReadRepoConfigFile err got %v, want <nil>", err) | ||
} | ||
defaultConfig := defaultRepoConfig() | ||
if config.ApiUrl != defaultConfig.ApiUrl { | ||
t.Errorf("api url got %v, want %v", config.ApiUrl, defaultConfig.ApiUrl) | ||
} | ||
if config.TeamSlug != initial.TeamSlug { | ||
t.Errorf("team slug got %v, want %v", config.TeamSlug, initial.TeamSlug) | ||
} | ||
} | ||
|
||
func TestWriteRepoConfig(t *testing.T) { | ||
fsys := afero.NewMemMapFs() | ||
cwd, err := fs.GetCwd() | ||
if err != nil { | ||
t.Fatalf("getting cwd: %v", err) | ||
} | ||
|
||
initial := &TurborepoConfig{} | ||
initial.TeamSlug = "my-team" | ||
err = WriteRepoConfigFile(fsys, cwd, initial) | ||
if err != nil { | ||
t.Errorf("WriteRepoConfigFile got %v, want <nil>", err) | ||
} | ||
|
||
config, err := ReadRepoConfigFile(fsys, cwd) | ||
if err != nil { | ||
t.Errorf("ReadRepoConfig err got %v, want <nil>", err) | ||
} | ||
if config.TeamSlug != initial.TeamSlug { | ||
t.Errorf("TeamSlug got %v want %v", config.TeamSlug, initial.TeamSlug) | ||
} | ||
defaultConfig := defaultRepoConfig() | ||
if config.ApiUrl != defaultConfig.ApiUrl { | ||
t.Errorf("ApiUrl got %v, want %v", config.ApiUrl, defaultConfig.ApiUrl) | ||
} | ||
} | ||
|
||
func TestReadUserConfigWhenMissing(t *testing.T) { | ||
fsys := afero.NewMemMapFs() | ||
config, err := ReadUserConfigFile(fsys) | ||
if err != nil { | ||
t.Errorf("ReadUserConfig err got %v, want <nil>", err) | ||
} | ||
if config != nil { | ||
t.Errorf("ReadUserConfig on non-existent file got %v, want <nil>", config) | ||
} | ||
} | ||
|
||
func TestWriteUserConfig(t *testing.T) { | ||
fsys := afero.NewMemMapFs() | ||
initial := defaultUserConfig() | ||
initial.Token = "my-token" | ||
initial.ApiUrl = "https://api.vercel.com" // should be overridden | ||
err := WriteUserConfigFile(fsys, initial) | ||
if err != nil { | ||
t.Errorf("WriteUserConfigFile err got %v, want <nil>", err) | ||
} | ||
|
||
config, err := ReadUserConfigFile(fsys) | ||
if err != nil { | ||
t.Errorf("ReadUserConfig err got %v, want <nil>", err) | ||
} | ||
if config.Token != initial.Token { | ||
t.Errorf("Token got %v want %v", config.Token, initial.Token) | ||
} | ||
// Verify that our legacy ApiUrl was upgraded | ||
defaultConfig := defaultUserConfig() | ||
if config.ApiUrl != defaultConfig.ApiUrl { | ||
t.Errorf("ApiUrl got %v, want %v", config.ApiUrl, defaultConfig.ApiUrl) | ||
} | ||
|
||
err = DeleteUserConfigFile(fsys) | ||
if err != nil { | ||
t.Errorf("DeleteUserConfigFile err got %v, want <nil>", err) | ||
} | ||
|
||
missing, err := ReadUserConfigFile(fsys) | ||
if err != nil { | ||
t.Errorf("ReadUserConfig err got %v, want <nil>", err) | ||
} | ||
if missing != nil { | ||
t.Errorf("reading deleted config got %v, want <nil>", missing) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This returns
afero.Fs
, but in places where we will end up needing it we'll need IOFS interfaces andafero.Lstater
which is not implemented byafero.Fs
. I'd pitch diving in and doingfsys := &afero.OsFs{}
for that reason and then wrapping it iniofsys := afero.IOFS{Fs: fsys}
.