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

commit 3683ab0e1fc4f02251b61e79bd50c9b367ee21c4
Author: Pasquale Congiusti <[email protected]>
AuthorDate: Mon Jan 24 17:05:01 2022 +0100

    feat(cmd/bind): --trait option
    
    * Add the possibility to use -t/--trait feature directly on bind
    * Code refactorying to reduce duplicates in cmd/run
    * Provided a cleaner output for run command
    
    Closes #2596
---
 pkg/cmd/bind.go       |  42 ++++++-----
 pkg/cmd/bind_test.go  |  30 ++++++++
 pkg/cmd/run.go        | 165 +++++++-----------------------------------
 pkg/cmd/run_test.go   | 196 ++++++++++++++++++++++++++++++++++++--------------
 pkg/cmd/trait_help.go | 107 +++++++++++++++++++++++++++
 5 files changed, 329 insertions(+), 211 deletions(-)

diff --git a/pkg/cmd/bind.go b/pkg/cmd/bind.go
index 4e581ce..e8c59e2 100644
--- a/pkg/cmd/bind.go
+++ b/pkg/cmd/bind.go
@@ -25,6 +25,7 @@ import (
 
        v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
        "github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+       "github.com/apache/camel-k/pkg/trait"
        "github.com/apache/camel-k/pkg/util/kubernetes"
        "github.com/apache/camel-k/pkg/util/reference"
        "github.com/apache/camel-k/pkg/util/uri"
@@ -64,6 +65,7 @@ func newCmdBind(rootCmdOptions *RootCmdOptions) 
(*cobra.Command, *bindCmdOptions
        cmd.Flags().StringArrayP("property", "p", nil, `Add a binding property 
in the form of "source.<key>=<value>", "sink.<key>=<value>", 
"error-handler.<key>=<value>" or "step-<n>.<key>=<value>"`)
        cmd.Flags().Bool("skip-checks", false, "Do not verify the binding for 
compliance with Kamelets and other Kubernetes resources")
        cmd.Flags().StringArray("step", nil, `Add binding steps as Kubernetes 
resources. Endpoints are expected in the format 
"[[apigroup/]version:]kind:[namespace/]name", plain Camel URIs or Kamelet 
name.`)
+       cmd.Flags().StringArrayP("trait", "t", nil, `Add a trait to the 
corresponding Integration.`)
 
        return &cmd, &options
 }
@@ -84,6 +86,7 @@ type bindCmdOptions struct {
        Properties   []string `mapstructure:"properties" yaml:",omitempty"`
        SkipChecks   bool     `mapstructure:"skip-checks" yaml:",omitempty"`
        Steps        []string `mapstructure:"steps" yaml:",omitempty"`
+       Traits       []string `mapstructure:"traits" yaml:",omitempty"`
 }
 
 func (o *bindCmdOptions) validate(cmd *cobra.Command, args []string) error {
@@ -128,10 +131,22 @@ func (o *bindCmdOptions) validate(cmd *cobra.Command, 
args []string) error {
                }
        }
 
-       return nil
+       client, err := o.GetCmdClient()
+       if err != nil {
+               return err
+       }
+       catalog := trait.NewCatalog(client)
+
+       return validateTraits(catalog, o.Traits)
 }
 
 func (o *bindCmdOptions) run(cmd *cobra.Command, args []string) error {
+       client, err := o.GetCmdClient()
+       if err != nil {
+               return err
+       }
+       catalog := trait.NewCatalog(client)
+
        source, err := o.decode(args[0], sourceKey)
        if err != nil {
                return err
@@ -174,28 +189,19 @@ func (o *bindCmdOptions) run(cmd *cobra.Command, args 
[]string) error {
                        binding.Spec.Steps = append(binding.Spec.Steps, step)
                }
        }
+       for _, item := range o.Connects {
+               o.Traits = append(o.Traits, 
fmt.Sprintf("service-binding.services=%s", item))
+       }
 
-       if len(o.Connects) > 0 {
-               trait := make(map[string]interface{})
-               trait["serviceBindings"] = o.Connects
-               specs := make(map[string]v1.TraitSpec)
-               data, err := json.Marshal(trait)
-               if err != nil {
-                       return err
+       if len(o.Traits) > 0 {
+               if binding.Spec.Integration == nil {
+                       binding.Spec.Integration = &v1.IntegrationSpec{}
                }
-               var spec v1.TraitSpec
-               err = json.Unmarshal(data, &spec.Configuration)
+               traits, err := configureTraits(o.Traits, catalog)
                if err != nil {
                        return err
                }
-               specs["service-binding"] = spec
-               binding.Spec.Integration = &v1.IntegrationSpec{}
-               binding.Spec.Integration.Traits = specs
-       }
-
-       client, err := o.GetCmdClient()
-       if err != nil {
-               return err
+               binding.Spec.Integration.Traits = traits
        }
 
        if o.OutputFormat != "" {
diff --git a/pkg/cmd/bind_test.go b/pkg/cmd/bind_test.go
index 34736d1..d1c396a 100644
--- a/pkg/cmd/bind_test.go
+++ b/pkg/cmd/bind_test.go
@@ -161,3 +161,33 @@ spec:
 status: {}
 `, output)
 }
+
+func TestBindTraits(t *testing.T) {
+       buildCmdOptions, bindCmd, _ := initializeBindCmdOptions(t)
+       output, err := test.ExecuteCommand(bindCmd, cmdBind, "my:src", 
"my:dst", "-o", "yaml", "-t", "mount.configs=configmap:my-cm", "-c", 
"my-service-binding")
+       assert.Equal(t, "yaml", buildCmdOptions.OutputFormat)
+
+       assert.Nil(t, err)
+       assert.Equal(t, `apiVersion: camel.apache.org/v1alpha1
+kind: KameletBinding
+metadata:
+  creationTimestamp: null
+  name: my-to-my
+spec:
+  integration:
+    traits:
+      mount:
+        configuration:
+          configs:
+          - configmap:my-cm
+      service-binding:
+        configuration:
+          services:
+          - my-service-binding
+  sink:
+    uri: my:dst
+  source:
+    uri: my:src
+status: {}
+`, output)
+}
diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go
index 3d0aa32..4c74065 100644
--- a/pkg/cmd/run.go
+++ b/pkg/cmd/run.go
@@ -29,7 +29,6 @@ import (
        "syscall"
 
        "github.com/magiconair/properties"
-       "github.com/mitchellh/mapstructure"
        "github.com/pkg/errors"
        "github.com/spf13/cobra"
        "github.com/spf13/pflag"
@@ -37,7 +36,9 @@ import (
        corev1 "k8s.io/api/core/v1"
        k8serrors "k8s.io/apimachinery/pkg/api/errors"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/apimachinery/pkg/runtime"
        "k8s.io/apimachinery/pkg/util/yaml"
+       "k8s.io/cli-runtime/pkg/printers"
 
        ctrl "sigs.k8s.io/controller-runtime/pkg/client"
 
@@ -245,7 +246,13 @@ func (o *runCmdOptions) validate() error {
                }
        }
 
-       return nil
+       client, err := o.GetCmdClient()
+       if err != nil {
+               return err
+       }
+       catalog := trait.NewCatalog(client)
+
+       return validateTraits(catalog, o.Traits)
 }
 
 func filterBuildPropertyFiles(maybePropertyFiles []string) []string {
@@ -266,18 +273,6 @@ func (o *runCmdOptions) run(cmd *cobra.Command, args 
[]string) error {
        }
 
        catalog := trait.NewCatalog(c)
-       tp := catalog.ComputeTraitsProperties()
-       for _, t := range o.Traits {
-               kv := strings.SplitN(t, "=", 2)
-               prefix := kv[0]
-               if strings.Contains(prefix, "[") {
-                       prefix = prefix[0:strings.Index(prefix, "[")]
-               }
-               if !util.StringSliceExists(tp, prefix) {
-                       return fmt.Errorf("%s is not a valid trait property", t)
-               }
-       }
-
        integration, err := o.createOrUpdateIntegration(cmd, c, args, catalog)
        if err != nil {
                return err
@@ -603,32 +598,19 @@ func (o *runCmdOptions) createOrUpdateIntegration(cmd 
*cobra.Command, c client.C
        for _, item := range o.EnvVars {
                o.Traits = append(o.Traits, fmt.Sprintf("environment.vars=%s", 
item))
        }
-
-       if err := o.configureTraits(integration, o.Traits, catalog); err != nil 
{
-               return nil, err
+       for _, item := range o.Connects {
+               o.Traits = append(o.Traits, 
fmt.Sprintf("service-binding.services=%s", item))
        }
-
-       switch o.OutputFormat {
-       case "":
-               // continue..
-       case "yaml":
-               data, err := kubernetes.ToYAML(integration)
-               if err != nil {
-                       return nil, err
-               }
-               fmt.Fprint(cmd.OutOrStdout(), string(data))
-               return nil, nil
-
-       case "json":
-               data, err := kubernetes.ToJSON(integration)
+       if len(o.Traits) > 0 {
+               traits, err := configureTraits(o.Traits, catalog)
                if err != nil {
                        return nil, err
                }
-               fmt.Fprint(cmd.OutOrStdout(), string(data))
-               return nil, nil
+               integration.Spec.Traits = traits
+       }
 
-       default:
-               return nil, fmt.Errorf("invalid output format option '%s', 
should be one of: yaml|json", o.OutputFormat)
+       if o.OutputFormat != "" {
+               return nil, showIntegrationOutput(cmd, integration, 
o.OutputFormat, c.GetScheme())
        }
 
        if existing == nil {
@@ -646,6 +628,14 @@ func (o *runCmdOptions) createOrUpdateIntegration(cmd 
*cobra.Command, c client.C
        return integration, nil
 }
 
+func showIntegrationOutput(cmd *cobra.Command, integration *v1.Integration, 
outputFormat string, scheme runtime.ObjectTyper) error {
+       printer := printers.NewTypeSetter(scheme)
+       printer.Delegate = &kubernetes.CLIPrinter{
+               Format: outputFormat,
+       }
+       return printer.PrintObj(integration, cmd.OutOrStdout())
+}
+
 func (o *runCmdOptions) parseAndConvertToTrait(
        c client.Client, integration *v1.Integration, params []string,
        parse func(string) (*resource.Config, error),
@@ -703,22 +693,6 @@ func (o *runCmdOptions) GetIntegrationName(sources 
[]string) string {
        return name
 }
 
-func (o *runCmdOptions) configureTraits(integration *v1.Integration, options 
[]string, catalog trait.Finder) error {
-       // configure ServiceBinding trait
-       for _, sb := range o.Connects {
-               bindings := fmt.Sprintf("service-binding.services=%s", sb)
-               options = append(options, bindings)
-       }
-       traits, err := configureTraits(options, catalog)
-       if err != nil {
-               return err
-       }
-
-       integration.Spec.Traits = traits
-
-       return nil
-}
-
 func loadPropertyFile(fileName string) (*properties.Properties, error) {
        file, err := util.ReadFile(fileName)
        if err != nil {
@@ -761,92 +735,3 @@ func resolvePodTemplate(ctx context.Context, templateSrc 
string, spec *v1.Integr
        }
        return err
 }
-
-func configureTraits(options []string, catalog trait.Finder) 
(map[string]v1.TraitSpec, error) {
-       traits := make(map[string]map[string]interface{})
-
-       for _, option := range options {
-               parts := traitConfigRegexp.FindStringSubmatch(option)
-               if len(parts) < 4 {
-                       return nil, errors.New("unrecognized config format 
(expected \"<trait>.<prop>=<value>\"): " + option)
-               }
-               id := parts[1]
-               fullProp := parts[2][1:]
-               value := parts[3]
-               if _, ok := traits[id]; !ok {
-                       traits[id] = make(map[string]interface{})
-               }
-
-               propParts := util.ConfigTreePropertySplit(fullProp)
-               var current = traits[id]
-               if len(propParts) > 1 {
-                       c, err := util.NavigateConfigTree(current, 
propParts[0:len(propParts)-1])
-                       if err != nil {
-                               return nil, err
-                       }
-                       if cc, ok := c.(map[string]interface{}); ok {
-                               current = cc
-                       } else {
-                               return nil, errors.New("trait configuration 
cannot end with a slice")
-                       }
-               }
-
-               prop := propParts[len(propParts)-1]
-               switch v := current[prop].(type) {
-               case []string:
-                       current[prop] = append(v, value)
-               case string:
-                       // Aggregate multiple occurrences of the same option 
into a string array, to emulate POSIX conventions.
-                       // This enables executing:
-                       // $ kamel run -t <trait>.<property>=<value_1> ... -t 
<trait>.<property>=<value_N>
-                       // Or:
-                       // $ kamel run --trait 
<trait>.<property>=<value_1>,...,<trait>.<property>=<value_N>
-                       current[prop] = []string{v, value}
-               case nil:
-                       current[prop] = value
-               }
-       }
-
-       specs := make(map[string]v1.TraitSpec)
-       for id, config := range traits {
-               t := catalog.GetTrait(id)
-               if t != nil {
-                       // let's take a clone to prevent default values set at 
runtime from being serialized
-                       zero := reflect.New(reflect.TypeOf(t)).Interface()
-                       err := configureTrait(config, zero)
-                       if err != nil {
-                               return nil, err
-                       }
-                       data, err := json.Marshal(zero)
-                       if err != nil {
-                               return nil, err
-                       }
-                       var spec v1.TraitSpec
-                       err = json.Unmarshal(data, &spec.Configuration)
-                       if err != nil {
-                               return nil, err
-                       }
-                       specs[id] = spec
-               }
-       }
-
-       return specs, nil
-}
-
-func configureTrait(config map[string]interface{}, trait interface{}) error {
-       md := mapstructure.Metadata{}
-
-       decoder, err := mapstructure.NewDecoder(
-               &mapstructure.DecoderConfig{
-                       Metadata:         &md,
-                       WeaklyTypedInput: true,
-                       TagName:          "property",
-                       Result:           &trait,
-               },
-       )
-       if err != nil {
-               return err
-       }
-
-       return decoder.Decode(config)
-}
diff --git a/pkg/cmd/run_test.go b/pkg/cmd/run_test.go
index 98740d6..7f277ed 100644
--- a/pkg/cmd/run_test.go
+++ b/pkg/cmd/run_test.go
@@ -19,8 +19,10 @@ package cmd
 
 import (
        "context"
+       "fmt"
        "io/ioutil"
        "os"
+       "path/filepath"
        "testing"
 
        v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
@@ -46,6 +48,17 @@ func initializeRunCmdOptions(t *testing.T) (*runCmdOptions, 
*cobra.Command, Root
        return runCmdOptions, rootCmd, *options
 }
 
+// nolint: unparam
+func initializeRunCmdOptionsWithOutput(t *testing.T) (*runCmdOptions, 
*cobra.Command, RootCmdOptions) {
+       t.Helper()
+
+       options, rootCmd := kamelTestPreAddCommandInit()
+       runCmdOptions := addTestRunCmdWithOutput(*options, rootCmd)
+       kamelTestPostAddCommandInit(t, rootCmd)
+
+       return runCmdOptions, rootCmd, *options
+}
+
 func addTestRunCmd(options RootCmdOptions, rootCmd *cobra.Command) 
*runCmdOptions {
        // add a testing version of run Command
        runCmd, runOptions := newCmdRun(&options)
@@ -60,6 +73,17 @@ func addTestRunCmd(options RootCmdOptions, rootCmd 
*cobra.Command) *runCmdOption
        return runOptions
 }
 
+func addTestRunCmdWithOutput(options RootCmdOptions, rootCmd *cobra.Command) 
*runCmdOptions {
+       // add a testing version of run Command with output
+       runCmd, runOptions := newCmdRun(&options)
+       runCmd.PersistentPreRunE = func(c *cobra.Command, args []string) error {
+               return nil
+       }
+       runCmd.Args = test.ArbitraryArgs
+       rootCmd.AddCommand(runCmd)
+       return runOptions
+}
+
 func TestRunNoFlag(t *testing.T) {
        runCmdOptions, rootCmd, _ := initializeRunCmdOptions(t)
        _, err := test.ExecuteCommand(rootCmd, cmdRun, integrationSource)
@@ -294,16 +318,27 @@ func TestRunSyncFlag(t *testing.T) {
        assert.Equal(t, true, runCmdOptions.Sync)
 }
 
-func TestRunTraitFlag(t *testing.T) {
+func TestRunExistingTraitFlag(t *testing.T) {
        runCmdOptions, rootCmd, _ := initializeRunCmdOptions(t)
        _, err := test.ExecuteCommand(rootCmd, cmdRun,
-               "--trait", "trait1",
-               "--trait", "trait2",
+               "--trait", "jvm.enabled",
+               "--trait", "logging.enabled",
                integrationSource)
        assert.Nil(t, err)
        assert.Len(t, runCmdOptions.Traits, 2)
-       assert.Equal(t, "trait1", runCmdOptions.Traits[0])
-       assert.Equal(t, "trait2", runCmdOptions.Traits[1])
+       assert.Equal(t, "jvm.enabled", runCmdOptions.Traits[0])
+       assert.Equal(t, "logging.enabled", runCmdOptions.Traits[1])
+}
+
+func TestRunMissingTraitFlag(t *testing.T) {
+       runCmdOptions, rootCmd, _ := initializeRunCmdOptions(t)
+       _, err := test.ExecuteCommand(rootCmd, cmdRun,
+               "--trait", "bogus.missing",
+               integrationSource)
+       assert.NotNil(t, err)
+       assert.Equal(t, "bogus.missing is not a valid trait property", 
err.Error())
+       assert.Len(t, runCmdOptions.Traits, 1)
+       assert.Equal(t, "bogus.missing", runCmdOptions.Traits[0])
 }
 
 func TestConfigureTraits(t *testing.T) {
@@ -373,9 +408,9 @@ func TestTraitsNestedConfig(t *testing.T) {
                "--trait", "custom.slice-of-map[3].f=h",
                "--trait", "custom.slice-of-map[2].f=i",
                "example.js")
-       if err != nil {
-               t.Error(err)
-       }
+       // We will have an error because those traits are not existing
+       // however we want to test how those properties are mapped in the 
configuration
+       assert.NotNil(t, err)
        catalog := &customTraitFinder{}
        traits, err := configureTraits(runCmdOptions.Traits, catalog)
 
@@ -448,51 +483,6 @@ func TestRunValidateArgs(t *testing.T) {
        assert.Equal(t, "One of the provided sources is not reachable: missing 
file or unsupported scheme in missing_file", err.Error())
 }
 
-//
-// This test does work when running as single test but fails
-// otherwise as we are using a global viper instance
-//
-
-/*
-const TestKamelConfigContent = `
-kamel:
-  install:
-    olm: false
-  run:
-    integration:
-      route:
-        sources:
-        - examples/dns.js
-        - examples/Sample.java
-`
-
-func TestRunWithSavedValues(t *testing.T) {
-       dir, err := ioutil.TempDir("", "run-")
-       assert.Nil(t, err)
-
-       defer func() {
-               _ = os.RemoveAll(dir)
-       }()
-
-       assert.Nil(t, os.Setenv("KAMEL_CONFIG_PATH", dir))
-       defer func() {
-               _ = os.Unsetenv("KAMEL_CONFIG_PATH")
-       }()
-
-       assert.Nil(t, ioutil.WriteFile(path.Join(dir, "kamel-config.yaml"), 
[]byte(TestKamelConfigContent), 0644))
-
-       options, rootCmd := kamelTestPreAddCommandInit()
-
-       runCmdOptions := addTestRunCmd(options, rootCmd)
-
-       kamelTestPostAddCommandInit(t, rootCmd)
-
-       _, err = test.ExecuteCommand(rootCmd, "run", "route.java")
-
-       assert.Nil(t, err)
-       assert.Len(t, runCmdOptions.Sources, 2)
-}*/
-
 func TestRunBinaryResource(t *testing.T) {
        binaryResourceSpec, err := binaryOrTextResource("file.ext", []byte{1, 
2, 3, 4}, "application/octet-stream", false, v1.ResourceTypeData, "")
        assert.Nil(t, err)
@@ -579,3 +569,103 @@ func TestFilterBuildPropertyFiles(t *testing.T) {
        assert.Equal(t, len(outputValues), 1)
        assert.Equal(t, outputValues[0], "/tmp/test")
 }
+
+const TestSrcContent = `
+import org.apache.camel.builder.RouteBuilder;
+
+public class Sample extends RouteBuilder {
+  @Override
+  public void configure() throws Exception {
+         from("timer:tick")
+        .log("Hello Camel K!");
+  }
+}
+`
+
+func TestOutputYaml(t *testing.T) {
+       var tmpFile *os.File
+       var err error
+       if tmpFile, err = ioutil.TempFile("", "camel-k-"); err != nil {
+               t.Error(err)
+       }
+
+       assert.Nil(t, tmpFile.Close())
+       assert.Nil(t, ioutil.WriteFile(tmpFile.Name(), []byte(TestSrcContent), 
0o400))
+       fileName := filepath.Base(tmpFile.Name())
+
+       buildCmdOptions, runCmd, _ := initializeRunCmdOptionsWithOutput(t)
+       output, err := test.ExecuteCommand(runCmd, cmdRun, tmpFile.Name(), 
"-o", "yaml")
+       assert.Equal(t, "yaml", buildCmdOptions.OutputFormat)
+
+       assert.Nil(t, err)
+       assert.Equal(t, fmt.Sprintf(`apiVersion: camel.apache.org/v1
+kind: Integration
+metadata:
+  creationTimestamp: null
+  name: %s
+spec:
+  sources:
+  - content: "\nimport org.apache.camel.builder.RouteBuilder;\n\npublic class 
Sample
+      extends RouteBuilder {\n  @Override\n  public void configure() throws 
Exception
+      {\n\t  from(\"timer:tick\")\n        .log(\"Hello Camel K!\");\n  }\n}\n"
+    name: %s
+status: {}
+`, fileName, fileName), output)
+}
+
+func TestTrait(t *testing.T) {
+       var tmpFile *os.File
+       var err error
+       if tmpFile, err = ioutil.TempFile("", "camel-k-"); err != nil {
+               t.Error(err)
+       }
+
+       assert.Nil(t, tmpFile.Close())
+       assert.Nil(t, ioutil.WriteFile(tmpFile.Name(), []byte(TestSrcContent), 
0o400))
+       fileName := filepath.Base(tmpFile.Name())
+
+       buildCmdOptions, runCmd, _ := initializeRunCmdOptionsWithOutput(t)
+       output, err := test.ExecuteCommand(runCmd, cmdRun, tmpFile.Name(), 
"-o", "yaml", "-t", "mount.configs=configmap:my-cm", "--connect", 
"my-service-binding")
+       assert.Equal(t, "yaml", buildCmdOptions.OutputFormat)
+
+       assert.Nil(t, err)
+       assert.Equal(t, fmt.Sprintf(`apiVersion: camel.apache.org/v1
+kind: Integration
+metadata:
+  creationTimestamp: null
+  name: %s
+spec:
+  sources:
+  - content: "\nimport org.apache.camel.builder.RouteBuilder;\n\npublic class 
Sample
+      extends RouteBuilder {\n  @Override\n  public void configure() throws 
Exception
+      {\n\t  from(\"timer:tick\")\n        .log(\"Hello Camel K!\");\n  }\n}\n"
+    name: %s
+  traits:
+    mount:
+      configuration:
+        configs:
+        - configmap:my-cm
+    service-binding:
+      configuration:
+        services:
+        - my-service-binding
+status: {}
+`, fileName, fileName), output)
+}
+
+func TestMissingTrait(t *testing.T) {
+       var tmpFile *os.File
+       var err error
+       if tmpFile, err = ioutil.TempFile("", "camel-k-"); err != nil {
+               t.Error(err)
+       }
+
+       assert.Nil(t, tmpFile.Close())
+       assert.Nil(t, ioutil.WriteFile(tmpFile.Name(), []byte(TestSrcContent), 
0o400))
+
+       buildCmdOptions, runCmd, _ := initializeRunCmdOptionsWithOutput(t)
+       output, err := test.ExecuteCommand(runCmd, cmdRun, tmpFile.Name(), 
"-o", "yaml", "-t", "bogus.fail=i-must-fail")
+       assert.Equal(t, "yaml", buildCmdOptions.OutputFormat)
+       assert.Equal(t, "Error: bogus.fail=i-must-fail is not a valid trait 
property\n", output)
+       assert.NotNil(t, err)
+}
diff --git a/pkg/cmd/trait_help.go b/pkg/cmd/trait_help.go
index 0ef7bc9..b56e8c6 100644
--- a/pkg/cmd/trait_help.go
+++ b/pkg/cmd/trait_help.go
@@ -26,12 +26,14 @@ import (
        "strings"
 
        "github.com/fatih/structs"
+       "github.com/mitchellh/mapstructure"
        "github.com/spf13/cobra"
        "gopkg.in/yaml.v2"
 
        v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
        "github.com/apache/camel-k/pkg/resources"
        "github.com/apache/camel-k/pkg/trait"
+       "github.com/apache/camel-k/pkg/util"
        "github.com/apache/camel-k/pkg/util/indentedwriter"
 )
 
@@ -261,3 +263,108 @@ func outputTraits(descriptions []*traitDescription) 
(string, error) {
                return nil
        })
 }
+
+func validateTraits(catalog *trait.Catalog, traits []string) error {
+       tp := catalog.ComputeTraitsProperties()
+       for _, t := range traits {
+               kv := strings.SplitN(t, "=", 2)
+               prefix := kv[0]
+               if strings.Contains(prefix, "[") {
+                       prefix = prefix[0:strings.Index(prefix, "[")]
+               }
+               if !util.StringSliceExists(tp, prefix) {
+                       return fmt.Errorf("%s is not a valid trait property", t)
+               }
+       }
+
+       return nil
+}
+
+func configureTraits(options []string, catalog trait.Finder) 
(map[string]v1.TraitSpec, error) {
+       traits := make(map[string]map[string]interface{})
+
+       for _, option := range options {
+               parts := traitConfigRegexp.FindStringSubmatch(option)
+               if len(parts) < 4 {
+                       return nil, errors.New("unrecognized config format 
(expected \"<trait>.<prop>=<value>\"): " + option)
+               }
+               id := parts[1]
+               fullProp := parts[2][1:]
+               value := parts[3]
+               if _, ok := traits[id]; !ok {
+                       traits[id] = make(map[string]interface{})
+               }
+
+               propParts := util.ConfigTreePropertySplit(fullProp)
+               var current = traits[id]
+               if len(propParts) > 1 {
+                       c, err := util.NavigateConfigTree(current, 
propParts[0:len(propParts)-1])
+                       if err != nil {
+                               return nil, err
+                       }
+                       if cc, ok := c.(map[string]interface{}); ok {
+                               current = cc
+                       } else {
+                               return nil, errors.New("trait configuration 
cannot end with a slice")
+                       }
+               }
+
+               prop := propParts[len(propParts)-1]
+               switch v := current[prop].(type) {
+               case []string:
+                       current[prop] = append(v, value)
+               case string:
+                       // Aggregate multiple occurrences of the same option 
into a string array, to emulate POSIX conventions.
+                       // This enables executing:
+                       // $ kamel run -t <trait>.<property>=<value_1> ... -t 
<trait>.<property>=<value_N>
+                       // Or:
+                       // $ kamel run --trait 
<trait>.<property>=<value_1>,...,<trait>.<property>=<value_N>
+                       current[prop] = []string{v, value}
+               case nil:
+                       current[prop] = value
+               }
+       }
+
+       specs := make(map[string]v1.TraitSpec)
+       for id, config := range traits {
+               t := catalog.GetTrait(id)
+               if t != nil {
+                       // let's take a clone to prevent default values set at 
runtime from being serialized
+                       zero := reflect.New(reflect.TypeOf(t)).Interface()
+                       err := configureTrait(config, zero)
+                       if err != nil {
+                               return nil, err
+                       }
+                       data, err := json.Marshal(zero)
+                       if err != nil {
+                               return nil, err
+                       }
+                       var spec v1.TraitSpec
+                       err = json.Unmarshal(data, &spec.Configuration)
+                       if err != nil {
+                               return nil, err
+                       }
+                       specs[id] = spec
+               }
+       }
+
+       return specs, nil
+}
+
+func configureTrait(config map[string]interface{}, trait interface{}) error {
+       md := mapstructure.Metadata{}
+
+       decoder, err := mapstructure.NewDecoder(
+               &mapstructure.DecoderConfig{
+                       Metadata:         &md,
+                       WeaklyTypedInput: true,
+                       TagName:          "property",
+                       Result:           &trait,
+               },
+       )
+       if err != nil {
+               return err
+       }
+
+       return decoder.Decode(config)
+}

Reply via email to