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") + } +}