This is an automated email from the ASF dual-hosted git repository. tsato pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-k.git
commit 69707cfb4bf27a1c124b37b4d199caf483a65d39 Author: Tadayoshi Sato <[email protected]> AuthorDate: Thu Jun 30 19:23:43 2022 +0900 feat(api): support addons in Traits schema --- addons/addons_test.go | 104 ++++++++++++++++++++ addons/master/test_support.go | 21 ++++ addons/tracing/test_support.go | 21 ++++ addons/tracing/tracing_test.go | 6 +- .../install/cli/duplicate_parameters_test.go | 2 +- .../install/cli/files/JavaDuplicateParams.java | 14 +-- e2e/namespace/native/native_test.go | 2 +- pkg/apis/camel/v1/common_types.go | 9 ++ pkg/apis/camel/v1/integrationkit_types.go | 3 + pkg/apis/camel/v1/trait/zz_generated.deepcopy.go | 50 ++++++++++ pkg/apis/camel/v1/zz_generated.deepcopy.go | 100 +++++++++++++++++++ pkg/cmd/bind.go | 3 +- pkg/cmd/kit_create.go | 2 +- pkg/cmd/run.go | 3 +- pkg/cmd/run_test.go | 7 +- pkg/cmd/trait_support.go | 97 ++++++++++++++++-- pkg/controller/integration/kits.go | 20 +++- pkg/trait/knative_test.go | 8 +- pkg/trait/test_support.go | 14 +++ pkg/trait/trait_catalog.go | 2 +- pkg/trait/trait_configure.go | 30 ++++-- pkg/trait/trait_configure_test.go | 46 ++++++++- pkg/trait/trait_test.go | 2 +- pkg/trait/util.go | 16 ++- pkg/trait/util_test.go | 108 +++++++++++++++++++++ 25 files changed, 642 insertions(+), 48 deletions(-) diff --git a/addons/addons_test.go b/addons/addons_test.go new file mode 100644 index 000000000..786172ee8 --- /dev/null +++ b/addons/addons_test.go @@ -0,0 +1,104 @@ +/* +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 addons + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/apache/camel-k/addons/master" + "github.com/apache/camel-k/addons/tracing" + v1 "github.com/apache/camel-k/pkg/apis/camel/v1" + "github.com/apache/camel-k/pkg/trait" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTraitConfiguration(t *testing.T) { + env := trait.Environment{ + Integration: &v1.Integration{ + Spec: v1.IntegrationSpec{ + Profile: v1.TraitProfileKubernetes, + Traits: v1.Traits{ + Addons: map[string]v1.AddonTrait{ + "master": trait.ToAddonTrait(t, map[string]interface{}{ + "enabled": true, + "resourceName": "test-lock", + "labelKey": "test-label", + "labelValue": "test-value", + }), + "tracing": trait.ToAddonTrait(t, map[string]interface{}{ + "enabled": true, + }), + }, + }, + }, + }, + } + c := trait.NewCatalog(nil) + require.NoError(t, c.Configure(&env)) + + require.NotNil(t, c.GetTrait("master")) + master, ok := c.GetTrait("master").(*master.TestMasterTrait) + require.True(t, ok) + assert.True(t, *master.Enabled) + assert.Equal(t, "test-lock", *master.ResourceName) + assert.Equal(t, "test-label", *master.LabelKey) + assert.Equal(t, "test-value", *master.LabelValue) + + require.NotNil(t, c.GetTrait("tracing")) + tracing, ok := c.GetTrait("tracing").(*tracing.TestTracingTrait) + require.True(t, ok) + assert.True(t, *tracing.Enabled) +} + +func TestTraitConfigurationFromAnnotations(t *testing.T) { + env := trait.Environment{ + Integration: &v1.Integration{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "trait.camel.apache.org/master.enabled": "true", + "trait.camel.apache.org/master.resource-name": "test-lock", + "trait.camel.apache.org/master.label-key": "test-label", + "trait.camel.apache.org/master.label-value": "test-value", + "trait.camel.apache.org/tracing.enabled": "true", + }, + }, + Spec: v1.IntegrationSpec{ + Profile: v1.TraitProfileKubernetes, + }, + }, + } + c := trait.NewCatalog(nil) + require.NoError(t, c.Configure(&env)) + + require.NotNil(t, c.GetTrait("master")) + master, ok := c.GetTrait("master").(*master.TestMasterTrait) + require.True(t, ok) + assert.True(t, *master.Enabled) + assert.Equal(t, "test-lock", *master.ResourceName) + assert.Equal(t, "test-label", *master.LabelKey) + assert.Equal(t, "test-value", *master.LabelValue) + + require.NotNil(t, c.GetTrait("tracing")) + tracing, ok := c.GetTrait("tracing").(*tracing.TestTracingTrait) + require.True(t, ok) + assert.True(t, *tracing.Enabled) +} diff --git a/addons/master/test_support.go b/addons/master/test_support.go new file mode 100644 index 000000000..ec6b7bf3b --- /dev/null +++ b/addons/master/test_support.go @@ -0,0 +1,21 @@ +/* +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 master + +// Expose masterTrait type for testing. +type TestMasterTrait = masterTrait diff --git a/addons/tracing/test_support.go b/addons/tracing/test_support.go new file mode 100644 index 000000000..90ba65bac --- /dev/null +++ b/addons/tracing/test_support.go @@ -0,0 +1,21 @@ +/* +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 tracing + +// Expose tracingTrait type for testing. +type TestTracingTrait = tracingTrait diff --git a/addons/tracing/tracing_test.go b/addons/tracing/tracing_test.go index e81707261..fbbb5bd56 100644 --- a/addons/tracing/tracing_test.go +++ b/addons/tracing/tracing_test.go @@ -20,14 +20,14 @@ package tracing import ( "testing" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" + v1 "github.com/apache/camel-k/pkg/apis/camel/v1" "github.com/apache/camel-k/pkg/trait" "github.com/apache/camel-k/pkg/util/camel" "github.com/stretchr/testify/assert" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/pointer" ) func TestTracingTraitOnQuarkus(t *testing.T) { diff --git a/e2e/namespace/install/cli/duplicate_parameters_test.go b/e2e/namespace/install/cli/duplicate_parameters_test.go index dbda54936..e6c52a21c 100644 --- a/e2e/namespace/install/cli/duplicate_parameters_test.go +++ b/e2e/namespace/install/cli/duplicate_parameters_test.go @@ -46,6 +46,6 @@ func TestDuplicateParameters(t *testing.T) { // the command is executed inside GetOutputString function commOutput := GetOutputString(comm) - outParams := `"traits":{"affinity":{"configuration":{"enabled":true}},"camel":{"configuration":{"properties":["prop1 = true","prop2 = true","foo = bar"]}},"pull-secret":{"configuration":{"enabled":true}},"tracing":{"configuration":{"enabled":true}}}` + outParams := `"traits":{"affinity":{"enabled":true},"camel":{"properties":["prop1 = true","prop2 = true","foo = bar"]},"pull-secret":{"enabled":true},"addons":{"tracing":{"enabled":true}}}` Expect(commOutput).To(ContainSubstring(outParams)) } diff --git a/e2e/namespace/install/cli/files/JavaDuplicateParams.java b/e2e/namespace/install/cli/files/JavaDuplicateParams.java index 465e1cc1c..95b73b1ac 100644 --- a/e2e/namespace/install/cli/files/JavaDuplicateParams.java +++ b/e2e/namespace/install/cli/files/JavaDuplicateParams.java @@ -20,11 +20,11 @@ import org.apache.camel.builder.RouteBuilder; public class JavaDuplicateParams extends RouteBuilder { - @Override - public void configure() throws Exception { - from("timer:tick") - .setHeader("m").constant("string!") - .setBody().simple("Magic${header.m}") - .log("${body}"); - } + @Override + public void configure() throws Exception { + from("timer:tick") + .setHeader("m").constant("string!") + .setBody().simple("Magic${header.m}") + .log("${body}"); + } } diff --git a/e2e/namespace/native/native_test.go b/e2e/namespace/native/native_test.go index 96c433e72..702d70617 100644 --- a/e2e/namespace/native/native_test.go +++ b/e2e/namespace/native/native_test.go @@ -69,7 +69,7 @@ func TestNativeIntegrations(t *testing.T) { // Check the fast-jar Kit is ready Eventually(Kits(ns, withFastJarLayout, KitWithPhase(v1.IntegrationKitPhaseReady)), - TestTimeoutMedium).Should(HaveLen(1)) + TestTimeoutVeryLong).Should(HaveLen(1)) fastJarKit := Kits(ns, withFastJarLayout, KitWithPhase(v1.IntegrationKitPhaseReady))()[0] // Check the Integration uses the fast-jar Kit diff --git a/pkg/apis/camel/v1/common_types.go b/pkg/apis/camel/v1/common_types.go index 12a4122d7..f49bc83e1 100644 --- a/pkg/apis/camel/v1/common_types.go +++ b/pkg/apis/camel/v1/common_types.go @@ -174,6 +174,9 @@ type Traits struct { // The configuration of Toleration trait Toleration *trait.TolerationTrait `property:"toleration" json:"toleration,omitempty"` + // The extension point with addon traits + Addons map[string]AddonTrait `json:"addons,omitempty"` + // Deprecated: for backward compatibility. Keda *TraitSpec `property:"keda" json:"keda,omitempty"` // Deprecated: for backward compatibility. @@ -186,6 +189,12 @@ type Traits struct { Tracing *TraitSpec `property:"tracing" json:"tracing,omitempty"` } +// AddonTrait represents the configuration of an addon trait +type AddonTrait struct { + // Generic raw message, typically a map containing the keys (trait parameters) and the values (either single text or array) + RawMessage `json:",inline"` +} + // A TraitSpec contains the configuration of a trait // Deprecated: superceded by each Trait type, left for backward compatibility. type TraitSpec struct { diff --git a/pkg/apis/camel/v1/integrationkit_types.go b/pkg/apis/camel/v1/integrationkit_types.go index e31e8c6c7..56f7e829a 100644 --- a/pkg/apis/camel/v1/integrationkit_types.go +++ b/pkg/apis/camel/v1/integrationkit_types.go @@ -75,6 +75,9 @@ type IntegrationKitTraits struct { // It's enabled by default. // NOTE: Compiling to a native executable, i.e. when using `package-type=native`, is only supported for kamelets, as well as YAML and XML integrations. It also requires at least 4GiB of memory, so the Pod running the native build, that is either the operator Pod, or the build Pod (depending on the build strategy configured for the platform), must have enough memory available. Quarkus *trait.QuarkusTrait `property:"quarkus" json:"quarkus,omitempty"` + + // The collection of addon trait configurations + Addons map[string]AddonTrait `json:"addons,omitempty"` } // IntegrationKitStatus defines the observed state of IntegrationKit diff --git a/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go b/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go index f5d4d636b..002296715 100644 --- a/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go +++ b/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go @@ -1,3 +1,4 @@ +//go:build !ignore_autogenerated // +build !ignore_autogenerated // Code generated by controller-gen. DO NOT EDIT. @@ -94,6 +95,26 @@ func (in *CamelTrait) DeepCopy() *CamelTrait { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Configuration) DeepCopyInto(out *Configuration) { + *out = *in + if in.RawMessage != nil { + in, out := &in.RawMessage, &out.RawMessage + *out = make(RawMessage, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Configuration. +func (in *Configuration) DeepCopy() *Configuration { + if in == nil { + return nil + } + out := new(Configuration) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ContainerTrait) DeepCopyInto(out *ContainerTrait) { *out = *in @@ -186,6 +207,11 @@ func (in *DependenciesTrait) DeepCopy() *DependenciesTrait { func (in *DeployerTrait) DeepCopyInto(out *DeployerTrait) { *out = *in in.Trait.DeepCopyInto(&out.Trait) + if in.UseSSA != nil { + in, out := &in.UseSSA, &out.UseSSA + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeployerTrait. @@ -820,6 +846,25 @@ func (in *QuarkusTrait) DeepCopy() *QuarkusTrait { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in RawMessage) DeepCopyInto(out *RawMessage) { + { + in := &in + *out = make(RawMessage, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RawMessage. +func (in RawMessage) DeepCopy() RawMessage { + if in == nil { + return nil + } + out := new(RawMessage) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RegistryTrait) DeepCopyInto(out *RegistryTrait) { *out = *in @@ -928,6 +973,11 @@ func (in *Trait) DeepCopyInto(out *Trait) { *out = new(bool) **out = **in } + if in.Configuration != nil { + in, out := &in.Configuration, &out.Configuration + *out = new(Configuration) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Trait. diff --git a/pkg/apis/camel/v1/zz_generated.deepcopy.go b/pkg/apis/camel/v1/zz_generated.deepcopy.go index c039f8e06..3a7ef475b 100644 --- a/pkg/apis/camel/v1/zz_generated.deepcopy.go +++ b/pkg/apis/camel/v1/zz_generated.deepcopy.go @@ -12,6 +12,26 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AddonTrait) DeepCopyInto(out *AddonTrait) { + *out = *in + if in.RawMessage != nil { + in, out := &in.RawMessage, &out.RawMessage + *out = make(RawMessage, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AddonTrait. +func (in *AddonTrait) DeepCopy() *AddonTrait { + if in == nil { + return nil + } + out := new(AddonTrait) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Artifact) DeepCopyInto(out *Artifact) { *out = *in @@ -813,6 +833,13 @@ func (in *IntegrationKitTraits) DeepCopyInto(out *IntegrationKitTraits) { *out = new(trait.QuarkusTrait) (*in).DeepCopyInto(*out) } + if in.Addons != nil { + in, out := &in.Addons, &out.Addons + *out = make(map[string]AddonTrait, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IntegrationKitTraits. @@ -1694,6 +1721,42 @@ func (in *Task) DeepCopy() *Task { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TraitConfiguration) DeepCopyInto(out *TraitConfiguration) { + *out = *in + if in.RawMessage != nil { + in, out := &in.RawMessage, &out.RawMessage + *out = make(RawMessage, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TraitConfiguration. +func (in *TraitConfiguration) DeepCopy() *TraitConfiguration { + if in == nil { + return nil + } + out := new(TraitConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TraitSpec) DeepCopyInto(out *TraitSpec) { + *out = *in + in.Configuration.DeepCopyInto(&out.Configuration) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TraitSpec. +func (in *TraitSpec) DeepCopy() *TraitSpec { + if in == nil { + return nil + } + out := new(TraitSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Traits) DeepCopyInto(out *Traits) { *out = *in @@ -1777,6 +1840,11 @@ func (in *Traits) DeepCopyInto(out *Traits) { *out = new(trait.JVMTrait) (*in).DeepCopyInto(*out) } + if in.Kamelets != nil { + in, out := &in.Kamelets, &out.Kamelets + *out = new(trait.KameletsTrait) + (*in).DeepCopyInto(*out) + } if in.Knative != nil { in, out := &in.Knative, &out.Knative *out = new(trait.KnativeTrait) @@ -1862,6 +1930,38 @@ func (in *Traits) DeepCopyInto(out *Traits) { *out = new(trait.TolerationTrait) (*in).DeepCopyInto(*out) } + if in.Addons != nil { + in, out := &in.Addons, &out.Addons + *out = make(map[string]AddonTrait, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.Keda != nil { + in, out := &in.Keda, &out.Keda + *out = new(TraitSpec) + (*in).DeepCopyInto(*out) + } + if in.Master != nil { + in, out := &in.Master, &out.Master + *out = new(TraitSpec) + (*in).DeepCopyInto(*out) + } + if in.Strimzi != nil { + in, out := &in.Strimzi, &out.Strimzi + *out = new(TraitSpec) + (*in).DeepCopyInto(*out) + } + if in.ThreeScale != nil { + in, out := &in.ThreeScale, &out.ThreeScale + *out = new(TraitSpec) + (*in).DeepCopyInto(*out) + } + if in.Tracing != nil { + in, out := &in.Tracing, &out.Tracing + *out = new(TraitSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Traits. diff --git a/pkg/cmd/bind.go b/pkg/cmd/bind.go index a5eac817e..2a1c4ad26 100644 --- a/pkg/cmd/bind.go +++ b/pkg/cmd/bind.go @@ -209,7 +209,8 @@ func (o *bindCmdOptions) run(cmd *cobra.Command, args []string) error { if binding.Spec.Integration == nil { binding.Spec.Integration = &v1.IntegrationSpec{} } - if err := configureTraits(o.Traits, &binding.Spec.Integration.Traits); err != nil { + catalog := trait.NewCatalog(client) + if err := configureTraits(o.Traits, &binding.Spec.Integration.Traits, catalog); err != nil { return err } } diff --git a/pkg/cmd/kit_create.go b/pkg/cmd/kit_create.go index 0979cf18e..b8eca7f2d 100644 --- a/pkg/cmd/kit_create.go +++ b/pkg/cmd/kit_create.go @@ -157,7 +157,7 @@ func (command *kitCreateCommandOptions) run(cmd *cobra.Command, args []string) e if err := command.parseAndConvertToTrait(command.Secrets, "mount.config"); err != nil { return err } - if err := configureTraits(command.Traits, &kit.Spec.Traits); err != nil { + if err := configureTraits(command.Traits, &kit.Spec.Traits, catalog); err != nil { return err } existed := false diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index e9e603976..bb5ff353c 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -668,7 +668,8 @@ func (o *runCmdOptions) createOrUpdateIntegration(cmd *cobra.Command, c client.C o.Traits = append(o.Traits, fmt.Sprintf("service-binding.services=%s", item)) } if len(o.Traits) > 0 { - if err := configureTraits(o.Traits, &integration.Spec.Traits); err != nil { + catalog := trait.NewCatalog(c) + if err := configureTraits(o.Traits, &integration.Spec.Traits, catalog); err != nil { return nil, err } } diff --git a/pkg/cmd/run_test.go b/pkg/cmd/run_test.go index a3a43aa3d..d826e2338 100644 --- a/pkg/cmd/run_test.go +++ b/pkg/cmd/run_test.go @@ -363,9 +363,14 @@ func TestConfigureTraits(t *testing.T) { if err != nil { t.Error(err) } + client, err := runCmdOptions.GetCmdClient() + if err != nil { + t.Error(err) + } + catalog := trait.NewCatalog(client) traits := v1.Traits{} - err = configureTraits(runCmdOptions.Traits, &traits) + err = configureTraits(runCmdOptions.Traits, &traits, catalog) assert.Nil(t, err) traitsMap, err := trait.ToMap(traits) diff --git a/pkg/cmd/trait_support.go b/pkg/cmd/trait_support.go index 4d134a3a5..e36ae09aa 100644 --- a/pkg/cmd/trait_support.go +++ b/pkg/cmd/trait_support.go @@ -18,10 +18,13 @@ limitations under the License. package cmd import ( + "encoding/json" "errors" "fmt" + "reflect" "strings" + v1 "github.com/apache/camel-k/pkg/apis/camel/v1" "github.com/apache/camel-k/pkg/trait" "github.com/apache/camel-k/pkg/util" "github.com/mitchellh/mapstructure" @@ -43,32 +46,68 @@ func validateTraits(catalog *trait.Catalog, traits []string) error { return nil } -func configureTraits(options []string, traits interface{}) error { - config := make(map[string]map[string]interface{}) +func configureTraits(options []string, traits interface{}, catalog trait.Finder) error { + config, err := optionsToMap(options) + if err != nil { + return err + } + + md := mapstructure.Metadata{} + decoder, err := mapstructure.NewDecoder( + &mapstructure.DecoderConfig{ + Metadata: &md, + WeaklyTypedInput: true, + TagName: "property", + Result: &traits, + }, + ) + if err != nil { + return err + } + + if err = decoder.Decode(config); err != nil { + return err + } + + if len(md.Unused) == 0 { + // No addons found + return nil + } + + addons := make(map[string]map[string]interface{}) + for _, id := range md.Unused { + addons[id] = config[id] + } + + return configureAddons(addons, traits, catalog) +} + +func optionsToMap(options []string) (map[string]map[string]interface{}, error) { + optionMap := make(map[string]map[string]interface{}) for _, option := range options { parts := traitConfigRegexp.FindStringSubmatch(option) if len(parts) < 4 { - return errors.New("unrecognized config format (expected \"<trait>.<prop>=<value>\"): " + option) + return nil, errors.New("unrecognized config format (expected \"<trait>.<prop>=<value>\"): " + option) } id := parts[1] fullProp := parts[2][1:] value := parts[3] - if _, ok := config[id]; !ok { - config[id] = make(map[string]interface{}) + if _, ok := optionMap[id]; !ok { + optionMap[id] = make(map[string]interface{}) } propParts := util.ConfigTreePropertySplit(fullProp) - var current = config[id] + var current = optionMap[id] if len(propParts) > 1 { c, err := util.NavigateConfigTree(current, propParts[0:len(propParts)-1]) if err != nil { - return err + return nil, err } if cc, ok := c.(map[string]interface{}); ok { current = cc } else { - return errors.New("trait configuration cannot end with a slice") + return nil, errors.New("trait configuration cannot end with a slice") } } @@ -88,18 +127,56 @@ func configureTraits(options []string, traits interface{}) error { } } + return optionMap, nil +} + +func configureAddons(config map[string]map[string]interface{}, traits interface{}, catalog trait.Finder) error { + // Addon traits require raw message mapping + addons := make(map[string]v1.AddonTrait) + for id, props := range config { + 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() + if err := configureAddon(props, zero); err != nil { + return err + } + data, err := json.Marshal(zero) + if err != nil { + return err + } + addon := v1.AddonTrait{} + if err = json.Unmarshal(data, &addon); err != nil { + return err + } + addons[id] = addon + } + } + if len(addons) > 0 { + if ts, ok := traits.(*v1.Traits); ok { + ts.Addons = addons + } + if ikts, ok := traits.(*v1.IntegrationKitTraits); ok { + ikts.Addons = addons + } + } + + return nil +} + +func configureAddon(props map[string]interface{}, trait interface{}) error { md := mapstructure.Metadata{} decoder, err := mapstructure.NewDecoder( &mapstructure.DecoderConfig{ Metadata: &md, WeaklyTypedInput: true, TagName: "property", - Result: &traits, + Result: &trait, }, ) if err != nil { return err } - return decoder.Decode(config) + return decoder.Decode(props) } diff --git a/pkg/controller/integration/kits.go b/pkg/controller/integration/kits.go index 5e8c97f4a..2f029b037 100644 --- a/pkg/controller/integration/kits.go +++ b/pkg/controller/integration/kits.go @@ -177,8 +177,8 @@ func hasMatchingTraits(traits interface{}, kitTraits interface{}) (bool, error) continue } id := string(t.ID()) - it, ok1 := traitsMap[id] - kt, ok2 := kitTraitsMap[id] + it, ok1 := findTrait(traitsMap, id) + kt, ok2 := findTrait(kitTraitsMap, id) if !ok1 && !ok2 { continue @@ -201,6 +201,22 @@ func hasMatchingTraits(traits interface{}, kitTraits interface{}) (bool, error) return true, nil } +func findTrait(traitsMap map[string]map[string]interface{}, id string) (map[string]interface{}, bool) { + if trait, ok := traitsMap[id]; ok { + return trait, true + } + + if addons, ok := traitsMap["addons"]; ok { + if addon, ok := addons[id]; ok { + if trait, ok := addon.(map[string]interface{}); ok { + return trait, true + } + } + } + + return nil, false +} + func matchesComparableTrait(ct trait.ComparableTrait, it map[string]interface{}, kt map[string]interface{}) (bool, error) { t1 := reflect.New(reflect.TypeOf(ct).Elem()).Interface() if err := trait.ToTrait(it, &t1); err != nil { diff --git a/pkg/trait/knative_test.go b/pkg/trait/knative_test.go index 590eeda63..de4c6d42d 100644 --- a/pkg/trait/knative_test.go +++ b/pkg/trait/knative_test.go @@ -106,7 +106,7 @@ func TestKnativeEnvConfigurationFromTrait(t *testing.T) { tc := NewCatalog(c) - err = tc.configure(&environment) + err = tc.Configure(&environment) assert.Nil(t, err) tr, _ := tc.GetTrait("knative").(*knativeTrait) @@ -227,7 +227,7 @@ func TestKnativeEnvConfigurationFromSource(t *testing.T) { tc := NewCatalog(c) - err = tc.configure(&environment) + err = tc.Configure(&environment) assert.Nil(t, err) tr, _ := tc.GetTrait("knative").(*knativeTrait) @@ -295,7 +295,7 @@ func TestKnativePlatformHttpConfig(t *testing.T) { tc := NewCatalog(c) - err = tc.configure(&environment) + err = tc.Configure(&environment) assert.Nil(t, err) err = tc.apply(&environment) @@ -342,7 +342,7 @@ func TestKnativePlatformHttpDependencies(t *testing.T) { tc := NewCatalog(c) - err = tc.configure(&environment) + err = tc.Configure(&environment) assert.Nil(t, err) err = tc.apply(&environment) diff --git a/pkg/trait/test_support.go b/pkg/trait/test_support.go index 5dd0aafb9..8bc15b37a 100644 --- a/pkg/trait/test_support.go +++ b/pkg/trait/test_support.go @@ -32,6 +32,7 @@ import ( traitv1 "github.com/apache/camel-k/pkg/apis/camel/v1/trait" "github.com/apache/camel-k/pkg/util/kubernetes" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -147,3 +148,16 @@ func traitToMap(t *testing.T, trait interface{}) map[string]interface{} { return traitMap } + +func ToAddonTrait(t *testing.T, config map[string]interface{}) v1.AddonTrait { + t.Helper() + + data, err := json.Marshal(config) + assert.NoError(t, err) + + var addon v1.AddonTrait + err = json.Unmarshal(data, &addon) + assert.NoError(t, err) + + return addon +} diff --git a/pkg/trait/trait_catalog.go b/pkg/trait/trait_catalog.go index 84386a2b7..f706fa959 100644 --- a/pkg/trait/trait_catalog.go +++ b/pkg/trait/trait_catalog.go @@ -88,7 +88,7 @@ func (c *Catalog) TraitsForProfile(profile v1.TraitProfile) []Trait { } func (c *Catalog) apply(environment *Environment) error { - if err := c.configure(environment); err != nil { + if err := c.Configure(environment); err != nil { return err } traits := c.traitsFor(environment) diff --git a/pkg/trait/trait_configure.go b/pkg/trait/trait_configure.go index fe0287a69..f50fc26d2 100644 --- a/pkg/trait/trait_configure.go +++ b/pkg/trait/trait_configure.go @@ -23,15 +23,15 @@ import ( "reflect" "strings" - "github.com/apache/camel-k/pkg/util" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" v1 "github.com/apache/camel-k/pkg/apis/camel/v1" + "github.com/apache/camel-k/pkg/util" ) -// configure reads trait configurations from environment and applies them to catalog. -func (c *Catalog) configure(env *Environment) error { +// Configure reads trait configurations from environment and applies them to catalog. +func (c *Catalog) Configure(env *Environment) error { if env.Platform != nil { if err := c.configureTraits(env.Platform.Status.Traits); err != nil { return err @@ -67,9 +67,17 @@ func (c *Catalog) configureTraits(traits interface{}) error { } for id, trait := range traitsMap { - t := trait // Avoid G601: Implicit memory aliasing in for loop - if catTrait := c.GetTrait(id); catTrait != nil { - if err := decodeTrait(t, catTrait, true); err != nil { + if id == "addons" { + continue + } + if err := c.configureTrait(id, trait); err != nil { + return err + } + } + // Addons + for id, trait := range traitsMap["addons"] { + if addons, ok := trait.(map[string]interface{}); ok { + if err := c.configureTrait(id, addons); err != nil { return err } } @@ -78,6 +86,16 @@ func (c *Catalog) configureTraits(traits interface{}) error { return nil } +func (c *Catalog) configureTrait(id string, trait map[string]interface{}) error { + if catTrait := c.GetTrait(id); catTrait != nil { + if err := decodeTrait(trait, catTrait, true); err != nil { + return err + } + } + + return nil +} + func decodeTrait(in map[string]interface{}, target Trait, root bool) error { // decode legacy configuration first if it exists if root && in["configuration"] != nil { diff --git a/pkg/trait/trait_configure_test.go b/pkg/trait/trait_configure_test.go index 125ddd42b..2c106fa39 100644 --- a/pkg/trait/trait_configure_test.go +++ b/pkg/trait/trait_configure_test.go @@ -30,6 +30,42 @@ import ( traitv1 "github.com/apache/camel-k/pkg/apis/camel/v1/trait" ) +func TestTraitConfiguration(t *testing.T) { + env := Environment{ + Integration: &v1.Integration{ + Spec: v1.IntegrationSpec{ + Profile: v1.TraitProfileKubernetes, + Traits: v1.Traits{ + Logging: &traitv1.LoggingTrait{ + JSON: pointer.Bool(true), + JSONPrettyPrint: pointer.Bool(false), + Level: "DEBUG", + }, + Service: &traitv1.ServiceTrait{ + Trait: traitv1.Trait{ + Enabled: pointer.Bool(true), + }, + Auto: pointer.Bool(true), + NodePort: pointer.Bool(false), + }, + }, + }, + }, + } + c := NewCatalog(nil) + assert.NoError(t, c.Configure(&env)) + logging, ok := c.GetTrait("logging").(*loggingTrait) + require.True(t, ok) + assert.True(t, *logging.JSON) + assert.False(t, *logging.JSONPrettyPrint) + assert.Equal(t, "DEBUG", logging.Level) + service, ok := c.GetTrait("service").(*serviceTrait) + require.True(t, ok) + assert.True(t, *service.Enabled) + assert.True(t, *service.Auto) + assert.False(t, *service.NodePort) +} + func TestTraitConfigurationFromAnnotations(t *testing.T) { env := Environment{ Integration: &v1.Integration{ @@ -51,7 +87,7 @@ func TestTraitConfigurationFromAnnotations(t *testing.T) { }, } c := NewCatalog(nil) - assert.NoError(t, c.configure(&env)) + assert.NoError(t, c.Configure(&env)) assert.True(t, *c.GetTrait("cron").(*cronTrait).Fallback) assert.Equal(t, "annotated-policy", c.GetTrait("cron").(*cronTrait).ConcurrencyPolicy) assert.True(t, *c.GetTrait("environment").(*environmentTrait).ContainerMeta) @@ -71,7 +107,7 @@ func TestFailOnWrongTraitAnnotations(t *testing.T) { }, } c := NewCatalog(nil) - assert.Error(t, c.configure(&env)) + assert.Error(t, c.Configure(&env)) } func TestTraitConfigurationOverrideRulesFromAnnotations(t *testing.T) { @@ -127,7 +163,7 @@ func TestTraitConfigurationOverrideRulesFromAnnotations(t *testing.T) { }, } c := NewCatalog(nil) - assert.NoError(t, c.configure(&env)) + assert.NoError(t, c.Configure(&env)) assert.Equal(t, "schedule2", c.GetTrait("cron").(*cronTrait).Schedule) assert.Equal(t, "cmp4", c.GetTrait("cron").(*cronTrait).Components) assert.Equal(t, "policy4", c.GetTrait("cron").(*cronTrait).ConcurrencyPolicy) @@ -149,7 +185,7 @@ func TestTraitListConfigurationFromAnnotations(t *testing.T) { }, } c := NewCatalog(nil) - assert.NoError(t, c.configure(&env)) + assert.NoError(t, c.Configure(&env)) assert.Equal(t, []string{"opt1", "opt2"}, c.GetTrait("jolokia").(*jolokiaTrait).Options) assert.Equal(t, []string{"Binding:xxx"}, c.GetTrait("service-binding").(*serviceBindingTrait).Services) } @@ -168,7 +204,7 @@ func TestTraitSplitConfiguration(t *testing.T) { }, } c := NewCatalog(nil) - assert.NoError(t, c.configure(&env)) + assert.NoError(t, c.Configure(&env)) assert.Equal(t, []string{"opt1", "opt2"}, c.GetTrait("owner").(*ownerTrait).TargetLabels) } diff --git a/pkg/trait/trait_test.go b/pkg/trait/trait_test.go index b55b43eba..2b9f23073 100644 --- a/pkg/trait/trait_test.go +++ b/pkg/trait/trait_test.go @@ -158,7 +158,7 @@ func TestTraitHierarchyDecode(t *testing.T) { } c := NewTraitTestCatalog() - err := c.configure(env) + err := c.Configure(env) assert.Nil(t, err) diff --git a/pkg/trait/util.go b/pkg/trait/util.go index 8b56e6f78..17e43a853 100644 --- a/pkg/trait/util.go +++ b/pkg/trait/util.go @@ -224,12 +224,22 @@ func AddSourceDependencies(source v1.SourceSpec, catalog *camel.RuntimeCatalog) return dependencies } -// ToMap accepts either v1.Traits or v1.IntegrationKitTraits and converts it to a map. -func ToMap(traits interface{}) (map[string]map[string]interface{}, error) { +// AssertTraitsType asserts that traits is either v1.Traits or v1.IntegrationKitTraits. +// This function is provided because Go doesn't have Either nor union types. +func AssertTraitsType(traits interface{}) error { _, ok1 := traits.(v1.Traits) _, ok2 := traits.(v1.IntegrationKitTraits) if !ok1 && !ok2 { - return nil, errors.New("traits must be either v1.Traits or v1.IntegrationKitTraits") + return errors.New("traits must be either v1.Traits or v1.IntegrationKitTraits") + } + + return nil +} + +// ToMap accepts either v1.Traits or v1.IntegrationKitTraits and converts it to a map. +func ToMap(traits interface{}) (map[string]map[string]interface{}, error) { + if err := AssertTraitsType(traits); err != nil { + return nil, err } data, err := json.Marshal(traits) diff --git a/pkg/trait/util_test.go b/pkg/trait/util_test.go new file mode 100644 index 000000000..3a56bf029 --- /dev/null +++ b/pkg/trait/util_test.go @@ -0,0 +1,108 @@ +/* +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 trait + +import ( + "testing" + + "k8s.io/utils/pointer" + + v1 "github.com/apache/camel-k/pkg/apis/camel/v1" + traitv1 "github.com/apache/camel-k/pkg/apis/camel/v1/trait" + + "github.com/stretchr/testify/assert" +) + +func TestToMap(t *testing.T) { + traits := v1.Traits{ + Container: &traitv1.ContainerTrait{ + Trait: traitv1.Trait{ + Enabled: pointer.Bool(true), + }, + Auto: pointer.Bool(false), + Expose: pointer.Bool(true), + Port: 8081, + PortName: "http-8081", + ServicePort: 81, + ServicePortName: "http-81", + }, + Service: &traitv1.ServiceTrait{ + Trait: traitv1.Trait{ + Enabled: pointer.Bool(true), + }, + }, + Addons: map[string]v1.AddonTrait{ + "tracing": ToAddonTrait(t, map[string]interface{}{ + "enabled": true, + }), + }, + } + expected := map[string]map[string]interface{}{ + "container": { + "enabled": true, + "auto": false, + "expose": true, + "port": float64(8081), + "portName": "http-8081", + "servicePort": float64(81), + "servicePortName": "http-81", + }, + "service": { + "enabled": true, + }, + "addons": { + "tracing": map[string]interface{}{ + "enabled": true, + }, + }, + } + + traitMap, err := ToMap(traits) + + assert.NoError(t, err) + assert.Equal(t, expected, traitMap) +} + +func TestToTrait(t *testing.T) { + config := map[string]interface{}{ + "enabled": true, + "auto": false, + "expose": true, + "port": 8081, + "portName": "http-8081", + "servicePort": 81, + "servicePortName": "http-81", + } + expected := traitv1.ContainerTrait{ + Trait: traitv1.Trait{ + Enabled: pointer.Bool(true), + }, + Auto: pointer.Bool(false), + Expose: pointer.Bool(true), + Port: 8081, + PortName: "http-8081", + ServicePort: 81, + ServicePortName: "http-81", + } + + trait := traitv1.ContainerTrait{} + err := ToTrait(config, &trait) + + assert.NoError(t, err) + assert.Equal(t, expected, trait) +}
