Repository: apache/camel-k
Pull request #3612 (2022-09-07 09:03:51)

feat: Auto-create platform when registry can be inferred from KEP-1755
Complements #2696.

**Release Note**
```release-note
feat: Auto-create platform when registry can be inferred from KEP-1755
```


astefanutti: > LGTM, should we have some E2E testing as well?

Yes, that'd be ideal, but I think that may require some thinking now that most of the e2e workflows use a globally installed operator. I've created #3615 to track that effort.

Patch:
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/go.mod b/go.mod
index 9ab82a740..d2c0ac0e2 100644
--- a/go.mod
+++ b/go.mod
@@ -37,7 +37,7 @@ require (
 	github.com/sirupsen/logrus v1.9.0
 	github.com/spf13/cobra v1.5.0
 	github.com/spf13/pflag v1.0.5
-	github.com/spf13/viper v1.12.0
+	github.com/spf13/viper v1.13.0
 	github.com/stoewer/go-strcase v1.2.0
 	github.com/stretchr/testify v1.8.0
 	go.uber.org/automaxprocs v1.4.0
@@ -125,7 +125,7 @@ require (
 	github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
 	github.com/opencontainers/runc v1.0.2 // indirect
 	github.com/pelletier/go-toml v1.9.5 // indirect
-	github.com/pelletier/go-toml/v2 v2.0.1 // indirect
+	github.com/pelletier/go-toml/v2 v2.0.5 // indirect
 	github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/prometheus/procfs v0.8.0 // indirect
@@ -136,7 +136,7 @@ require (
 	github.com/spf13/afero v1.8.2 // indirect
 	github.com/spf13/cast v1.5.0 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
-	github.com/subosito/gotenv v1.3.0 // indirect
+	github.com/subosito/gotenv v1.4.1 // indirect
 	github.com/vbatts/tar-split v0.11.2 // indirect
 	go.opencensus.io v0.23.0 // indirect
 	go.uber.org/atomic v1.9.0 // indirect
@@ -153,7 +153,7 @@ require (
 	google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect
 	google.golang.org/grpc v1.46.2 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect
-	gopkg.in/ini.v1 v1.66.4 // indirect
+	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	k8s.io/component-base v0.23.8 // indirect
 	k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf // indirect
diff --git a/go.sum b/go.sum
index 7b915708b..397600fa3 100644
--- a/go.sum
+++ b/go.sum
@@ -1227,8 +1227,8 @@ github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko
 github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
 github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
 github.com/pelletier/go-toml/v2 v2.0.0-beta.2/go.mod h1:+X+aW6gUj6Hda43TeYHVCIvYNG/jqY/8ZFXAeXXHl+Q=
-github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
-github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
+github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
+github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
 github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
 github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
 github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw=
@@ -1415,8 +1415,8 @@ github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q
 github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
 github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
 github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4=
-github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
-github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
+github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU=
+github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw=
 github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518/go.mod h1:CKI4AZ4XmGV240rTHfO0hfE83S6/a3/Q1siZJ/vXf7A=
 github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
 github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
@@ -1439,11 +1439,12 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
-github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI=
-github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs=
+github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
+github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
 github.com/sylvia7788/contextcheck v1.0.4/go.mod h1:vuPKJMQ7MQ91ZTqfdyreNKwZjyUg6KO+IebVyQDedZQ=
 github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
 github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
@@ -2320,8 +2321,8 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
-gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
 gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
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/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{}),
 	}
 
diff --git a/pkg/trait/platform.go b/pkg/trait/platform.go
index 635bc1258..d2e7b6783 100644
--- a/pkg/trait/platform.go
+++ b/pkg/trait/platform.go
@@ -20,7 +20,7 @@ package trait
 import (
 	"fmt"
 
-	k8serrors "k8s.io/apimachinery/pkg/api/errors"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/utils/pointer"
 
 	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
@@ -29,6 +29,7 @@ import (
 	"github.com/apache/camel-k/pkg/platform"
 	"github.com/apache/camel-k/pkg/util/defaults"
 	"github.com/apache/camel-k/pkg/util/openshift"
+	image "github.com/apache/camel-k/pkg/util/registry"
 )
 
 type platformTrait struct {
@@ -59,6 +60,10 @@ func (t *platformTrait) Configure(e *Environment) (bool, error) {
 					return false, err
 				} else if ocp {
 					t.CreateDefault = &ocp
+				} else if addr, err := image.GetRegistryAddress(e.Ctx, t.Client); err != nil {
+					return false, err
+				} else if addr != nil {
+					t.CreateDefault = pointer.Bool(true)
 				}
 			}
 			if t.Global == nil {
@@ -96,7 +101,7 @@ func (t *platformTrait) Apply(e *Environment) error {
 
 func (t *platformTrait) getOrCreatePlatform(e *Environment) (*v1.IntegrationPlatform, error) {
 	pl, err := platform.GetOrFindForResource(e.Ctx, t.Client, e.Integration, false)
-	if err != nil && k8serrors.IsNotFound(err) {
+	if err != nil && apierrors.IsNotFound(err) {
 		if pointer.BoolDeref(t.CreateDefault, false) {
 			platformName := e.Integration.Status.Platform
 			if platformName == "" {
@@ -126,7 +131,7 @@ func (t *platformTrait) getOrCreatePlatform(e *Environment) (*v1.IntegrationPlat
 			e.Resources.Add(pl)
 
 			// Make sure that IntegrationPlatform installed in operator namespace can be seen by others
-			if err := install.IntegrationPlatformViewerRole(e.Ctx, t.Client, namespace); err != nil && !k8serrors.IsAlreadyExists(err) {
+			if err := install.IntegrationPlatformViewerRole(e.Ctx, t.Client, namespace); err != nil && !apierrors.IsAlreadyExists(err) {
 				t.L.Info(fmt.Sprintf("Cannot install global IntegrationPlatform viewer role in namespace '%s': skipping.", namespace))
 			}
 
diff --git a/pkg/util/camel/camel_util.go b/pkg/util/camel/camel_util.go
index f41dad240..7b6215881 100644
--- a/pkg/util/camel/camel_util.go
+++ b/pkg/util/camel/camel_util.go
@@ -65,10 +65,10 @@ func newSemVerConstraint(versionConstraint string) *semver.Constraints {
 	constraint, err := semver.NewConstraint(versionConstraint)
 	if err != nil || constraint == nil {
 		if err != nil {
-			log.Debug("Unable to parse version constraint: %s, error:\n", versionConstraint, err.Error())
+			log.Debugf("Unable to parse version constraint: %s, error: %s", versionConstraint, err.Error())
 		}
 		if constraint == nil {
-			log.Debug("Unable to parse version constraint: %s\n", versionConstraint)
+			log.Debugf("Unable to parse version constraint: %s", versionConstraint)
 		}
 	}
 


Test patch:
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_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())
+}