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

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

commit b848abaab2832e5a86429befae578a1bccd3365f
Author: nicolaferraro <ni.ferr...@gmail.com>
AuthorDate: Mon Dec 13 16:40:08 2021 +0100

    Fix #1107: add support for nested trait configuration
---
 pkg/cmd/run.go               | 30 +++++++++++++----
 pkg/cmd/run_test.go          | 49 ++++++++++++++++++++++++++++
 pkg/trait/trait_catalog.go   |  6 ++++
 pkg/trait/trait_configure.go | 26 ++++++++++++---
 pkg/util/util.go             | 77 ++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 176 insertions(+), 12 deletions(-)

diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go
index 732e1da..5dfca09 100644
--- a/pkg/cmd/run.go
+++ b/pkg/cmd/run.go
@@ -54,7 +54,7 @@ import (
        "github.com/apache/camel-k/pkg/util/watch"
 )
 
-var traitConfigRegexp = 
regexp.MustCompile(`^([a-z0-9-]+)((?:\.[a-z0-9-]+)+)=(.*)$`)
+var traitConfigRegexp = 
regexp.MustCompile(`^([a-z0-9-]+)((?:\[[0-9]+\]|\.[a-z0-9-]+)+)=(.*)$`)
 
 func newCmdRun(rootCmdOptions *RootCmdOptions) (*cobra.Command, 
*runCmdOptions) {
        options := runCmdOptions{
@@ -794,7 +794,7 @@ func resolvePodTemplate(ctx context.Context, templateSrc 
string, spec *v1.Integr
        return err
 }
 
-func configureTraits(options []string, catalog *trait.Catalog) 
(map[string]v1.TraitSpec, error) {
+func configureTraits(options []string, catalog trait.Finder) 
(map[string]v1.TraitSpec, error) {
        traits := make(map[string]map[string]interface{})
 
        for _, option := range options {
@@ -803,23 +803,39 @@ func configureTraits(options []string, catalog 
*trait.Catalog) (map[string]v1.Tr
                        return nil, errors.New("unrecognized config format 
(expected \"<trait>.<prop>=<value>\"): " + option)
                }
                id := parts[1]
-               prop := parts[2][1:]
+               fullProp := parts[2][1:]
                value := parts[3]
                if _, ok := traits[id]; !ok {
                        traits[id] = make(map[string]interface{})
                }
-               switch v := traits[id][prop].(type) {
+
+               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:
-                       traits[id][prop] = append(v, value)
+                       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>
-                       traits[id][prop] = []string{v, value}
+                       current[prop] = []string{v, value}
                case nil:
-                       traits[id][prop] = value
+                       current[prop] = value
                }
        }
 
diff --git a/pkg/cmd/run_test.go b/pkg/cmd/run_test.go
index aa25816..dc35ee4 100644
--- a/pkg/cmd/run_test.go
+++ b/pkg/cmd/run_test.go
@@ -387,6 +387,55 @@ func TestConfigureTraits(t *testing.T) {
        assertTraitConfiguration(t, traits, "prometheus", 
`{"podMonitor":false}`)
 }
 
+type customTrait struct {
+       trait.BaseTrait `property:",squash"`
+       // SimpleMap
+       SimpleMap  map[string]string            `property:"simple-map" 
json:"simpleMap,omitempty"`
+       DoubleMap  map[string]map[string]string `property:"double-map" 
json:"doubleMap,omitempty"`
+       SliceOfMap []map[string]string          `property:"slice-of-map" 
json:"sliceOfMap,omitempty"`
+}
+
+func (c customTrait) Configure(environment *trait.Environment) (bool, error) {
+       panic("implement me")
+}
+func (c customTrait) Apply(environment *trait.Environment) error {
+       panic("implement me")
+}
+
+var _ trait.Trait = &customTrait{}
+
+type customTraitFinder struct {
+}
+
+func (finder customTraitFinder) GetTrait(id string) trait.Trait {
+       if id == "custom" {
+               return &customTrait{}
+       }
+       return nil
+}
+
+func TestTraitsNestedConfig(t *testing.T) {
+       runCmdOptions, rootCmd, _ := initializeRunCmdOptions(t)
+       _, err := test.ExecuteCommand(rootCmd, "run",
+               "--trait", "custom.simple-map.a=b",
+               "--trait", "custom.simple-map.y=z",
+               "--trait", "custom.double-map.m.n=q",
+               "--trait", "custom.double-map.m.o=w",
+               "--trait", "custom.slice-of-map[0].f=g",
+               "--trait", "custom.slice-of-map[3].f=h",
+               "--trait", "custom.slice-of-map[2].f=i",
+               "example.js")
+       if err != nil {
+               t.Error(err)
+       }
+       catalog := &customTraitFinder{}
+       traits, err := configureTraits(runCmdOptions.Traits, catalog)
+
+       assert.Nil(t, err)
+       assert.Len(t, traits, 1)
+       assertTraitConfiguration(t, traits, "custom", 
`{"simpleMap":{"a":"b","y":"z"},"doubleMap":{"m":{"n":"q","o":"w"}},"sliceOfMap":[{"f":"g"},null,{"f":"i"},{"f":"h"}]}`)
+}
+
 func assertTraitConfiguration(t *testing.T, traits map[string]v1.TraitSpec, 
trait string, expected string) {
        t.Helper()
 
diff --git a/pkg/trait/trait_catalog.go b/pkg/trait/trait_catalog.go
index d681592..7831a4f 100644
--- a/pkg/trait/trait_catalog.go
+++ b/pkg/trait/trait_catalog.go
@@ -182,3 +182,9 @@ func (c *Catalog) processFields(fields []*structs.Field, 
processor func(string))
                }
        }
 }
+
+type Finder interface {
+       GetTrait(id string) Trait
+}
+
+var _ Finder = &Catalog{}
diff --git a/pkg/trait/trait_configure.go b/pkg/trait/trait_configure.go
index c782797..1bba95b 100644
--- a/pkg/trait/trait_configure.go
+++ b/pkg/trait/trait_configure.go
@@ -23,6 +23,7 @@ import (
        "reflect"
        "strings"
 
+       "github.com/apache/camel-k/pkg/util"
        "github.com/mitchellh/mapstructure"
        "github.com/pkg/errors"
 
@@ -88,7 +89,7 @@ func decodeTraitSpec(in *v1.TraitSpec, target interface{}) 
error {
 }
 
 func (c *Catalog) configureTraitsFromAnnotations(annotations 
map[string]string) error {
-       options := make(map[string]map[string]string, len(annotations))
+       options := make(map[string]map[string]interface{}, len(annotations))
        for k, v := range annotations {
                if strings.HasPrefix(k, v1.TraitAnnotationPrefix) {
                        configKey := strings.TrimPrefix(k, 
v1.TraitAnnotationPrefix)
@@ -97,9 +98,24 @@ func (c *Catalog) configureTraitsFromAnnotations(annotations 
map[string]string)
                                id := parts[0]
                                prop := parts[1]
                                if _, ok := options[id]; !ok {
-                                       options[id] = make(map[string]string)
+                                       options[id] = 
make(map[string]interface{})
                                }
-                               options[id][prop] = v
+
+                               propParts := util.ConfigTreePropertySplit(prop)
+                               var current = options[id]
+                               if len(propParts) > 1 {
+                                       c, err := 
util.NavigateConfigTree(current, propParts[0:len(propParts)-1])
+                                       if err != nil {
+                                               return err
+                                       }
+                                       if cc, ok := 
c.(map[string]interface{}); ok {
+                                               current = cc
+                                       } else {
+                                               return errors.New(`invalid 
array specification: to set an array value use the ["v1", "v2"] format`)
+                                       }
+                               }
+                               current[prop] = v
+
                        } else {
                                return fmt.Errorf("wrong format for trait 
annotation %q: missing trait ID", k)
                        }
@@ -108,7 +124,7 @@ func (c *Catalog) 
configureTraitsFromAnnotations(annotations map[string]string)
        return c.configureFromOptions(options)
 }
 
-func (c *Catalog) configureFromOptions(traits map[string]map[string]string) 
error {
+func (c *Catalog) configureFromOptions(traits 
map[string]map[string]interface{}) error {
        for id, config := range traits {
                t := c.GetTrait(id)
                if t != nil {
@@ -121,7 +137,7 @@ func (c *Catalog) configureFromOptions(traits 
map[string]map[string]string) erro
        return nil
 }
 
-func configureTrait(id string, config map[string]string, trait interface{}) 
error {
+func configureTrait(id string, config map[string]interface{}, trait 
interface{}) error {
        md := mapstructure.Metadata{}
 
        var valueConverter mapstructure.DecodeHookFuncKind = func(sourceKind 
reflect.Kind, targetKind reflect.Kind, data interface{}) (interface{}, error) {
diff --git a/pkg/util/util.go b/pkg/util/util.go
index 2d2f73e..69fa4cb 100644
--- a/pkg/util/util.go
+++ b/pkg/util/util.go
@@ -29,6 +29,7 @@ import (
        "path/filepath"
        "regexp"
        "sort"
+       "strconv"
        "strings"
 
        "go.uber.org/multierr"
@@ -855,3 +856,79 @@ func WithTempDir(pattern string, consumer func(string) 
error) error {
 
        return multierr.Append(consumerErr, removeErr)
 }
+
+// Parses a property spec and returns its parts.
+func ConfigTreePropertySplit(property string) []string {
+       var res = make([]string, 0)
+       initialParts := strings.Split(property, ".")
+       for _, p := range initialParts {
+               cur := p
+               var tmp []string
+               for strings.Contains(cur[1:], "[") && strings.HasSuffix(cur, 
"]") {
+                       pos := strings.LastIndex(cur, "[")
+                       tmp = append(tmp, cur[pos:])
+                       cur = cur[0:pos]
+               }
+               if len(cur) > 0 {
+                       tmp = append(tmp, cur)
+               }
+               for i := len(tmp) - 1; i >= 0; i = i - 1 {
+                       res = append(res, tmp[i])
+               }
+       }
+       return res
+}
+
+// NavigateConfigTree switch to the element in the tree represented by the 
"nodes" spec and creates intermediary
+// nodes if missing. Nodes specs starting with "[" and ending in "]" are 
treated as slice indexes.
+func NavigateConfigTree(current interface{}, nodes []string) (interface{}, 
error) {
+       if len(nodes) == 0 {
+               return current, nil
+       }
+       isSlice := func(idx int) bool {
+               if idx >= len(nodes) {
+                       return false
+               }
+               return strings.HasPrefix(nodes[idx], "[") && 
strings.HasSuffix(nodes[idx], "]")
+       }
+       makeNext := func() interface{} {
+               if isSlice(1) {
+                       slice := make([]interface{}, 0)
+                       return &slice
+               } else {
+                       return make(map[string]interface{})
+               }
+       }
+       switch c := current.(type) {
+       case map[string]interface{}:
+               var next interface{}
+               if n, ok := c[nodes[0]]; ok {
+                       next = n
+               } else {
+                       next = makeNext()
+                       c[nodes[0]] = next
+               }
+               return NavigateConfigTree(next, nodes[1:])
+       case *[]interface{}:
+               if !isSlice(0) {
+                       return nil, fmt.Errorf("attempting to set map value %q 
into a slice", nodes[0])
+               }
+               pos, err := strconv.Atoi(nodes[0][1 : len(nodes[0])-1])
+               if err != nil {
+                       return nil, errors.Wrapf(err, "value %q inside brackets 
is not numeric", nodes[0])
+               }
+               var next interface{}
+               if len(*c) > pos && (*c)[pos] != nil {
+                       next = (*c)[pos]
+               } else {
+                       next = makeNext()
+                       for len(*c) <= pos {
+                               *c = append(*c, nil)
+                       }
+                       (*c)[pos] = next
+               }
+               return NavigateConfigTree(next, nodes[1:])
+       default:
+               return nil, errors.New("invalid node type in configuration")
+       }
+}

Reply via email to