diff --git a/docs/modules/ROOT/pages/cli/file-based-config.adoc b/docs/modules/ROOT/pages/cli/file-based-config.adoc index 40b4575cad..30a9fa569e 100644 --- a/docs/modules/ROOT/pages/cli/file-based-config.adoc +++ b/docs/modules/ROOT/pages/cli/file-based-config.adoc @@ -2,23 +2,93 @@ File-based configuration is used to set command flags. Flag values do not need to be entered on a regular basis. The file is read on Kamel startup and the flags are set accordingly. -The file's default name is `kamel-config.yaml` . Which should be placed in either of this directory structure: +The file's default name is `kamel-config.yaml`, it can be changed by setting the environment variable `KAMEL_CONFIG_NAME`. Kamel tries to read the file from following directories in the given order: - - `~/.kamel/` - - `./.kamel/` - `.` + - `./.kamel/` + - `~/.kamel/` -it can be overridden by setting an env value `KAMEL_CONFIG_NAME` to file path. +It can be overridden by setting the environment variable `KAMEL_CONFIG_PATH` to file path. -To configure this flag, create a file named kamel-config.yaml on the same directory as your integration. The file must contain a yaml structure as shown below: +To configure this flag, create a file named `kamel-config.yaml` on the same directory as your integration. The file must contain a yaml structure as shown below: -.Kamel-config.yaml +.kamel-config.yaml ```yaml kamel: install: - namespace: kamel - healthPort: 8081 - monitoringPort: 8082 + health-port: 8081 + monitoring-port: 8082 ``` + +As there are several supported locations, it can be handy to list a configuration file in one specific location, in this particular case the `config` command can be used. + +To list the configuration file used in practice by Kamel: + +[source,console] +---- +$ kamel config --list +The configuration file is read from /some/path/kamel-config.yaml +kamel: + config: + default-namespace: some-name +---- + +Alternatively, the same result can be retrieved using the `--folder` flag with `used` as value. + +[source,console] +---- +$ kamel config --list --folder used +---- + +The flag `--folder` supports 4 other possible values, one per possible location. + +To list the configuration file in the working directory (`.`): + +[source,console] +---- +$ kamel config --list --folder working +---- + +To list the configuration file in the folder `.kamel` located in the working directory (`./.kamel/`): + +[source,console] +---- +$ kamel config --list --folder sub +---- + +To list the configuration file in the home directory (`~/.kamel/`): + +[source,console] +---- +$ kamel config --list --folder home +---- + +To list the configuration file located in the folder whose path is set in the environment variable `KAMEL_CONFIG_PATH`: + +[source,console] +---- +$ kamel config --list --folder env +---- + +The `config` command can also set the default namespace for all Kamel commands thanks to the flag `--default-namespace` as next: + +[source,console] +---- +$ kamel config --default-namespace some-name +---- + +Note that the flag `--default-namespace` can be associated with `--list` to see directly the resulting content: + +[source,console] +---- +$ kamel config --list --default-namespace some-name +The configuration file is read from /some/path/kamel-config.yaml +kamel: + config: + default-namespace: some-name + install: + health-port: 8081 + monitoring-port: 8082 +---- diff --git a/pkg/cmd/config.go b/pkg/cmd/config.go new file mode 100644 index 0000000000..0b361d1631 --- /dev/null +++ b/pkg/cmd/config.go @@ -0,0 +1,164 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "gopkg.in/yaml.v2" +) + +// ConfigFolder defines the different types of folder containing the configuration file. +type ConfigFolder string + +const ( + // The path of the folder containing the configuration file is retrieved from the environment + // variable KAMEL_CONFIG_PATH. + ConfigFolderEnvVar ConfigFolder = "env" + // The path of the folder containing the configuration file is $HOME/.kamel. + ConfigFolderHome ConfigFolder = "home" + // The folder containing the configuration file is .kamel located in the working directory. + ConfigFolderSubDirectory ConfigFolder = "sub" + // The folder containing the configuration file is the working directory. + ConfigFolderWorking ConfigFolder = "working" + // The folder containing the configuration file is the directory currently used by Kamel. + ConfigFolderUsed ConfigFolder = "used" +) + +// nolint: unparam +func newCmdConfig(rootCmdOptions *RootCmdOptions) (*cobra.Command, *configCmdOptions) { + options := configCmdOptions{} + cmd := cobra.Command{ + Use: "config", + Short: "Configure the default settings", + PreRunE: decode(&options), + Args: options.validateArgs, + RunE: options.run, + } + + cmd.Flags().String("folder", "used", "The type of folder containing the configuration file to read/write. The supported values are 'env', 'home', 'sub', 'working' and 'used' for respectively $KAMEL_CONFIG_PATH, $HOME/.kamel, .kamel, . and the folder used by kamel") + cmd.Flags().String("default-namespace", "", "The name of the namespace to use by default") + cmd.Flags().BoolP("list", "l", false, "List all existing settings") + return &cmd, &options +} + +type configCmdOptions struct { + DefaultNamespace string `mapstructure:"default-namespace"` +} + +func (o *configCmdOptions) validateArgs(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + return errors.New("no arguments are expected") + } + return nil +} + +func (o *configCmdOptions) run(cmd *cobra.Command, args []string) error { + path, err := getConfigLocation(cmd) + if err != nil { + return err + } + if cmd.Flags().Lookup("default-namespace").Changed { + err = o.saveConfiguration(cmd, path) + if err != nil { + return err + } + } + if cmd.Flags().Lookup("list").Changed { + err = printConfiguration(cmd, path) + if err != nil { + return err + } + } + return nil +} + +// Save the configuration at the given location. +func (o *configCmdOptions) saveConfiguration(cmd *cobra.Command, path string) error { + cfg, err := LoadConfigurationFrom(path) + if err != nil { + return err + } + + cfg.Update(cmd, pathToRoot(cmd), o, true) + + err = cfg.Save() + if err != nil { + return err + } + return nil +} + +// Gives the location of the configuration file. +func getConfigLocation(cmd *cobra.Command) (string, error) { + var folder ConfigFolder + if s, err := cmd.Flags().GetString("folder"); err == nil { + folder = ConfigFolder(s) + } else { + return "", err + } + var path string + switch folder { + case ConfigFolderUsed: + path = viper.ConfigFileUsed() + if path != "" { + return path, nil + } + case ConfigFolderEnvVar: + path = os.Getenv("KAMEL_CONFIG_PATH") + case ConfigFolderHome: + home, err := os.UserHomeDir() + cobra.CheckErr(err) + path = filepath.Join(home, ".kamel") + case ConfigFolderSubDirectory: + path = ".kamel" + case ConfigFolderWorking: + path = "." + default: + return "", fmt.Errorf("unsupported type of folder: %s", folder) + } + configName := os.Getenv("KAMEL_CONFIG_NAME") + if configName == "" { + configName = DefaultConfigName + } + return filepath.Join(path, fmt.Sprintf("%s.yaml", configName)), nil +} + +// Print the content of the configuration file located at the given path. +func printConfiguration(cmd *cobra.Command, path string) error { + cfg, err := LoadConfigurationFrom(path) + if err != nil { + return err + } + if len(cfg.content) == 0 { + fmt.Fprintf(cmd.OutOrStdout(), "No settings could be found in %s\n", cfg.location) + } else { + bs, err := yaml.Marshal(cfg.content) + if err != nil { + return fmt.Errorf("unable to marshal config to YAML: %w", err) + } + fmt.Fprintf(cmd.OutOrStdout(), "The configuration file is read from %s\n", cfg.location) + fmt.Fprintln(cmd.OutOrStdout(), string(bs)) + } + return nil +} diff --git a/pkg/cmd/config_test.go b/pkg/cmd/config_test.go new file mode 100644 index 0000000000..57d5a981e8 --- /dev/null +++ b/pkg/cmd/config_test.go @@ -0,0 +1,137 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/apache/camel-k/pkg/util/test" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +const cmdConfig = "config" + +// nolint: unparam +func initializeConfigCmdOptions(t *testing.T, mock bool) (*configCmdOptions, *cobra.Command, RootCmdOptions) { + t.Helper() + + options, rootCmd := kamelTestPreAddCommandInit() + configCmdOptions := addTestConfigCmd(*options, rootCmd, mock) + kamelTestPostAddCommandInit(t, rootCmd) + + return configCmdOptions, rootCmd, *options +} + +func addTestConfigCmd(options RootCmdOptions, rootCmd *cobra.Command, mock bool) *configCmdOptions { + // add a testing version of config Command + configCmd, configOptions := newCmdConfig(&options) + if mock { + configCmd.RunE = func(c *cobra.Command, args []string) error { + return nil + } + } + configCmd.Args = test.ArbitraryArgs + rootCmd.AddCommand(configCmd) + return configOptions +} + +func TestConfigNonExistingFlag(t *testing.T) { + _, rootCmd, _ := initializeConfigCmdOptions(t, true) + _, err := test.ExecuteCommand(rootCmd, cmdConfig, "--nonExistingFlag") + assert.NotNil(t, err) +} + +func TestConfigDefaultNamespaceFlag(t *testing.T) { + configCmdOptions, rootCmd, _ := initializeConfigCmdOptions(t, true) + _, err := test.ExecuteCommand(rootCmd, cmdConfig, "--default-namespace", "foo") + assert.Nil(t, err) + assert.Equal(t, "foo", configCmdOptions.DefaultNamespace) +} + +func TestConfigListFlag(t *testing.T) { + _, rootCmd, _ := initializeConfigCmdOptions(t, false) + output, err := test.ExecuteCommand(rootCmd, cmdConfig, "--list") + assert.Nil(t, err) + assert.True(t, strings.Contains(output, "No settings"), "The output is unexpected: "+output) +} + +func TestConfigFolderFlagToUsed(t *testing.T) { + _, rootCmd, _ := initializeConfigCmdOptions(t, false) + output, err := test.ExecuteCommand(rootCmd, cmdConfig, "--list", "--folder", "used") + assert.Nil(t, err) + assert.True(t, strings.Contains(output, fmt.Sprintf(" %s", DefaultConfigLocation)), "The output is unexpected: "+output) +} + +func TestConfigFolderFlagToSub(t *testing.T) { + _, rootCmd, _ := initializeConfigCmdOptions(t, false) + output, err := test.ExecuteCommand(rootCmd, cmdConfig, "--list", "--folder", "sub") + assert.Nil(t, err) + assert.True(t, strings.Contains(output, fmt.Sprintf(" .kamel/%s", DefaultConfigLocation)), "The output is unexpected: "+output) +} + +func TestConfigFolderFlagToHome(t *testing.T) { + _, rootCmd, _ := initializeConfigCmdOptions(t, false) + output, err := test.ExecuteCommand(rootCmd, cmdConfig, "--list", "--folder", "home") + assert.Nil(t, err) + assert.True(t, strings.Contains(output, fmt.Sprintf("/.kamel/%s", DefaultConfigLocation)), "The output is unexpected: "+output) +} + +func TestConfigFolderFlagToEnv(t *testing.T) { + os.Setenv("KAMEL_CONFIG_PATH", "/foo/bar") + t.Cleanup(func() { os.Unsetenv("KAMEL_CONFIG_PATH") }) + _, rootCmd, _ := initializeConfigCmdOptions(t, false) + output, err := test.ExecuteCommand(rootCmd, cmdConfig, "--list", "--folder", "env") + assert.Nil(t, err) + assert.True(t, strings.Contains(output, fmt.Sprintf("/foo/bar/%s", DefaultConfigLocation)), "The output is unexpected: "+output) +} + +func TestConfigFolderFlagToEnvWithConfigName(t *testing.T) { + os.Setenv("KAMEL_CONFIG_NAME", "config") + os.Setenv("KAMEL_CONFIG_PATH", "/foo/bar") + t.Cleanup(func() { + os.Unsetenv("KAMEL_CONFIG_NAME") + os.Unsetenv("KAMEL_CONFIG_PATH") + }) + _, rootCmd, _ := initializeConfigCmdOptions(t, false) + output, err := test.ExecuteCommand(rootCmd, cmdConfig, "--list", "--folder", "env") + assert.Nil(t, err) + assert.True(t, strings.Contains(output, "/foo/bar/config.yaml"), "The output is unexpected: "+output) +} + +func TestConfigDefaultNamespace(t *testing.T) { + _, err := os.Stat(DefaultConfigLocation) + assert.True(t, os.IsNotExist(err), "No file at "+DefaultConfigLocation+" was expected") + _, rootCmd, _ := initializeConfigCmdOptions(t, false) + t.Cleanup(func() { os.Remove(DefaultConfigLocation) }) + _, err = test.ExecuteCommand(rootCmd, cmdConfig, "--default-namespace", "foo") + assert.Nil(t, err) + _, err = os.Stat(DefaultConfigLocation) + assert.Nil(t, err, "A file at "+DefaultConfigLocation+" was expected") + output, err := test.ExecuteCommand(rootCmd, cmdConfig, "--list") + assert.Nil(t, err) + assert.True(t, strings.Contains(output, "foo"), "The output is unexpected: "+output) + _, rootCmd, _ = initializeInstallCmdOptions(t) + _, err = test.ExecuteCommand(rootCmd, cmdInstall) + assert.Nil(t, err) + // Check default namespace is set + assert.Equal(t, "foo", rootCmd.Flag("namespace").Value.String()) +} diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index b75afd321c..f72128903f 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -152,6 +152,7 @@ func addKamelSubcommands(cmd *cobra.Command, options *RootCmdOptions) { cmd.AddCommand(cmdOnly(newCmdBind(options))) cmd.AddCommand(cmdOnly(newCmdPromote(options))) cmd.AddCommand(newCmdKamelet(options)) + cmd.AddCommand(cmdOnly(newCmdConfig(options))) } func addHelpSubCommands(cmd *cobra.Command, options *RootCmdOptions) error { @@ -182,9 +183,13 @@ func (command *RootCmdOptions) preRun(cmd *cobra.Command, _ []string) error { return errors.Wrap(err, "cannot get command client") } if command.Namespace == "" { - current, err := c.GetCurrentNamespace(command.KubeConfig) - if err != nil { - return errors.Wrap(err, "cannot get current namespace") + current := viper.GetString("kamel.config.default-namespace") + if current == "" { + defaultNS, err := c.GetCurrentNamespace(command.KubeConfig) + if err != nil { + return errors.Wrap(err, "cannot get current namespace") + } + current = defaultNS } err = cmd.Flag("namespace").Value.Set(current) if err != nil { diff --git a/pkg/cmd/util_config.go b/pkg/cmd/util_config.go index 68d47eb7a8..3ebb313e2d 100644 --- a/pkg/cmd/util_config.go +++ b/pkg/cmd/util_config.go @@ -55,8 +55,17 @@ type Config struct { // LoadConfiguration loads a kamel configuration file. func LoadConfiguration() (*Config, error) { + return loadConfiguration(viper.ConfigFileUsed()) +} + +// LoadConfiguration loads a kamel configuration file from a specific location. +func LoadConfigurationFrom(location string) (*Config, error) { + return loadConfiguration(location) +} + +func loadConfiguration(location string) (*Config, error) { config := Config{ - location: viper.ConfigFileUsed(), + location: location, content: make(map[string]interface{}), }