-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
…Fs and fs.AbsolutePath
3419aec
to
5a89151
Compare
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 is an important step toward using afero
everywhere, I can't wait to get it landed.
@@ -51,6 +52,7 @@ func main() { | |||
|
|||
ui := ui.BuildColoredUi(colorMode) | |||
c := cli.NewCLI("turbo", turboVersion) | |||
fsys := afero.NewOsFs() |
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 and afero.Lstater
which is not implemented by afero.Fs
. I'd pitch diving in and doing fsys := &afero.OsFs{}
for that reason and then wrapping it in iofsys := afero.IOFS{Fs: fsys}
.
// 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 comment
The reason will be displayed to describe this comment to others. Learn more.
fs.AbsolutePath
🎉
This looks like it clobbers whatever is there? Should we make this a mergeConfigFile
that does a get-set
? (Most particularly for ApiUrl
and LoginUrl
, things that may be hand-tweaked.)
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 comment
The 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.
// 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 comment
The reason will be displayed to describe this comment to others. Learn more.
xdg.ConfigFile
creates the directory as a side effect: https://github.com/adrg/xdg/blob/v0.4.0/internal/pathutil/pathutil.go#L51
And the Exists
call directly calls os.Stat
or os.Lstat
(depending upon the implementation)
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 comment
The 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 comment
The 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.
} | ||
|
||
// 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 comment
The 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 logout
so I feel like we should remove the config for logging-in only.
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.
I did a little digging, and the only value we use from the user config file is Token
. I think what we ought to do is stop reusing the same struct to represent both configs.
At the risk of pushing a significant change down the road, this area of the codebase might be best handled post-cobra
migration when we bring in viper
, which is built on cobra
, to help us resolve our configuration situation a bit better.
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.
} | ||
|
||
// WriteFile writes the given bytes to the specified file | ||
func WriteFile(fs afero.Fs, filename AbsolutePath, toWrite []byte, mode os.FileMode) error { |
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.
Can we switch all of these to be afero.IOFS
? At that point we can have a common interface of return fs.MethodName...
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.
Yes, since IOFS
wraps Fs
, but IOFS
is read-only because io/fs.FS
is read-only. So the functions that do writing won't exactly fit the pattern.
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.
I started doing this, but ran into some issues. afero.ReadFile
has different behavior from myIOFS.ReadFile()
regarding the leading slash, due to the call to ValidPath
here.
It looks like Go's io/fs.FS
does not allow absolute paths, and further digging shows that io/fs.FS
expects non-absolute, platform-independent paths (e.g. ToSlash()
has been called). So it is not a candidate for a direct pass-through from our current implementation of AbsolutePath
, which is a platform-dependent absolute path.
I can think of a few options:
- Store
fs.AbsolutePath
in a manner consistent withio/fs.FS
. I think this would be fine w/ 100% coverage of our filesystem operations, which is the goal anyways - do conversions when using an instance of
io/fs.FS
. This might be ok, but it makes me nervous. - I don't think it's practical to avoid
io/fs.FS
? At least not without changes todoublestar
? It looks like it's a little bit of an accident that our current usage ofdoublestar
isn't triggering this, it's based on all of the instances ofio/fs.FS
that we've passed in so far also implementio/fs.ReadDir
, which short-circuits the call toOpen()
, which would fail.
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 was the one that I was hoping would work out and ... lol.
Seems like the best ... path ... forward here will be to make this sort of change after we've got path types through the entire system. We've got a large number of path types that we need; I'mma get a PR up.
ui: c.UI, | ||
logger: c.Config.Logger, | ||
fsys: c.Config.Fs, | ||
repoRoot: c.Config.Cwd, |
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.
That's a super-confusing remapping. We should probably not call it cwd
if it's actually the repo root.
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.
Heh. It's actually more confusing than it looks. We have potentially 3 paths in play:
- The actual working directory of the
turbo
process - The directory that contains
turbo.json
-> the "root" of the monorepo - The directory that contains the
.git
folder
None of them are guaranteed to be the same. We most often care about the second in the list. The .git
location is mostly handled by scm
, but we need to be careful when using git
for hashing. And of course the actual working directory is implied whenever we forget to use an absolute path. cwd
is therefore likely the worst name for the field, and is an artifact from the fact that the flag name is --cwd
, with the assumption that the working directory should be the directory that contains turbo.json
.
I'm going to suggest:
cwd
->turboRoot
across the codebase, with a comment onConfig
explaining what it is (directory containingturbo.json
)- do it in a separate PR
@@ -57,6 +60,8 @@ func getCmd(config *config.Config, ui cli.Ui) *cobra.Command { | |||
link := &link{ | |||
ui: ui, | |||
logger: config.Logger, | |||
fsys: config.Fs, | |||
cwd: config.Cwd, |
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.
Maybe we change the config to split into two fields and populate them with the same value during the transition?
(These things are not the same, even if we're treating them as the same now.)
@@ -36,7 +36,7 @@ Usage: turbo unlink | |||
|
|||
// Run executes tasks in the monorepo | |||
func (c *UnlinkCommand) Run(args []string) int { | |||
if err := config.WriteRepoConfigFile(&config.TurborepoConfig{}); err != nil { | |||
if err := config.WriteRepoConfigFile(c.Config.Fs, c.Config.Cwd, &config.TurborepoConfig{}); err != nil { |
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 is an example of why this property is super-confusing.
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.
Worth mentioning, I'm really really trying to avoid letting perfect be the enemy of good here. This is strictly an improvement and I don't see anything that should truly stop this from landing.
None of the open comments have anything to do with the implementation at present; they're primarily about additional refactoring options. That should not block this clear improvement from landing so that we're able to make continuous incremental improvements.
Resolving merge conflicts dismissed reviews... |
As noted in #1156 (comment), `xdg.ConfigFile` has side effects. One of the consequences of the side effects is that sometimes, when simply wanting to check for existence, we would error out entirely because the path wasn't writable. This splits the process of creating and reading a config file path, which means that the error won't be encountered unless a user is explicitly trying to create a configuration file. (Which *should* be an error.) Fixes #1270
afero.Fs
toConfig
fs.AbsolutePath
andafero.Fs
for reading and writing repo-specific config and user-specific configlogin
, now that we can useafero.Fs
for testsfs.AbsolutePath
andafero.Fs
Possible next step: move
config_file.go
to its own package.