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

use dataDir/config.yaml as the default config path #4466

Merged
merged 6 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
67 changes: 43 additions & 24 deletions cmd/cli/config/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/spf13/viper"

"github.com/bacalhau-project/bacalhau/cmd/util"
"github.com/bacalhau-project/bacalhau/cmd/util/flags/cliflags"
"github.com/bacalhau-project/bacalhau/cmd/util/hook"
"github.com/bacalhau-project/bacalhau/pkg/config"
"github.com/bacalhau-project/bacalhau/pkg/config/types"
Expand All @@ -26,40 +27,58 @@ func newSetCmd() *cobra.Command {
PostRunE: hook.ClientPostRunHooks,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
// initialize a new or open an existing repo. We need to ensure a repo
// exists before we can create or modify a config file in it.
_, err := util.SetupRepoConfig(cmd)
// Get the value of the --config flag
configFlag, err := cmd.Flags().GetString("config")
if err != nil {
return fmt.Errorf("failed to setup repo: %w", err)
return fmt.Errorf("failed to get config flag: %w", err)
}
return setConfig(cmd.PersistentFlags().Lookup("config").Value.String(), args[0], args[1:]...)
},
// provide auto completion for arguments to the `set` command
ValidArgsFunction: setAutoComplete,
}

bacalhauCfgDir := "bacalhau"
bacalhauCfgFile := config.DefaultFileName
// If --config flag is set, use it to set the Viper key
if configFlag != "" {
viper.Set(cliflags.RootCommandConfigFiles, []string{configFlag})
}

usrCfgDir, err := os.UserConfigDir()
if err != nil {
log.Warn().Err(err).Msg("Failed to find user-specific configuration directory. Using current directory to write config.")
} else {
bacalhauCfgDir = filepath.Join(usrCfgDir, bacalhauCfgDir)
if err := os.MkdirAll(bacalhauCfgDir, util2.OS_USER_RWX); err != nil {
// This means we failed to create a directory either in the current directory, or the user config dir
// indicating a some-what serious misconfiguration of the system. We panic here to provide as much
// detail as possible.
log.Panic().Err(err).Msgf("Failed to create bacalhau configuration directory: %s", bacalhauCfgDir)
}
bacalhauCfgFile = filepath.Join(bacalhauCfgDir, bacalhauCfgFile)
var configPath string
// load configs to get the config file path
rawConfig, err := util.SetupConfigType(cmd)
if err != nil {
return fmt.Errorf("failed to setup config: %w", err)
}

configPath = rawConfig.ConfigFileUsed()
if configPath == "" {
// we fall back to the default config file path $BACALHAU_DIR/config.yaml
// this requires initializing a new or opening an existing data-dir
bacalhauConfig, err := util.DecodeBacalhauConfig(rawConfig)
if err != nil {
return fmt.Errorf("failed to decode bacalhau config: %w", err)
}
_, err = util.SetupRepo(bacalhauConfig)
if err != nil {
return fmt.Errorf("failed to setup repo: %w", err)
}
configPath = filepath.Join(bacalhauConfig.DataDir, config.DefaultFileName)

// create the config file if it doesn't exist
if _, err := os.Stat(configPath); os.IsNotExist(err) {
if err := os.WriteFile(configPath, []byte{}, util2.OS_USER_RWX); err != nil {
return fmt.Errorf("failed to create default config file %s: %w", configPath, err)
}
}
}

return setConfig(configPath, args[0], args[1:]...)
},
// Provide auto completion for arguments to the `set` command
ValidArgsFunction: setAutoComplete,
}

setCmd.PersistentFlags().String("config", bacalhauCfgFile, "Path to the config file")
setCmd.PersistentFlags().String("config", "", "Path to the config file (default is $BACALHAU_DIR/config.yaml)")
return setCmd
Copy link
Member

Choose a reason for hiding this comment

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

This can all be simplified a fair bit. setConfig will create the file if it doesn't already exist so no need to create one here.

