Skip to content

Commit

Permalink
Start plumbing through afero.Fs
Browse files Browse the repository at this point in the history
  • Loading branch information
gsoltis committed Apr 29, 2022
1 parent ef28083 commit 3419aec
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 103 deletions.
4 changes: 3 additions & 1 deletion cli/cmd/turbo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"
"time"

"github.com/spf13/afero"
"github.com/vercel/turborepo/cli/internal/config"
"github.com/vercel/turborepo/cli/internal/info"
"github.com/vercel/turborepo/cli/internal/login"
Expand Down Expand Up @@ -51,6 +52,7 @@ func main() {

ui := ui.BuildColoredUi(colorMode)
c := cli.NewCLI("turbo", turboVersion)
fsys := afero.NewOsFs()

util.InitPrintf()

Expand All @@ -59,7 +61,7 @@ func main() {
c.ErrorWriter = os.Stderr
// Parse and validate cmd line flags and env vars
// Note that cf can be nil
cf, err := config.ParseAndValidate(c.Args, ui, turboVersion)
cf, err := config.ParseAndValidate(c.Args, fsys, ui, turboVersion)
if err != nil {
ui.Error(fmt.Sprintf("%s %s", uiPkg.ERROR_PREFIX, color.RedString(err.Error())))
os.Exit(1)
Expand Down
20 changes: 16 additions & 4 deletions cli/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ type CacheConfig struct {
// ParseAndValidate parses the cmd line flags / env vars, and verifies that all required
// flags have been set. Users can pass in flags when calling a subcommand, or set env vars
// with the prefix 'TURBO_'. If both values are set, the env var value will be used.
func ParseAndValidate(args []string, ui cli.Ui, turboVersion string) (c *Config, err error) {
func ParseAndValidate(args []string, fsys afero.Fs, ui cli.Ui, turboVersion string) (c *Config, err error) {

// Special check for ./turbo invocation without any args
// Return the help message
Expand Down Expand Up @@ -111,8 +111,20 @@ func ParseAndValidate(args []string, ui cli.Ui, turboVersion string) (c *Config,
if err != nil {
return nil, err
}
userConfig, _ := ReadUserConfigFile()
partialConfig, _ := ReadConfigFile(filepath.Join(".turbo", "config.json"))
userConfig, err := ReadUserConfigFile(fsys)
if err != nil {
return nil, fmt.Errorf("reading user config file: %v", err)
}
if userConfig == nil {
userConfig = defaultUserConfig()
}
partialConfig, err := ReadRepoConfigFile(fsys, cwd)
if err != nil {
return nil, fmt.Errorf("reading repo config file: %v", err)
}
if partialConfig == nil {
partialConfig = defaultRepoConfig()
}
partialConfig.Token = userConfig.Token

enverr := envconfig.Process("TURBO", partialConfig)
Expand Down Expand Up @@ -214,7 +226,7 @@ func ParseAndValidate(args []string, ui cli.Ui, turboVersion string) (c *Config,
RootPackageJSON: rootPackageJSON,
TurboConfigJSON: turboConfigJson,
Cwd: cwd,
Fs: afero.NewOsFs(),
Fs: fsys,
}

c.ApiClient.SetToken(partialConfig.Token)
Expand Down
93 changes: 47 additions & 46 deletions cli/internal/config/config_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package config

import (
"encoding/json"
"io/ioutil"
"errors"
"os"
"path/filepath"

"github.com/adrg/xdg"
Expand All @@ -24,6 +25,20 @@ 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(fsys afero.Fs, path fs.AbsolutePath, config *TurborepoConfig) error {
jsonBytes, marshallError := json.Marshal(config)
Expand All @@ -37,14 +52,6 @@ func writeConfigFile(fsys afero.Fs, path fs.AbsolutePath, config *TurborepoConfi
return nil
}

// WriteRepoConfigFileToMigrate is used to write the portion of the config file that is saved
// within the repository itself.
func WriteRepoConfigFileToMigrate(config *TurborepoConfig) error {
fs.EnsureDir(filepath.Join(".turbo", "config.json"))
path := filepath.Join(".turbo", "config.json")
return writeConfigFile(path, config)
}

// WriteRepoConfigFile is used to write the portion of the config file that is saved
// within the repository itself.
func WriteRepoConfigFile(fsys afero.Fs, repoRoot fs.AbsolutePath, toWrite *TurborepoConfig) error {
Expand All @@ -56,44 +63,39 @@ func WriteRepoConfigFile(fsys afero.Fs, repoRoot fs.AbsolutePath, toWrite *Turbo
return writeConfigFile(fsys, path, toWrite)
}

// WriteUserConfigFileToMigrate 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 WriteUserConfigFileToMigrate(config *TurborepoConfig) error {
func userConfigPath(fsys afero.Fs) (fs.AbsolutePath, error) {
path, err := xdg.ConfigFile(filepath.Join("turborepo", "config.json"))
if err != nil {
return err
return "", err
}
absPath, err := fs.CheckedToAbsolutePath(path)
if err != nil {
return "", err
}
return writeConfigFile(path, config)
return absPath, nil
}

func WriteUserConfigFile(fsys afero.Fs, config *TurborepoConfig) error {
path, err := xdg.ConfigFile(filepath.Join("turborepo", "config.json"))
if err != nil {
return err
}
absPath, err := fs.CheckedToAbsolutePath(path)
path, err := userConfigPath(fsys)
if err != nil {
return err
}
return writeConfigFile(fsys, absPath, config)
return writeConfigFile(fsys, path, config)
}

// 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: "",
}
b, err := ioutil.ReadFile(path)
// 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" {
config.ApiUrl = "https://vercel.com/api"
Expand All @@ -102,25 +104,24 @@ 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)
}

// DeleteUserConfigFileToMigrate deletes a user config file
func DeleteUserConfigFileToMigrate() error {
return WriteUserConfigFileToMigrate(&TurborepoConfig{})
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(fsys afero.Fs) error {
return WriteUserConfigFileToMigrate(&TurborepoConfig{})
path, err := userConfigPath(fsys)
if err != nil {
return err
}
return fs.RemoveFile(fsys, path)
}
139 changes: 139 additions & 0 deletions cli/internal/config/config_file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package config

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)
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)
}
}
14 changes: 8 additions & 6 deletions cli/internal/fs/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,10 @@ func (ap AbsolutePath) Dir() AbsolutePath {
func (ap AbsolutePath) MkdirAll() error {
return os.MkdirAll(ap.asString(), DirPermissions)
}
func (ap AbsolutePath) Remove() error {
return os.Remove(ap.asString())
}
func (ap AbsolutePath) Open() (*os.File, error) {
return os.Open(ap.asString())
}

// func (ap AbsolutePath) ReadFile() ([]byte, error) {
// return ioutil.ReadFile(ap.asString())
// }
func (ap AbsolutePath) FileExists() bool {
return FileExists(ap.asString())
}
Expand All @@ -88,3 +82,11 @@ func EnsureDirFS(fs afero.Fs, filename AbsolutePath) error {
func WriteFile(fs afero.Fs, filename AbsolutePath, toWrite []byte, mode os.FileMode) error {
return afero.WriteFile(fs, filename.asString(), toWrite, mode)
}

func ReadFile(fs afero.Fs, filename AbsolutePath) ([]byte, error) {
return afero.ReadFile(fs, filename.asString())
}

func RemoveFile(fs afero.Fs, filename AbsolutePath) error {
return fs.Remove(filename.asString())
}
Loading

0 comments on commit 3419aec

Please sign in to comment.