You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by pc...@apache.org on 2022/09/07 12:45:41 UTC

[camel-k] branch main updated: feat(cli): Add a config command to manage the default settings

This is an automated email from the ASF dual-hosted git repository.

pcongiusti pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-k.git


The following commit(s) were added to refs/heads/main by this push:
     new 90888bc7a feat(cli): Add a config command to manage the default settings
90888bc7a is described below

commit 90888bc7a94948989cfcd860bb6802fac0d83576
Author: Nicolas Filotto <nf...@talend.com>
AuthorDate: Mon Sep 5 10:17:40 2022 +0200

    feat(cli): Add a config command to manage the default settings
---
 docs/modules/ROOT/pages/cli/file-based-config.adoc |  88 +++++++++--
 e2e/namespace/install/cli/config_test.go           |  71 +++++++++
 pkg/cmd/config.go                                  | 164 +++++++++++++++++++++
 pkg/cmd/config_test.go                             | 137 +++++++++++++++++
 pkg/cmd/root.go                                    |  11 +-
 pkg/cmd/util_config.go                             |  11 +-
 6 files changed, 469 insertions(+), 13 deletions(-)

diff --git a/docs/modules/ROOT/pages/cli/file-based-config.adoc b/docs/modules/ROOT/pages/cli/file-based-config.adoc
index 40b4575ca..a824cc1eb 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 the 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` accepts 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/e2e/namespace/install/cli/config_test.go b/e2e/namespace/install/cli/config_test.go
new file mode 100644
index 000000000..fada15bc0
--- /dev/null
+++ b/e2e/namespace/install/cli/config_test.go
@@ -0,0 +1,71 @@
+//go:build integration
+// +build integration
+
+// To enable compilation of this file in Goland, go to "Settings -> Go -> Vendoring & Build Tags -> Custom Tags" and add "integration"
+
+/*
+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 common
+
+import (
+	"os"
+	"strings"
+	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+
+	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/assert"
+
+	. "github.com/apache/camel-k/e2e/support"
+	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
+	"github.com/apache/camel-k/pkg/cmd"
+)
+
+func TestKamelCLIConfig(t *testing.T) {
+	WithNewTestNamespace(t, func(ns string) {
+		operatorID := "camel-k-cli-config"
+		Expect(KamelInstallWithID(operatorID, ns).Execute()).To(Succeed())
+
+		t.Run("check default namespace", func(t *testing.T) {
+			_, err := os.Stat(cmd.DefaultConfigLocation)
+			assert.True(t, os.IsNotExist(err), "No file at "+cmd.DefaultConfigLocation+" was expected")
+			t.Cleanup(func() { os.Remove(cmd.DefaultConfigLocation) })
+			Expect(Kamel("config", "--default-namespace", ns).Execute()).To(Succeed())
+			_, err = os.Stat(cmd.DefaultConfigLocation)
+			assert.Nil(t, err, "A file at "+cmd.DefaultConfigLocation+" was expected")
+			Expect(Kamel("run", "--operator-id", operatorID, "files/yaml.yaml").Execute()).To(Succeed())
+
+			Eventually(IntegrationPodPhase(ns, "yaml"), TestTimeoutLong).Should(Equal(corev1.PodRunning))
+			Eventually(IntegrationConditionStatus(ns, "yaml", v1.IntegrationConditionReady), TestTimeoutShort).
+				Should(Equal(corev1.ConditionTrue))
+			Eventually(IntegrationLogs(ns, "yaml"), TestTimeoutShort).Should(ContainSubstring("Magicstring!"))
+
+			// first line of the integration logs
+			logs := strings.Split(IntegrationLogs(ns, "yaml")(), "\n")[0]
+			podName := IntegrationPod(ns, "yaml")().Name
+
+			logsCLI := GetOutputStringAsync(Kamel("log", "yaml"))
+			Eventually(logsCLI).Should(ContainSubstring("Monitoring pod " + podName))
+			Eventually(logsCLI).Should(ContainSubstring(logs))
+
+			// Clean up
+			Expect(Kamel("delete", "--all").Execute()).To(Succeed())
+		})
+	})
+}
diff --git a/pkg/cmd/config.go b/pkg/cmd/config.go
new file mode 100644
index 000000000..0b361d163
--- /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 000000000..57d5a981e
--- /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 b75afd321..f72128903 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 68d47eb7a..3ebb313e2 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{}),
 	}