Suggested change
RunE: func(cmd *cobra.Command, args []string) error {
// initialize a new or open an existing repo. We need to ensure a repo
// exists before we can create or modify a config file in it.
_, err := util.SetupRepoConfig(cmd)
// Get the value of the --config flag
configFlag, err := cmd.Flags().GetString("config")
if err != nil {
return fmt.Errorf("failed to setup repo: %w", err)
return fmt.Errorf("failed to get config flag: %w", err)
}
return setConfig(cmd.PersistentFlags().Lookup("config").Value.String(), args[0], args[1:]...)
},
// provide auto completion for arguments to the `set` command
ValidArgsFunction: setAutoComplete,
}
bacalhauCfgDir := "bacalhau"
bacalhauCfgFile := config.DefaultFileName
// If --config flag is set, use it to set the Viper key
if configFlag != "" {
viper.Set(cliflags.RootCommandConfigFiles, []string{configFlag})
}
usrCfgDir, err := os.UserConfigDir()
if err != nil {
log.Warn().Err(err).Msg("Failed to find user-specific configuration directory. Using current directory to write config.")
} else {
bacalhauCfgDir = filepath.Join(usrCfgDir, bacalhauCfgDir)
if err := os.MkdirAll(bacalhauCfgDir, util2.OS_USER_RWX); err != nil {
// This means we failed to create a directory either in the current directory, or the user config dir
// indicating a some-what serious misconfiguration of the system. We panic here to provide as much
// detail as possible.
log.Panic().Err(err).Msgf("Failed to create bacalhau configuration directory: %s", bacalhauCfgDir)
}
bacalhauCfgFile = filepath.Join(bacalhauCfgDir, bacalhauCfgFile)
var configPath string
// load configs to get the config file path
rawConfig, err := util.SetupConfigType(cmd)
if err != nil {
return fmt.Errorf("failed to setup config: %w", err)
}
configPath = rawConfig.ConfigFileUsed()
if configPath == "" {
// we fall back to the default config file path $BACALHAU_DIR/config.yaml
// this requires initializing a new or opening an existing data-dir
bacalhauConfig, err := util.DecodeBacalhauConfig(rawConfig)
if err != nil {
return fmt.Errorf("failed to decode bacalhau config: %w", err)
}
_, err = util.SetupRepo(bacalhauConfig)
if err != nil {
return fmt.Errorf("failed to setup repo: %w", err)
}
configPath = filepath.Join(bacalhauConfig.DataDir, config.DefaultFileName)
// create the config file if it doesn't exist
if _, err := os.Stat(configPath); os.IsNotExist(err) {
if err := os.WriteFile(configPath, []byte{}, util2.OS_USER_RWX); err != nil {
return fmt.Errorf("failed to create default config file %s: %w", configPath, err)
}
}
}
return setConfig(configPath, args[0], args[1:]...)
},
// Provide auto completion for arguments to the `set` command
ValidArgsFunction: setAutoComplete,
}
setCmd.PersistentFlags().String("config", bacalhauCfgFile, "Path to the config file")
setCmd.PersistentFlags().String("config", "", "Path to the config file (default is $BACALHAU_DIR/config.yaml)")
return setCmd
// initialize a new or open an existing repo. We need to ensure a repo
// exists before we can create or modify a config file in it.
cfg, err := util.SetupRepoConfig(cmd)
if err != nil {
return fmt.Errorf("failed to setup repo: %w", err)
}
cfgFile := cmd.PersistentFlags().Lookup("config").Value.String()
if cfgFile == "" {
cfgFile = filepath.Join(cfg.DataDir, config.DefaultFileName)
}
return setConfig(cfgFile, args[0], args[1:]...)
},
// provide auto completion for arguments to the `set` command
ValidArgsFunction: setAutoComplete,
}
setCmd.PersistentFlags().String("config", "", "Path to the config file")
return setCmd

Copy link
Member Author

Choose a reason for hiding this comment

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

