Skip to content

Commit

Permalink
New default path for config file (#4301)
Browse files Browse the repository at this point in the history
It changes the default path for the config file from the legacy $HOME/loadimpact/ directory to an owner's agnostic $HOME/k6/.
  • Loading branch information
codebien authored Feb 18, 2025
1 parent fbe7295 commit b0070e7
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 41 deletions.
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")
}

// 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

0 comments on commit b0070e7

Please sign in to comment.