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 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
105 changes: 99 additions & 6 deletions internal/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,30 @@ func readDiskConfig(gs *state.GlobalState) (Config, error) {
return conf, nil
}

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

func readLegacyDiskConfig(gs *state.GlobalState) (Config, error) {
// CHeck if the legacy config exists in the supplied filesystem
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// CHeck if the legacy config exists in the supplied filesystem
// 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
}

// 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 {
Expand All @@ -177,6 +201,40 @@ func readEnvConfig(envMap map[string]string) (Config, error) {
return conf, err
}

// 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 does not exist (custom one or the default)
// 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.Warn("The configuration file has been found on the old path. " +
"Please, run again `k6 cloud login` or `k6 login` commands to migrate to the new path. " +
"If you already migrated it manually, then remove the file from the old path.\n\n")
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)
}

// 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
Expand All @@ -186,17 +244,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 +275,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 +363,36 @@ func validateScenarioConfig(conf lib.ExecutorConfig, isExecutable func(string) b
}
return nil
}

// migrateLegacyConfigFileIfAny moves the configuration file from
// the old default `~/.config/loadimpact/...` folder
// to the new `~/.config/k6/...` default folder.
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
}

if err := gs.FS.MkdirAll(filepath.Dir(gs.Flags.ConfigFilePath), 0o755); err != nil {
return err
}

err = gs.FS.Rename(legacyFpath, gs.Flags.ConfigFilePath)
if err != nil {
return err
}

gs.Logger.Infof("Note, the configuration file has been migrated "+
"from old default path (%q) to the new version (%q).\n\n", legacyFpath, gs.Flags.ConfigFilePath)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"from old default path (%q) to the new version (%q).\n\n", legacyFpath, gs.Flags.ConfigFilePath)
"from the old default path (%q) to the new one (%q).\n\n", legacyFpath, gs.Flags.ConfigFilePath)

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
}
57 changes: 53 additions & 4 deletions internal/cmd/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package cmd

import (
"encoding/json"
"io"
"io/fs"
"testing"
"time"

"github.com/mstoykov/envconfig"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/guregu/null.v3"
Expand Down Expand Up @@ -213,7 +215,7 @@ func TestReadDiskConfigWithDefaultFlags(t *testing.T) {
memfs := fsext.NewMemMapFs()

conf := []byte(`{"iterations":1028,"cloud":{"field1":"testvalue"}}`)
defaultConfigPath := ".config/loadimpact/k6/config.json"
defaultConfigPath := ".config/k6/config.json"
require.NoError(t, fsext.WriteFile(memfs, defaultConfigPath, conf, 0o644))

defaultFlags := state.GetDefaultFlags(".config")
Expand Down Expand Up @@ -298,7 +300,7 @@ func TestReadDiskConfigNotJSONContentError(t *testing.T) {
memfs := fsext.NewMemMapFs()

conf := []byte(`bad json format`)
defaultConfigPath := ".config/loadimpact/k6/config.json"
defaultConfigPath := ".config/k6/config.json"
require.NoError(t, fsext.WriteFile(memfs, defaultConfigPath, conf, 0o644))

gs := &state.GlobalState{
Expand Down Expand Up @@ -342,7 +344,7 @@ func TestWriteDiskConfigWithDefaultFlags(t *testing.T) {
err := writeDiskConfig(gs, c)
require.NoError(t, err)

finfo, err := memfs.Stat(".config/loadimpact/k6/config.json")
finfo, err := memfs.Stat(".config/k6/config.json")
require.NoError(t, err)
assert.NotEmpty(t, finfo.Size())
}
Expand All @@ -352,7 +354,7 @@ func TestWriteDiskConfigOverwrite(t *testing.T) {
memfs := fsext.NewMemMapFs()

conf := []byte(`{"iterations":1028,"cloud":{"field1":"testvalue"}}`)
defaultConfigPath := ".config/loadimpact/k6/config.json"
defaultConfigPath := ".config/k6/config.json"
require.NoError(t, fsext.WriteFile(memfs, defaultConfigPath, conf, 0o644))

defaultFlags := state.GetDefaultFlags(".config")
Expand Down Expand Up @@ -405,3 +407,50 @@ func TestWriteDiskConfigNoJSONContentError(t *testing.T) {
var serr *json.SyntaxError
assert.ErrorAs(t, err, &serr)
}

func TestMigrateLegacyConfigFileIfAny(t *testing.T) {
t.Parallel()
memfs := fsext.NewMemMapFs()

conf := []byte(`{"iterations":1028,"cloud":{"field1":"testvalue"}}`)
legacyConfigPath := ".config/loadimpact/k6/config.json"
require.NoError(t, fsext.WriteFile(memfs, legacyConfigPath, conf, 0o644))

logger := logrus.New()
logger.SetOutput(io.Discard)

defaultFlags := state.GetDefaultFlags(".config")
gs := &state.GlobalState{
FS: memfs,
Flags: defaultFlags,
DefaultFlags: defaultFlags,
UserOSConfigDir: ".config",
Logger: logger,
}

err := migrateLegacyConfigFileIfAny(gs)
require.NoError(t, err)

f, err := fsext.ReadFile(memfs, ".config/k6/config.json")
require.NoError(t, err)
assert.Equal(t, f, conf)

_, err = memfs.Stat(legacyConfigPath)
assert.ErrorIs(t, err, fs.ErrNotExist)
}

func TestMigrateLegacyConfigFileIfAnyWhenFileDoesNotExist(t *testing.T) {
t.Parallel()
memfs := fsext.NewMemMapFs()

defaultFlags := state.GetDefaultFlags(".config")
gs := &state.GlobalState{
FS: memfs,
Flags: defaultFlags,
DefaultFlags: defaultFlags,
UserOSConfigDir: ".config",
}

err := migrateLegacyConfigFileIfAny(gs)
require.NoError(t, err)
}
4 changes: 4 additions & 0 deletions internal/cmd/login_cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ This will set the default token used when just "k6 run -o cloud" is passed.`,
Please use the "k6 cloud login" command instead.
`,
RunE: func(cmd *cobra.Command, _ []string) error {
if err := migrateLegacyConfigFileIfAny(gs); err != nil {
return err
}

currentDiskConf, err := readDiskConfig(gs)
if err != nil {
return err
Expand Down
4 changes: 4 additions & 0 deletions internal/cmd/login_influxdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ func getCmdLoginInfluxDB(gs *state.GlobalState) *cobra.Command {
This will set the default server used when just "-o influxdb" is passed.`,
Args: cobra.MaximumNArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
if err := migrateLegacyConfigFileIfAny(gs); err != nil {
return err
}

config, err := readDiskConfig(gs)
if err != nil {
return err
Expand Down
Loading