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