Skip to content
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

New default path for config file #4301

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
50 changes: 26 additions & 24 deletions cmd/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ const defaultConfigFileName = "config.json"
type GlobalState struct {
Ctx context.Context

FS fsext.Fs
Getwd func() (string, error)
BinaryName string
CmdArgs []string
Env map[string]string
Events *event.System
FS fsext.Fs
Getwd func() (string, error)
UserOSConfigDir string
BinaryName string
CmdArgs []string
Env map[string]string
Events *event.System

DefaultFlags, Flags GlobalFlags

Expand Down Expand Up @@ -106,23 +107,24 @@ func NewGlobalState(ctx context.Context) *GlobalState {
defaultFlags := GetDefaultFlags(confDir)

return &GlobalState{
Ctx: ctx,
FS: fsext.NewOsFs(),
Getwd: os.Getwd,
BinaryName: filepath.Base(binary),
CmdArgs: os.Args,
Env: env,
Events: event.NewEventSystem(100, logger),
DefaultFlags: defaultFlags,
Flags: getFlags(defaultFlags, env),
OutMutex: outMutex,
Stdout: stdout,
Stderr: stderr,
Stdin: os.Stdin,
OSExit: os.Exit,
SignalNotify: signal.Notify,
SignalStop: signal.Stop,
Logger: logger,
Ctx: ctx,
FS: fsext.NewOsFs(),
Getwd: os.Getwd,
UserOSConfigDir: confDir,
BinaryName: filepath.Base(binary),
CmdArgs: os.Args,
Env: env,
Events: event.NewEventSystem(100, logger),
DefaultFlags: defaultFlags,
Flags: getFlags(defaultFlags, env),
OutMutex: outMutex,
Stdout: stdout,
Stderr: stderr,
Stdin: os.Stdin,
OSExit: os.Exit,
SignalNotify: signal.Notify,
SignalStop: signal.Stop,
Logger: logger,
FallbackLogger: &logrus.Logger{ // we may modify the other one
Out: stderr,
Formatter: new(logrus.TextFormatter), // no fancy formatting here
Expand All @@ -149,7 +151,7 @@ func GetDefaultFlags(homeDir string) GlobalFlags {
return GlobalFlags{
Address: "localhost:6565",
ProfilingEnabled: false,
ConfigFilePath: filepath.Join(homeDir, "loadimpact", "k6", defaultConfigFileName),
ConfigFilePath: filepath.Join(homeDir, "k6", defaultConfigFileName),
LogOutput: "stderr",
}
}
Expand Down
7 changes: 6 additions & 1 deletion internal/cmd/cloud_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func getCmdCloudLogin(gs *state.GlobalState) *cobra.Command {

# Display the stored token
$ {{.}} cloud login -s

# Reset the stored token
$ {{.}} cloud login -r`[1:])

Expand Down Expand Up @@ -66,6 +66,11 @@ the "k6 run -o cloud" command.

// run is the code that runs when the user executes `k6 cloud login`
func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error {
err := migrateLegacyConfigFileIfAny(c.globalState)
if err != nil {
return err
}

currentDiskConf, err := readDiskConfig(c.globalState)
if err != nil {
return err
Expand Down
125 changes: 113 additions & 12 deletions internal/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func (c Config) Apply(cfg Config) Config {
return c
}

// Returns a Config but only parses the Options inside.
// getPartialConfig returns a Config but only parses the Options inside.
func getPartialConfig(flags *pflag.FlagSet) (Config, error) {
opts, err := getOptions(flags)
if err != nil {
Expand Down Expand Up @@ -124,7 +124,7 @@ func getConfig(flags *pflag.FlagSet) (Config, error) {
}, nil
}

// Reads the configuration file from the supplied filesystem and returns it or
// readDiskConfig reads the configuration file from the supplied filesystem and returns it or
// an error. The only situation in which an error won't be returned is if the
// user didn't explicitly specify a config file path and the default config file
// doesn't exist.
Expand All @@ -151,8 +151,33 @@ func readDiskConfig(gs *state.GlobalState) (Config, error) {
return conf, nil
}

// Serializes the configuration to a JSON file and writes it in the supplied
// location on the supplied filesystem
// legacyConfigFilePath returns the path of the old location,
// which is now deprecated and superseded by a new default.
func legacyConfigFilePath(gs *state.GlobalState) string {
return filepath.Join(gs.UserOSConfigDir, "loadimpact", "k6", "config.json")
}
inancgumus marked this conversation as resolved.
Show resolved Hide resolved

// readLegacyDiskConfig reads the configuration file stored on the old default path.
func readLegacyDiskConfig(gs *state.GlobalState) (Config, error) {
// Check if the legacy config exists in the supplied filesystem
legacyPath := legacyConfigFilePath(gs)
if _, err := gs.FS.Stat(legacyPath); err != nil {
return Config{}, err
}
data, err := fsext.ReadFile(gs.FS, legacyPath)
if err != nil {
return Config{}, fmt.Errorf("couldn't load the configuration from %q: %w", legacyPath, err)
}
var conf Config
err = json.Unmarshal(data, &conf)
if err != nil {
return Config{}, fmt.Errorf("couldn't parse the configuration from %q: %w", legacyPath, err)
}
return conf, nil
}

// writeDiskConfig serializes the configuration to a JSON file and writes it in the supplied
// location on the supplied filesystem.
func writeDiskConfig(gs *state.GlobalState, conf Config) error {
data, err := json.MarshalIndent(conf, "", " ")
if err != nil {
Expand All @@ -166,7 +191,7 @@ func writeDiskConfig(gs *state.GlobalState, conf Config) error {
return fsext.WriteFile(gs.FS, gs.Flags.ConfigFilePath, data, 0o644)
}

// Reads configuration variables from the environment.
// readEnvConfig reads configuration variables from the environment.
func readEnvConfig(envMap map[string]string) (Config, error) {
// TODO: replace envconfig and refactor the whole configuration from the ground up :/
conf := Config{}
Expand All @@ -177,7 +202,41 @@ func readEnvConfig(envMap map[string]string) (Config, error) {
return conf, err
}

// Assemble the final consolidated configuration from all of the different sources:
// loadConfigFile wraps the ordinary readDiskConfig operation.
// It adds the capability to fallbacks on the legacy default path if required.
//
// Unfortunately, readDiskConfig() silences the NotFound error.
// We don't want to change it as it is used across several places;
// and, hopefully, this code will be available only for a single major version.
// After we should restore to lookup only in a single location for config file (the default).
func loadConfigFile(gs *state.GlobalState) (Config, error) {
// use directly the main flow if the user passed a custom path
if gs.Flags.ConfigFilePath != gs.DefaultFlags.ConfigFilePath {
return readDiskConfig(gs)
}

_, err := gs.FS.Stat(gs.Flags.ConfigFilePath)
if err != nil && errors.Is(err, fs.ErrNotExist) {
// if the passed path (the default) does not exist
// then we attempt to load the legacy path
legacyConf, legacyErr := readLegacyDiskConfig(gs)
if legacyErr != nil && !errors.Is(legacyErr, fs.ErrNotExist) {
return Config{}, legacyErr
}
// a legacy file has been found
if legacyErr == nil {
gs.Logger.Warnf("The configuration file has been found on the old default path (%q). "+
"Please, run again `k6 cloud login` or `k6 login` commands to migrate to the new default path.\n\n",
legacyConfigFilePath(gs))
return legacyConf, nil
}
// the legacy file doesn't exist, then we fallback on the main flow
// to return the silenced error for not existing config file
}
return readDiskConfig(gs)
}

// getConsolidatedConfig assemble the final consolidated configuration from all of the different sources:
// - start with the CLI-provided options to get shadowed (non-Valid) defaults in there
// - add the global file config options
// - add the Runner-provided options (they may come from Bundle too if applicable)
Expand All @@ -186,17 +245,19 @@ func readEnvConfig(envMap map[string]string) (Config, error) {
// - set some defaults if they weren't previously specified
// TODO: add better validation, more explicit default values and improve consistency between formats
// TODO: accumulate all errors and differentiate between the layers?
func getConsolidatedConfig(gs *state.GlobalState, cliConf Config, runnerOpts lib.Options) (conf Config, err error) {
fileConf, err := readDiskConfig(gs)
func getConsolidatedConfig(gs *state.GlobalState, cliConf Config, runnerOpts lib.Options) (Config, error) {
fileConf, err := loadConfigFile(gs)
if err != nil {
return conf, errext.WithExitCodeIfNone(err, exitcodes.InvalidConfig)
err = fmt.Errorf("failed to load the configuration file from the local file system: %w", err)
return Config{}, errext.WithExitCodeIfNone(err, exitcodes.InvalidConfig)
}

envConf, err := readEnvConfig(gs.Env)
if err != nil {
return conf, errext.WithExitCodeIfNone(err, exitcodes.InvalidConfig)
return Config{}, errext.WithExitCodeIfNone(err, exitcodes.InvalidConfig)
}

conf = cliConf.Apply(fileConf)
conf := cliConf.Apply(fileConf)

warnOnShortHandOverride(conf.Options, runnerOpts, "script", gs.Logger)
conf = conf.Apply(Config{Options: runnerOpts})
Expand All @@ -215,7 +276,7 @@ func getConsolidatedConfig(gs *state.GlobalState, cliConf Config, runnerOpts lib
// (e.g. env vars) overrode our default value. This is not done in
// lib.Options.Validate to avoid circular imports.
if _, err = metrics.GetResolversForTrendColumns(conf.SummaryTrendStats); err != nil {
return conf, err
return Config{}, err
}

return conf, nil
Expand Down Expand Up @@ -303,3 +364,43 @@ func validateScenarioConfig(conf lib.ExecutorConfig, isExecutable func(string) b
}
return nil
}

// migrateLegacyConfigFileIfAny copies the configuration file from
// the old default `~/.config/loadimpact/...` folder
// to the new `~/.config/k6/...` default folder.
// If the old file is not found no error is returned.
// It keeps the old file as a backup.
func migrateLegacyConfigFileIfAny(gs *state.GlobalState) error {
fn := func() error {
legacyFpath := legacyConfigFilePath(gs)
_, err := gs.FS.Stat(legacyFpath)
if errors.Is(err, fs.ErrNotExist) {
return nil
}
if err != nil {
return err
}
newPath := gs.DefaultFlags.ConfigFilePath
if err := gs.FS.MkdirAll(filepath.Dir(newPath), 0o755); err != nil {
return err
}
// copy the config file leaving the old available as a backup
f, err := fsext.ReadFile(gs.FS, legacyFpath)
if err != nil {
return err
}
err = fsext.WriteFile(gs.FS, newPath, f, 0o644)
if err != nil {
return err
}
gs.Logger.Infof("Note, the configuration file has been migrated "+
"from the old default path (%q) to the new one (%q). "+
"Clean up the old path after you verified that you can run tests by using the new configuration.\n\n",
legacyFpath, newPath)
return nil
}
if err := fn(); err != nil {
return fmt.Errorf("move from the old to the new configuration's filepath failed: %w", err)
}
return nil
}
Loading
Loading