few issues with this approach:

  • a repo is going to be initialized and created unnecessarily even if the user define a config path. The repo can be well different than the one in user's config file as this --config flag is different than the one SetupRepoConfig() is basing on
  • we will create a config instance and log that a config was initialize using paths that can be different from one provided by the user as we are calling SetupRepoConfig() before parsing --config
  • setConfig is not creating the config file for us, and we don't want it to. We are only creating a config file if we are using the default path where we are creating config.yaml under the repo. Otherwise if the user provided a config file that doesn't exist, then we better fail instead of creating it to avoid undesired behavour with typos

Here are examples of this behaviour:

# config file with datadir setup
→ cat bacalhau_config.yaml
datadir: /Users/walid/test/config

# set a new entry in that config, you will see that repo is getting initialized
# at the default location instead of the one in the config file
→ bacalhau config set api.host 2.2.2.2 --config bacalhau_config.yaml
21:49:11.858 | INF ../../workspace/bacalhau/pkg/config/config.go:211 > Config loaded from: [], and with data-dir /Users/walid/.bacalhau
21:49:11.859 | INF ../../workspace/bacalhau/pkg/repo/fs.go:91 > Initializing repo at /Users/walid/.bacalhau
21:49:12.018 | INF ../../workspace/bacalhau/cmd/cli/config/set.go:49 > Writing config to bacalhau_config.yaml

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, you're right, the creation of the repo should happen on if a flag isn't provided:

func newSetCmd() *cobra.Command {
	setCmd := &cobra.Command{
		Use:          "set",
		Args:         cobra.MinimumNArgs(2),
		Short:        "Set a value in the config.",
		PreRunE:      hook.ClientPreRunHooks,
		PostRunE:     hook.ClientPostRunHooks,
		SilenceUsage: true,
		RunE: func(cmd *cobra.Command, args []string) error {
			cfgFile := cmd.PersistentFlags().Lookup("config").Value.String()
			if cfgFile == "" {
				// if the user didn't provide a flag, then ensure a repo is initialize and write/create file there.
				cfg, err := util.SetupRepoConfig(cmd)
				if err != nil {
					return err
				}
				cfgFile = filepath.Join(cfg.DataDir, config.DefaultFileName)
			}
			return setConfig(cfgFile, args[0], args[1:]...)
		},
		// provide auto completion for arguments to the `set` command
		ValidArgsFunction: setAutoComplete,
	}

	setCmd.PersistentFlags().String("config", "", "Path to the config file")
	return setCmd
}

func setConfig(cfgFilePath, key string, value ...string) error {
	v := viper.New()
	v.SetConfigFile(cfgFilePath)
	if err := v.ReadInConfig(); err != nil {
		if !os.IsNotExist(err) {
			return err
		}
	}
	parsed, err := types.CastConfigValueForKey(key, value)
	if err != nil {
		return err
	}
	v.Set(key, parsed)
	if err := v.WriteConfigAs(cfgFilePath); err != nil {
		return err
	}

	return nil
}

Copy link
Member

Choose a reason for hiding this comment

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

As far as 21:49:11.858 | INF ../../workspace/bacalhau/pkg/config/config.go:211 > Config loaded from: [], and with data-dir /Users/walid/.bacalhau

I think we should only produce this log line when we have loaded a config from a file

Copy link
Member Author

Choose a reason for hiding this comment

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

First, I don't like that we are forking how config path is resolved in set command compared to others. I tried to minimize the fork by still calling SetupConfig the same as how all other methods do. If in the future we introduce a new logic on how config path is resolved, such as introducing an env variable, we wont fall in bugs where we miss to update the code here as well. It is getting late here. I have an idea to reuse the same config flags that I'll push tomorrow.

Second, we still have to create the config file if it is coming from the repo

Last, I have to disagree about the logging. Even if configs are not loaded from a file because we couldn't fine one or the user didn't provide one, we should still log that to help them understand what is going on

Copy link
Member

Choose a reason for hiding this comment

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

Okay, my goal with these comments is to get use to simplify things, my approval is sticky, so merge when you are satisfied.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've made changes here to make sure we are using the same --config flag for all commands, both for reading and writing it. Config initialization is now as unified as it can get using util.SetupConfig() that relies on that flag

b985723

}

func setConfig(cfgFilePath, key string, value ...string) error {
log.Info().Msgf("Writing config to %s", cfgFilePath)
wdbaruni marked this conversation as resolved.
Show resolved Hide resolved
v := viper.New()
v.SetConfigFile(cfgFilePath)
if err := v.ReadInConfig(); err != nil {
Expand Down
119 changes: 83 additions & 36 deletions cmd/cli/config/set_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//go:build unit || !integration

package config
package config_test

import (
"io"
Expand All @@ -11,61 +11,108 @@ import (
"github.com/stretchr/testify/require"
"sigs.k8s.io/yaml"

cmd2 "github.com/bacalhau-project/bacalhau/cmd/cli"
"github.com/bacalhau-project/bacalhau/pkg/config/types"
)

// TestAdditiveSet calls sets on the config system sequentially with different values
// and asserts that after each set only the newly set value was added to the config.
// Essentially we are testing two different things:
func TestAdditiveSet(t *testing.T) {
cfgFilePath := filepath.Join(t.TempDir(), "config.yaml")
func TestSetWithExplicitConfigPath(t *testing.T) {
tempDir := t.TempDir()
explicitConfigPath := filepath.Join(tempDir, "explicit_config.yaml")

err := setConfig(cfgFilePath, "api.host", "127.0.0.1")
require.NoError(t, err)
err = setConfig(cfgFilePath, "api.port", "1234")
// Create an empty config file
_, err := os.Create(explicitConfigPath)
require.NoError(t, err)

expected := types.Bacalhau{API: types.API{
Host: "127.0.0.1",
Port: 1234,
}}
actual := unmarshalConfigFile(t, cfgFilePath)
cmd := cmd2.NewRootCmd()
cmd.SetArgs([]string{"config", "set", "--config", explicitConfigPath, "api.host", "2.2.2.2"})

err = cmd.Execute()
require.NoError(t, err)

actual := unmarshalConfigFile(t, explicitConfigPath)
expected := types.Bacalhau{API: types.API{Host: "2.2.2.2"}}
require.Equal(t, expected, actual)
}

func TestSetWithDefaultConfigPath(t *testing.T) {
tempDir := t.TempDir()
t.Setenv("BACALHAU_DIR", tempDir)

cmd := cmd2.NewRootCmd()
cmd.SetArgs([]string{"config", "set", "api.port", "5678"})

err = setConfig(cfgFilePath, "compute.enabled", "true")
err := cmd.Execute()
require.NoError(t, err)
err = setConfig(cfgFilePath, "compute.orchestrators", "http://127.0.0.1:1234", "http://1.1.1.1:1234")

defaultConfigPath := filepath.Join(tempDir, "config.yaml")
actual := unmarshalConfigFile(t, defaultConfigPath)
expected := types.Bacalhau{API: types.API{Port: 5678}}
require.Equal(t, expected, actual)
}

func TestSetMultipleValues(t *testing.T) {
tempDir := t.TempDir()
os.Setenv("BACALHAU_DIR", tempDir)
defer os.Unsetenv("BACALHAU_DIR")

cmd := cmd2.NewRootCmd()
cmd.SetArgs([]string{"config", "set", "compute.orchestrators", "http://127.0.0.1:1234", "http://1.1.1.1:1234"})

err := cmd.Execute()
require.NoError(t, err)

expected = types.Bacalhau{
API: types.API{
Host: "127.0.0.1",
Port: 1234,
},
Compute: types.Compute{
Enabled: true,
Orchestrators: []string{
"http://127.0.0.1:1234",
"http://1.1.1.1:1234",
},
defaultConfigPath := filepath.Join(tempDir, "config.yaml")
actual := unmarshalConfigFile(t, defaultConfigPath)
expected := types.Bacalhau{Compute: types.Compute{
Orchestrators: []string{
"http://127.0.0.1:1234",
"http://1.1.1.1:1234",
},
}
actual = unmarshalConfigFile(t, cfgFilePath)

}}
require.Equal(t, expected, actual)

}

func TestSetFailure(t *testing.T) {
cfgFilePath := filepath.Join(t.TempDir(), "config.yaml")
// fails as the key isn't a valid config key
err := setConfig(cfgFilePath, "not.a.config.key", "porkchop sandwiches")
func TestSetInvalidKey(t *testing.T) {
tempDir := t.TempDir()
t.Setenv("BACALHAU_DIR", tempDir)

cmd := cmd2.NewRootCmd()
cmd.SetArgs([]string{"config", "set", "invalid.key", "value"})

err := cmd.Execute()
require.Error(t, err)
require.Contains(t, err.Error(), "not a valid config key")
}

func unmarshalConfigFile(t testing.TB, path string) types.Bacalhau {
func TestSetAdditiveChanges(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.yaml")

// Create an empty config file
_, err := os.Create(configPath)
require.NoError(t, err)

// First set
cmd1 := cmd2.NewRootCmd()
cmd1.SetArgs([]string{"config", "set", "--config", configPath, "api.host", "127.0.0.1"})
err = cmd1.Execute()
require.NoError(t, err)

// Second set
cmd2 := cmd2.NewRootCmd()
cmd2.SetArgs([]string{"config", "set", "--config", configPath, "api.port", "1234"})
err = cmd2.Execute()
require.NoError(t, err)

actual := unmarshalConfigFile(t, configPath)
expected := types.Bacalhau{API: types.API{
Host: "127.0.0.1",
Port: 1234,
}}
require.Equal(t, expected, actual)
}

func unmarshalConfigFile(t testing.TB, path string) types.Bacalhau {
configFile, err := os.Open(path)
require.NoError(t, err)
t.Cleanup(func() {
Expand Down
20 changes: 20 additions & 0 deletions cmd/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"github.com/bacalhau-project/bacalhau/cmd/util"
"github.com/bacalhau-project/bacalhau/cmd/util/flags/cliflags"
"github.com/bacalhau-project/bacalhau/cmd/util/flags/configflags"
"github.com/bacalhau-project/bacalhau/pkg/config/types"
"github.com/bacalhau-project/bacalhau/pkg/logger"
"github.com/bacalhau-project/bacalhau/pkg/system"
)

Expand Down Expand Up @@ -69,6 +71,22 @@ func NewRootCmd() *cobra.Command {
return err
}

// Configure logging
// While we allow users to configure logging via the config file, they are applied
// and will override this configuration at a later stage when the config is loaded.
// This is needed to ensure any logs before the config is loaded are captured.
wdbaruni marked this conversation as resolved.
Show resolved Hide resolved
logMode := viper.GetString(types.LoggingModeKey)
if logMode == "" {
logMode = string(logger.LogModeDefault)
}
logLevel := viper.GetString(types.LoggingLevelKey)
if logLevel == "" {
logLevel = "Info"
}
if err := logger.ConfigureLogging(logMode, logLevel); err != nil {
return fmt.Errorf("failed to configure logging: %w", err)
}

return nil
}

Expand Down Expand Up @@ -110,6 +128,8 @@ func NewRootCmd() *cobra.Command {
func Execute(ctx context.Context) {
rootCmd := NewRootCmd()
rootCmd.SetContext(ctx)
rootCmd.SilenceErrors = true
rootCmd.SilenceUsage = true
if err := rootCmd.Execute(); err != nil {
util.Fatal(rootCmd, err, 1)
}
Expand Down
28 changes: 6 additions & 22 deletions cmd/util/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package util

import (
"fmt"
"os"
"path/filepath"

"github.com/spf13/cobra"
"github.com/spf13/pflag"
Expand All @@ -14,7 +12,6 @@ import (
"github.com/bacalhau-project/bacalhau/cmd/util/hook"
"github.com/bacalhau-project/bacalhau/pkg/config"
"github.com/bacalhau-project/bacalhau/pkg/config/types"
"github.com/bacalhau-project/bacalhau/pkg/config_legacy"
"github.com/bacalhau-project/bacalhau/pkg/repo"
"github.com/bacalhau-project/bacalhau/pkg/setup"
)
Expand Down Expand Up @@ -51,26 +48,9 @@ func SetupConfigType(cmd *cobra.Command) (*config.Config, error) {
// check if the user specified config files via the --config flag
configFiles := getConfigFiles(v)

// if none were provided look in $XDG_CONFIG_HOME/bacalhau/config.yaml
if len(configFiles) == 0 {
xdgPath, err := os.UserConfigDir()
if err == nil {
path := filepath.Join(xdgPath, "bacalhau", config_legacy.FileName)
if _, err := os.Stat(path); err != nil {
// if the file exists and could not be read, return an error
if !os.IsNotExist(err) {
return nil, fmt.Errorf("loading config file at %q: %w", path, err)
}
} else {
// the file exists, use it.
configFiles = append(configFiles, path)
}
}
}
// if a config file is present, apply it to the config
// apply user specified config files via the --config flag, if any
if len(configFiles) > 0 {
cmd.Printf("Config file(s) found at path(s): %s\n", configFiles)
opts = append(opts, config.WithPaths(configFiles...))
opts = append(opts, config.WithPaths(getConfigFiles(v)...))
}

configFlags, err := getConfigFlags(v, cmd)
Expand Down Expand Up @@ -103,6 +83,10 @@ func SetupConfig(cmd *cobra.Command) (types.Bacalhau, error) {
if err != nil {
return types.Bacalhau{}, err
}
return DecodeBacalhauConfig(cfg)
}

func DecodeBacalhauConfig(cfg *config.Config) (types.Bacalhau, error) {
wdbaruni marked this conversation as resolved.
Show resolved Hide resolved
var out types.Bacalhau
if err := cfg.Unmarshal(&out); err != nil {
return types.Bacalhau{}, err
Expand Down
23 changes: 21 additions & 2 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/mitchellh/mapstructure"
Expand Down Expand Up @@ -188,6 +189,26 @@ func New(opts ...Option) (*Config, error) {
c.base.Set(name, value)
}

// if no config file was provided, we look for a config.yaml under the resolved data directory,
// and if it exists, we create and return a new config with the resolved path.
// we attempt this last to ensure the data-dir is resolved correctly from all config sources.
if len(c.paths) == 0 {
configFile := filepath.Join(c.base.GetString(types.DataDirKey), DefaultFileName)
if _, err := os.Stat(configFile); err == nil {
opts = append(opts, WithPaths(configFile))
return New(opts...)
}
}

// log the resolved config paths
absoluteConfigPaths := make([]string, len(c.paths))
for i, path := range c.paths {
absoluteConfigPaths[i], err = filepath.Abs(path)
if err != nil {
absoluteConfigPaths[i] = path
}
}
log.Info().Msgf("Config loaded from: %s, and with data-dir %s", absoluteConfigPaths, c.base.Get(types.DataDirKey))
return c, nil
}

Expand Down Expand Up @@ -218,7 +239,6 @@ func getNodeType(input string) (requester, compute bool, err error) {
// from the read config file.
// Load returns an error if the file cannot be read.
func (c *Config) Load(path string) error {
log.Info().Msgf("loading config file: %q", path)
c.base.SetConfigFile(path)
if err := c.base.ReadInConfig(); err != nil {
return err
Expand All @@ -229,7 +249,6 @@ func (c *Config) Load(path string) error {
// Merge merges a new configuration file specified by `path` with the existing config.
// Merge returns an error if the file cannot be read
func (c *Config) Merge(path string) error {
log.Info().Msgf("merging config file: %q", path)
c.base.SetConfigFile(path)
if err := c.base.MergeInConfig(); err != nil {
return err
Expand Down
Loading
Loading