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

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

commit b1b70e0fbb04b4c445a0e87b82f2c87c64cf3f6e
Author: Luca Burgazzoli <lburgazz...@gmail.com>
AuthorDate: Sun Jul 24 15:04:37 2022 +0200

    fix: camel-k ignores changes to traits configured using annotations #3479
---
 .../common/kamelet_binding_with_image_test.go      | 120 +++++++++++
 e2e/support/test_support.go                        |  40 ++++
 .../integration/integration_controller.go          | 203 +++++++++++--------
 pkg/controller/integration/kits.go                 |  69 ++++---
 pkg/controller/integration/kits_test.go            |  10 +-
 .../kameletbinding/kamelet_binding_controller.go   |  15 ++
 pkg/controller/kameletbinding/monitor.go           |  16 +-
 pkg/trait/util.go                                  | 219 ++++++++++++++++++++-
 pkg/trait/util_test.go                             | 177 ++++++++++++++++-
 pkg/util/digest/digest.go                          |   4 +
 pkg/util/kubernetes/util.go                        |   3 +-
 11 files changed, 748 insertions(+), 128 deletions(-)

diff --git a/e2e/global/common/kamelet_binding_with_image_test.go 
b/e2e/global/common/kamelet_binding_with_image_test.go
new file mode 100644
index 000000000..129089e51
--- /dev/null
+++ b/e2e/global/common/kamelet_binding_with_image_test.go
@@ -0,0 +1,120 @@
+//go:build integration
+// +build integration
+
+// To enable compilation of this file in Goland, go to "Settings -> Go -> 
Vendoring & Build Tags -> Custom Tags" and add "integration"
+
+/*
+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 common
+
+import (
+       "github.com/onsi/gomega/gstruct"
+       "testing"
+
+       . "github.com/apache/camel-k/e2e/support"
+       . "github.com/onsi/gomega"
+
+       corev1 "k8s.io/api/core/v1"
+
+       "github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+)
+
+func TestBindingWithImage(t *testing.T) {
+       WithNewTestNamespace(t, func(ns string) {
+               operatorID := "camel-k-binding-image"
+               bindingID := "with-image-binding"
+
+               Expect(KamelInstallWithID(operatorID, 
ns).Execute()).To(Succeed())
+
+               from := corev1.ObjectReference{
+                       Kind:       "Kamelet",
+                       Name:       "my-own-timer-source",
+                       APIVersion: v1alpha1.SchemeGroupVersion.String(),
+               }
+
+               to := corev1.ObjectReference{
+                       Kind:       "Kamelet",
+                       Name:       "my-own-log-sink",
+                       APIVersion: v1alpha1.SchemeGroupVersion.String(),
+               }
+
+               emptyMap := map[string]string{}
+
+               annotations1 := map[string]string{
+                       "trait.camel.apache.org/container.image":      
"docker.io/jmalloc/echo-server:0.3.2",
+                       "trait.camel.apache.org/jvm.enabled":          "false",
+                       "trait.camel.apache.org/kamelets.enabled":     "false",
+                       "trait.camel.apache.org/dependencies.enabled": "false",
+                       "test": "1",
+               }
+               annotations2 := map[string]string{
+                       "trait.camel.apache.org/container.image":      
"docker.io/jmalloc/echo-server:0.3.3",
+                       "trait.camel.apache.org/jvm.enabled":          "false",
+                       "trait.camel.apache.org/kamelets.enabled":     "false",
+                       "trait.camel.apache.org/dependencies.enabled": "false",
+                       "test": "2",
+               }
+
+               t.Run("run with initial image", func(t *testing.T) {
+                       expectedImage := 
annotations1["trait.camel.apache.org/container.image"]
+
+                       RegisterTestingT(t)
+
+                       Expect(BindKameletTo(ns, bindingID, annotations1, from, 
to, emptyMap, emptyMap)()).
+                               To(Succeed())
+                       Eventually(IntegrationGeneration(ns, bindingID)).
+                               Should(gstruct.PointTo(BeNumerically("==", 1)))
+                       Eventually(IntegrationAnnotations(ns, bindingID)).
+                               Should(HaveKeyWithValue("test", "1"))
+                       Eventually(IntegrationAnnotations(ns, bindingID)).
+                               
Should(HaveKeyWithValue("trait.camel.apache.org/container.image", 
expectedImage))
+                       Eventually(IntegrationStatusImage(ns, bindingID)).
+                               Should(Equal(expectedImage))
+
+                       Eventually(IntegrationPodPhase(ns, bindingID), 
TestTimeoutLong).
+                               Should(Equal(corev1.PodRunning))
+                       Eventually(IntegrationPodImage(ns, bindingID)).
+                               Should(Equal(expectedImage))
+               })
+
+               t.Run("run with new image", func(t *testing.T) {
+                       expectedImage := 
annotations2["trait.camel.apache.org/container.image"]
+
+                       RegisterTestingT(t)
+
+                       Expect(BindKameletTo(ns, bindingID, annotations2, from, 
to, emptyMap, emptyMap)()).
+                               To(Succeed())
+                       Eventually(IntegrationGeneration(ns, bindingID)).
+                               Should(gstruct.PointTo(BeNumerically("==", 1)))
+                       Eventually(IntegrationAnnotations(ns, bindingID)).
+                               Should(HaveKeyWithValue("test", "2"))
+                       Eventually(IntegrationAnnotations(ns, bindingID)).
+                               
Should(HaveKeyWithValue("trait.camel.apache.org/container.image", 
expectedImage))
+                       Eventually(IntegrationStatusImage(ns, bindingID)).
+                               Should(Equal(expectedImage))
+
+                       Eventually(IntegrationPodPhase(ns, bindingID), 
TestTimeoutLong).
+                               Should(Equal(corev1.PodRunning))
+                       Eventually(IntegrationPodImage(ns, bindingID)).
+                               Should(Equal(expectedImage))
+               })
+
+               // Cleanup
+               Expect(Kamel("delete", "--all", "-n", 
ns).Execute()).Should(BeNil())
+       })
+}
diff --git a/e2e/support/test_support.go b/e2e/support/test_support.go
index 1d690cfb1..8cfa8d1fa 100644
--- a/e2e/support/test_support.go
+++ b/e2e/support/test_support.go
@@ -545,6 +545,26 @@ func IntegrationSpecReplicas(ns string, name string) 
func() *int32 {
        }
 }
 
+func IntegrationGeneration(ns string, name string) func() *int64 {
+       return func() *int64 {
+               it := Integration(ns, name)()
+               if it == nil {
+                       return nil
+               }
+               return &it.Generation
+       }
+}
+
+func IntegrationStatusObserverGeneration(ns string, name string) func() *int64 
{
+       return func() *int64 {
+               it := Integration(ns, name)()
+               if it == nil {
+                       return nil
+               }
+               return &it.Status.ObservedGeneration
+       }
+}
+
 func IntegrationStatusReplicas(ns string, name string) func() *int32 {
        return func() *int32 {
                it := Integration(ns, name)()
@@ -555,6 +575,26 @@ func IntegrationStatusReplicas(ns string, name string) 
func() *int32 {
        }
 }
 
+func IntegrationStatusImage(ns string, name string) func() string {
+       return func() string {
+               it := Integration(ns, name)()
+               if it == nil {
+                       return ""
+               }
+               return it.Status.Image
+       }
+}
+
+func IntegrationAnnotations(ns string, name string) func() map[string]string {
+       return func() map[string]string {
+               it := Integration(ns, name)()
+               if it == nil {
+                       return map[string]string{}
+               }
+               return it.Annotations
+       }
+}
+
 func IntegrationCondition(ns string, name string, conditionType 
v1.IntegrationConditionType) func() *v1.IntegrationCondition {
        return func() *v1.IntegrationCondition {
                it := Integration(ns, name)()
diff --git a/pkg/controller/integration/integration_controller.go 
b/pkg/controller/integration/integration_controller.go
index c17a1babf..0d0f61d20 100644
--- a/pkg/controller/integration/integration_controller.go
+++ b/pkg/controller/integration/integration_controller.go
@@ -23,6 +23,8 @@ import (
        "reflect"
        "time"
 
+       "github.com/apache/camel-k/pkg/trait"
+
        appsv1 "k8s.io/api/apps/v1"
        batchv1 "k8s.io/api/batch/v1"
        corev1 "k8s.io/api/core/v1"
@@ -75,6 +77,115 @@ func newReconciler(mgr manager.Manager, c client.Client) 
reconcile.Reconciler {
        )
 }
 
+func integrationUpdateFunc(old *v1.Integration, it *v1.Integration) bool {
+       // Observe the time to first readiness metric
+       previous := old.Status.GetCondition(v1.IntegrationConditionReady)
+       if next := it.Status.GetCondition(v1.IntegrationConditionReady); 
(previous == nil || previous.Status != corev1.ConditionTrue && 
(previous.FirstTruthyTime == nil || previous.FirstTruthyTime.IsZero())) &&
+               next != nil && next.Status == corev1.ConditionTrue && 
next.FirstTruthyTime != nil && !next.FirstTruthyTime.IsZero() &&
+               it.Status.InitializationTimestamp != nil {
+               duration := 
next.FirstTruthyTime.Time.Sub(it.Status.InitializationTimestamp.Time)
+               Log.WithValues("request-namespace", it.Namespace, 
"request-name", it.Name, "ready-after", duration.Seconds()).
+                       ForIntegration(it).Infof("First readiness after %s", 
duration)
+               timeToFirstReadiness.Observe(duration.Seconds())
+       }
+
+       // If traits have changed, the reconciliation loop must kick in as
+       // traits may have impact
+       sameTraits, err := trait.IntegrationsHaveSameTraits(old, it)
+       if err != nil {
+               Log.ForIntegration(it).Error(
+                       err,
+                       "unable to determine if old and new resource have the 
same traits")
+       }
+       if !sameTraits {
+               return true
+       }
+
+       // Ignore updates to the integration status in which case 
metadata.Generation does not change,
+       // or except when the integration phase changes as it's used to 
transition from one phase
+       // to another.
+       return old.Generation != it.Generation ||
+               old.Status.Phase != it.Status.Phase
+}
+
+func integrationKitEnqueueRequestsFromMapFunc(c client.Client, kit 
*v1.IntegrationKit) []reconcile.Request {
+       var requests []reconcile.Request
+       if kit.Status.Phase != v1.IntegrationKitPhaseReady && kit.Status.Phase 
!= v1.IntegrationKitPhaseError {
+               return requests
+       }
+
+       list := &v1.IntegrationList{}
+       // Do global search in case of global operator (it may be using a 
global platform)
+       var opts []ctrl.ListOption
+       if !platform.IsCurrentOperatorGlobal() {
+               opts = append(opts, ctrl.InNamespace(kit.Namespace))
+       }
+       if err := c.List(context.Background(), list, opts...); err != nil {
+               log.Error(err, "Failed to retrieve integration list")
+               return requests
+       }
+
+       for i := range list.Items {
+               integration := &list.Items[i]
+               log.Debug("Integration Controller: Assessing integration", 
"integration", integration.Name, "namespace", integration.Namespace)
+
+               match, err := sameOrMatch(kit, integration)
+               if err != nil {
+                       log.Errorf(err, "Error matching integration %q with kit 
%q", integration.Name, kit.Name)
+                       continue
+               }
+               if !match {
+                       continue
+               }
+
+               if integration.Status.Phase == v1.IntegrationPhaseBuildingKit ||
+                       integration.Status.Phase == v1.IntegrationPhaseRunning {
+                       log.Infof("Kit %s ready, notify integration: %s", 
kit.Name, integration.Name)
+                       requests = append(requests, reconcile.Request{
+                               NamespacedName: types.NamespacedName{
+                                       Namespace: integration.Namespace,
+                                       Name:      integration.Name,
+                               },
+                       })
+               }
+       }
+
+       return requests
+}
+
+func integrationPlatformEnqueueRequestsFromMapFunc(c client.Client, p 
*v1.IntegrationPlatform) []reconcile.Request {
+       var requests []reconcile.Request
+
+       if p.Status.Phase == v1.IntegrationPlatformPhaseReady {
+               list := &v1.IntegrationList{}
+
+               // Do global search in case of global operator (it may be using 
a global platform)
+               var opts []ctrl.ListOption
+               if !platform.IsCurrentOperatorGlobal() {
+                       opts = append(opts, ctrl.InNamespace(p.Namespace))
+               }
+
+               if err := c.List(context.Background(), list, opts...); err != 
nil {
+                       log.Error(err, "Failed to list integrations")
+                       return requests
+               }
+
+               for _, integration := range list.Items {
+                       if integration.Status.Phase == 
v1.IntegrationPhaseWaitingForPlatform {
+                               log.Infof("Platform %s ready, wake-up 
integration: %s", p.Name, integration.Name)
+                               requests = append(requests, reconcile.Request{
+                                       NamespacedName: types.NamespacedName{
+                                               Namespace: 
integration.Namespace,
+                                               Name:      integration.Name,
+                                       },
+                               })
+                       }
+               }
+       }
+
+       return requests
+}
+
 func add(mgr manager.Manager, c client.Client, r reconcile.Reconciler) error {
        b := builder.ControllerManagedBy(mgr).
                Named("integration-controller").
@@ -90,21 +201,8 @@ func add(mgr manager.Manager, c client.Client, r 
reconcile.Reconciler) error {
                                        if !ok {
                                                return false
                                        }
-                                       // Observe the time to first readiness 
metric
-                                       previous := 
old.Status.GetCondition(v1.IntegrationConditionReady)
-                                       if next := 
it.Status.GetCondition(v1.IntegrationConditionReady); (previous == nil || 
previous.Status != corev1.ConditionTrue && (previous.FirstTruthyTime == nil || 
previous.FirstTruthyTime.IsZero())) &&
-                                               next != nil && next.Status == 
corev1.ConditionTrue && next.FirstTruthyTime != nil && 
!next.FirstTruthyTime.IsZero() &&
-                                               
it.Status.InitializationTimestamp != nil {
-                                               duration := 
next.FirstTruthyTime.Time.Sub(it.Status.InitializationTimestamp.Time)
-                                               
Log.WithValues("request-namespace", it.Namespace, "request-name", it.Name, 
"ready-after", duration.Seconds()).
-                                                       
ForIntegration(it).Infof("First readiness after %s", duration)
-                                               
timeToFirstReadiness.Observe(duration.Seconds())
-                                       }
-                                       // Ignore updates to the integration 
status in which case metadata.Generation does not change,
-                                       // or except when the integration phase 
changes as it's used to transition from one phase
-                                       // to another.
-                                       return old.Generation != it.Generation 
||
-                                               old.Status.Phase != 
it.Status.Phase
+
+                                       return integrationUpdateFunc(old, it)
                                },
                                DeleteFunc: func(e event.DeleteEvent) bool {
                                        // Evaluates to false if the object has 
been confirmed deleted
@@ -116,92 +214,25 @@ func add(mgr manager.Manager, c client.Client, r 
reconcile.Reconciler) error {
                // or running phase.
                Watches(&source.Kind{Type: &v1.IntegrationKit{}},
                        handler.EnqueueRequestsFromMapFunc(func(a ctrl.Object) 
[]reconcile.Request {
-                               var requests []reconcile.Request
                                kit, ok := a.(*v1.IntegrationKit)
                                if !ok {
                                        log.Error(fmt.Errorf("type assertion 
failed: %v", a), "Failed to retrieve integration list")
-                                       return requests
-                               }
-
-                               if kit.Status.Phase != 
v1.IntegrationKitPhaseReady && kit.Status.Phase != v1.IntegrationKitPhaseError {
-                                       return requests
-                               }
-
-                               list := &v1.IntegrationList{}
-                               // Do global search in case of global operator 
(it may be using a global platform)
-                               var opts []ctrl.ListOption
-                               if !platform.IsCurrentOperatorGlobal() {
-                                       opts = append(opts, 
ctrl.InNamespace(kit.Namespace))
-                               }
-                               if err := c.List(context.Background(), list, 
opts...); err != nil {
-                                       log.Error(err, "Failed to retrieve 
integration list")
-                                       return requests
-                               }
-
-                               for i := range list.Items {
-                                       integration := &list.Items[i]
-                                       log.Debug("Integration Controller: 
Assessing integration", "integration", integration.Name, "namespace", 
integration.Namespace)
-
-                                       if match, err := 
integrationMatches(integration, kit); err != nil {
-                                               log.Errorf(err, "Error matching 
integration %q with kit %q", integration.Name, kit.Name)
-
-                                               continue
-                                       } else if !match {
-                                               continue
-                                       }
-                                       if integration.Status.Phase == 
v1.IntegrationPhaseBuildingKit ||
-                                               integration.Status.Phase == 
v1.IntegrationPhaseRunning {
-                                               log.Infof("Kit %s ready, notify 
integration: %s", kit.Name, integration.Name)
-                                               requests = append(requests, 
reconcile.Request{
-                                                       NamespacedName: 
types.NamespacedName{
-                                                               Namespace: 
integration.Namespace,
-                                                               Name:      
integration.Name,
-                                                       },
-                                               })
-                                       }
+                                       return []reconcile.Request{}
                                }
 
-                               return requests
+                               return 
integrationKitEnqueueRequestsFromMapFunc(c, kit)
                        })).
                // Watch for IntegrationPlatform phase transitioning to ready 
and enqueue
                // requests for any integrations that are in phase waiting for 
platform
                Watches(&source.Kind{Type: &v1.IntegrationPlatform{}},
                        handler.EnqueueRequestsFromMapFunc(func(a ctrl.Object) 
[]reconcile.Request {
-                               var requests []reconcile.Request
                                p, ok := a.(*v1.IntegrationPlatform)
                                if !ok {
                                        log.Error(fmt.Errorf("type assertion 
failed: %v", a), "Failed to list integrations")
-                                       return requests
-                               }
-
-                               if p.Status.Phase == 
v1.IntegrationPlatformPhaseReady {
-                                       list := &v1.IntegrationList{}
-
-                                       // Do global search in case of global 
operator (it may be using a global platform)
-                                       var opts []ctrl.ListOption
-                                       if !platform.IsCurrentOperatorGlobal() {
-                                               opts = append(opts, 
ctrl.InNamespace(p.Namespace))
-                                       }
-
-                                       if err := c.List(context.Background(), 
list, opts...); err != nil {
-                                               log.Error(err, "Failed to list 
integrations")
-                                               return requests
-                                       }
-
-                                       for _, integration := range list.Items {
-                                               if integration.Status.Phase == 
v1.IntegrationPhaseWaitingForPlatform {
-                                                       log.Infof("Platform %s 
ready, wake-up integration: %s", p.Name, integration.Name)
-                                                       requests = 
append(requests, reconcile.Request{
-                                                               NamespacedName: 
types.NamespacedName{
-                                                                       
Namespace: integration.Namespace,
-                                                                       Name:   
   integration.Name,
-                                                               },
-                                                       })
-                                               }
-                                       }
+                                       return []reconcile.Request{}
                                }
 
-                               return requests
+                               return 
integrationPlatformEnqueueRequestsFromMapFunc(c, p)
                        })).
                // Watch for the owned Deployments
                Owns(&appsv1.Deployment{}, 
builder.WithPredicates(StatusChangedPredicate{})).
diff --git a/pkg/controller/integration/kits.go 
b/pkg/controller/integration/kits.go
index 4861d8972..72b504e86 100644
--- a/pkg/controller/integration/kits.go
+++ b/pkg/controller/integration/kits.go
@@ -26,9 +26,9 @@ import (
        "k8s.io/apimachinery/pkg/labels"
        "k8s.io/apimachinery/pkg/selection"
 
+       v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
        ctrl "sigs.k8s.io/controller-runtime/pkg/client"
 
-       v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
        "github.com/apache/camel-k/pkg/platform"
        "github.com/apache/camel-k/pkg/trait"
        "github.com/apache/camel-k/pkg/util"
@@ -82,6 +82,18 @@ func lookupKitsForIntegration(ctx context.Context, c 
ctrl.Reader, integration *v
        return kits, nil
 }
 
+// sameOrMatch returns whether the v1.IntegrationKit is the one used by the 
v1.Integration or if it meets the
+// requirements of the v1.Integration.
+func sameOrMatch(kit *v1.IntegrationKit, integration *v1.Integration) (bool, 
error) {
+       if integration.Status.IntegrationKit != nil {
+               if integration.Status.IntegrationKit.Namespace == kit.Namespace 
&& integration.Status.IntegrationKit.Name == kit.Name {
+                       return true, nil
+               }
+       }
+
+       return integrationMatches(integration, kit)
+}
+
 // integrationMatches returns whether the v1.IntegrationKit meets the 
requirements of the v1.Integration.
 func integrationMatches(integration *v1.Integration, kit *v1.IntegrationKit) 
(bool, error) {
        ilog := log.ForIntegration(integration)
@@ -101,7 +113,17 @@ func integrationMatches(integration *v1.Integration, kit 
*v1.IntegrationKit) (bo
        //
        // A kit can be used only if it contains a subset of the traits and 
related configurations
        // declared on integration.
-       if match, err := hasMatchingTraits(integration.Spec.Traits, 
kit.Spec.Traits); !match || err != nil {
+
+       itc, err := trait.NewUnstructuredTraitsForIntegration(integration)
+       if err != nil {
+               return false, err
+       }
+       ikc, err := trait.NewUnstructuredTraitsForIntegrationKit(kit)
+       if err != nil {
+               return false, err
+       }
+
+       if match, err := hasMatchingTraits(itc, ikc); !match || err != nil {
                ilog.Debug("Integration and integration-kit traits do not 
match", "integration", integration.Name, "integration-kit", kit.Name, 
"namespace", integration.Namespace)
                return false, err
        }
@@ -147,7 +169,17 @@ func kitMatches(kit1 *v1.IntegrationKit, kit2 
*v1.IntegrationKit) (bool, error)
        if len(kit1.Spec.Dependencies) != len(kit2.Spec.Dependencies) {
                return false, nil
        }
-       if match, err := hasMatchingTraits(kit1.Spec.Traits, kit2.Spec.Traits); 
!match || err != nil {
+
+       c1, err := trait.NewUnstructuredTraitsForIntegrationKit(kit1)
+       if err != nil {
+               return false, err
+       }
+       c2, err := trait.NewUnstructuredTraitsForIntegrationKit(kit2)
+       if err != nil {
+               return false, err
+       }
+
+       if match, err := hasMatchingTraits(c1, c2); !match || err != nil {
                return false, err
        }
        if !util.StringSliceContains(kit1.Spec.Dependencies, 
kit2.Spec.Dependencies) {
@@ -157,15 +189,7 @@ func kitMatches(kit1 *v1.IntegrationKit, kit2 
*v1.IntegrationKit) (bool, error)
        return true, nil
 }
 
-func hasMatchingTraits(traits interface{}, kitTraits interface{}) (bool, 
error) {
-       traitMap, err := trait.ToTraitMap(traits)
-       if err != nil {
-               return false, err
-       }
-       kitTraitMap, err := trait.ToTraitMap(kitTraits)
-       if err != nil {
-               return false, err
-       }
+func hasMatchingTraits(traitMap trait.Unstructured, kitTraitMap 
trait.Unstructured) (bool, error) {
        catalog := trait.NewCatalog(nil)
 
        for _, t := range catalog.AllTraits() {
@@ -173,9 +197,10 @@ func hasMatchingTraits(traits interface{}, kitTraits 
interface{}) (bool, error)
                        // We don't store the trait configuration if the trait 
cannot influence the kit behavior
                        continue
                }
+
                id := string(t.ID())
-               it, ok1 := findTrait(traitMap, id)
-               kt, ok2 := findTrait(kitTraitMap, id)
+               it, ok1 := traitMap.Get(id)
+               kt, ok2 := kitTraitMap.Get(id)
 
                if !ok1 && !ok2 {
                        continue
@@ -198,22 +223,6 @@ 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/controller/integration/kits_test.go 
b/pkg/controller/integration/kits_test.go
index 506d26f96..694be1521 100644
--- a/pkg/controller/integration/kits_test.go
+++ b/pkg/controller/integration/kits_test.go
@@ -21,15 +21,17 @@ import (
        "context"
        "testing"
 
-       "github.com/stretchr/testify/assert"
-
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "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/apache/camel-k/pkg/trait"
        "github.com/apache/camel-k/pkg/util/log"
        "github.com/apache/camel-k/pkg/util/test"
+
+       "github.com/stretchr/testify/assert"
 )
 
 func TestLookupKitForIntegration_DiscardKitsInError(t *testing.T) {
@@ -277,7 +279,7 @@ func TestHasMatchingTraits_KitNoTraitShouldNotBePicked(t 
*testing.T) {
        a := buildKitAction{}
        a.InjectLogger(log.Log)
 
-       ok, err := hasMatchingTraits(integration.Spec.Traits, kit.Spec.Traits)
+       ok, err := trait.IntegrationAndKitHaveSameTraits(integration, kit)
        assert.Nil(t, err)
        assert.False(t, ok)
 }
@@ -332,7 +334,7 @@ func TestHasMatchingTraits_KitSameTraitShouldBePicked(t 
*testing.T) {
        a := buildKitAction{}
        a.InjectLogger(log.Log)
 
-       ok, err := hasMatchingTraits(integration.Spec.Traits, kit.Spec.Traits)
+       ok, err := trait.IntegrationAndKitHaveSameTraits(integration, kit)
        assert.Nil(t, err)
        assert.True(t, ok)
 }
diff --git a/pkg/controller/kameletbinding/kamelet_binding_controller.go 
b/pkg/controller/kameletbinding/kamelet_binding_controller.go
index 8cd8bebb1..77278f3c1 100644
--- a/pkg/controller/kameletbinding/kamelet_binding_controller.go
+++ b/pkg/controller/kameletbinding/kamelet_binding_controller.go
@@ -37,6 +37,8 @@ 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/client"
+       "github.com/apache/camel-k/pkg/trait"
+
        camelevent "github.com/apache/camel-k/pkg/event"
        "github.com/apache/camel-k/pkg/platform"
        "github.com/apache/camel-k/pkg/util/monitoring"
@@ -87,6 +89,19 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error {
                                if !ok {
                                        return false
                                }
+
+                               // If traits have changed, the reconciliation 
loop must kick in as
+                               // traits may have impact
+                               sameTraits, err := 
trait.KameletBindingsHaveSameTraits(oldKameletBinding, newKameletBinding)
+                               if err != nil {
+                                       
Log.ForKameletBinding(newKameletBinding).Error(
+                                               err,
+                                               "unable to determine if old and 
new resource have the same traits")
+                               }
+                               if !sameTraits {
+                                       return true
+                               }
+
                                // Ignore updates to the kameletBinding status 
in which case metadata.Generation
                                // does not change, or except when the 
kameletBinding phase changes as it's used
                                // to transition from one phase to another
diff --git a/pkg/controller/kameletbinding/monitor.go 
b/pkg/controller/kameletbinding/monitor.go
index 841a313fc..dc62ea1a9 100644
--- a/pkg/controller/kameletbinding/monitor.go
+++ b/pkg/controller/kameletbinding/monitor.go
@@ -31,6 +31,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"
 )
 
 // NewMonitorAction returns an action that monitors the KameletBinding after 
it's fully initialized.
@@ -76,14 +77,25 @@ func (action *monitorAction) Handle(ctx context.Context, 
kameletbinding *v1alpha
        operatorIDChanged := v1.GetOperatorIDAnnotation(kameletbinding) != "" &&
                (v1.GetOperatorIDAnnotation(kameletbinding) != 
v1.GetOperatorIDAnnotation(&it))
 
+       sameTraits, err := trait.IntegrationAndBindingSameTraits(&it, 
kameletbinding)
+       if err != nil {
+               return nil, err
+       }
+
        // Check if the integration needs to be changed
        expected, err := CreateIntegrationFor(ctx, action.client, 
kameletbinding)
        if err != nil {
                return nil, err
        }
 
-       if !equality.Semantic.DeepDerivative(expected.Spec, it.Spec) || 
operatorIDChanged {
-               action.L.Info("Monitor: KameletBinding needs a rebuild")
+       semanticEquality := equality.Semantic.DeepDerivative(expected.Spec, 
it.Spec)
+
+       if !semanticEquality || operatorIDChanged || !sameTraits {
+               action.L.Info(
+                       "Monitor: KameletBinding needs a rebuild",
+                       "semantic-equality", !semanticEquality,
+                       "operatorid-changed", operatorIDChanged,
+                       "traites-changed", !sameTraits)
 
                // KameletBinding has changed and needs rebuild
                target := kameletbinding.DeepCopy()
diff --git a/pkg/trait/util.go b/pkg/trait/util.go
index c1310ec6e..2b71cd960 100644
--- a/pkg/trait/util.go
+++ b/pkg/trait/util.go
@@ -29,10 +29,11 @@ import (
        user "github.com/mitchellh/go-homedir"
        "github.com/pkg/errors"
        "github.com/scylladb/go-set/strset"
-
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        ctrl "sigs.k8s.io/controller-runtime/pkg/client"
 
        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/client"
        "github.com/apache/camel-k/pkg/metadata"
        "github.com/apache/camel-k/pkg/util"
@@ -40,6 +41,24 @@ import (
        "github.com/apache/camel-k/pkg/util/property"
 )
 
+type Unstructured map[string]map[string]interface{}
+
+func (u Unstructured) Get(id string) (map[string]interface{}, bool) {
+       if t, ok := u[id]; ok {
+               return t, true
+       }
+
+       if addons, ok := u["addons"]; ok {
+               if addon, ok := addons[id]; ok {
+                       if t, ok := addon.(map[string]interface{}); ok {
+                               return t, true
+                       }
+               }
+       }
+
+       return nil, false
+}
+
 var exactVersionRegexp = regexp.MustCompile(`^(\d+)\.(\d+)\.([\w-.]+)$`)
 
 // getIntegrationKit retrieves the kit set on the integration.
@@ -237,7 +256,7 @@ func AssertTraitsType(traits interface{}) error {
 }
 
 // ToTraitMap accepts either v1.Traits or v1.IntegrationKitTraits and converts 
it to a map of traits.
-func ToTraitMap(traits interface{}) (map[string]map[string]interface{}, error) 
{
+func ToTraitMap(traits interface{}) (Unstructured, error) {
        if err := AssertTraitsType(traits); err != nil {
                return nil, err
        }
@@ -246,7 +265,7 @@ func ToTraitMap(traits interface{}) 
(map[string]map[string]interface{}, error) {
        if err != nil {
                return nil, err
        }
-       traitMap := make(map[string]map[string]interface{})
+       traitMap := make(Unstructured)
        if err = json.Unmarshal(data, &traitMap); err != nil {
                return nil, err
        }
@@ -320,3 +339,197 @@ func getBuilderTask(tasks []v1.Task) *v1.BuilderTask {
        }
        return nil
 }
+
+// Equals return if traits are the same.
+func Equals(i1 Unstructured, i2 Unstructured) bool {
+       return reflect.DeepEqual(i1, i2)
+}
+
+// IntegrationsHaveSameTraits return if traits are the same.
+func IntegrationsHaveSameTraits(i1 *v1.Integration, i2 *v1.Integration) (bool, 
error) {
+       c1, err := NewUnstructuredTraitsForIntegration(i1)
+       if err != nil {
+               return false, err
+       }
+       c2, err := NewUnstructuredTraitsForIntegration(i2)
+       if err != nil {
+               return false, err
+       }
+
+       return Equals(c1, c2), nil
+}
+
+// IntegrationKitsHaveSameTraits return if traits are the same.
+func IntegrationKitsHaveSameTraits(i1 *v1.IntegrationKit, i2 
*v1.IntegrationKit) (bool, error) {
+       c1, err := NewUnstructuredTraitsForIntegrationKit(i1)
+       if err != nil {
+               return false, err
+       }
+       c2, err := NewUnstructuredTraitsForIntegrationKit(i2)
+       if err != nil {
+               return false, err
+       }
+
+       return Equals(c1, c2), nil
+}
+
+// KameletBindingsHaveSameTraits return if traits are the same.
+func KameletBindingsHaveSameTraits(i1 *v1alpha1.KameletBinding, i2 
*v1alpha1.KameletBinding) (bool, error) {
+       c1, err := NewUnstructuredTraitsForKameletBinding(i1)
+       if err != nil {
+               return false, err
+       }
+       c2, err := NewUnstructuredTraitsForKameletBinding(i2)
+       if err != nil {
+               return false, err
+       }
+
+       return Equals(c1, c2), nil
+}
+
+// IntegrationAndBindingSameTraits return if traits are the same.
+func IntegrationAndBindingSameTraits(i1 *v1.Integration, i2 
*v1alpha1.KameletBinding) (bool, error) {
+       c1, err := NewUnstructuredTraitsForIntegration(i1)
+       if err != nil {
+               return false, err
+       }
+       c2, err := NewUnstructuredTraitsForKameletBinding(i2)
+       if err != nil {
+               return false, err
+       }
+
+       return Equals(c1, c2), nil
+}
+
+// IntegrationAndKitHaveSameTraits return if traits are the same.
+func IntegrationAndKitHaveSameTraits(i1 *v1.Integration, i2 
*v1.IntegrationKit) (bool, error) {
+       c1, err := NewUnstructuredTraitsForIntegration(i1)
+       if err != nil {
+               return false, err
+       }
+       c2, err := NewUnstructuredTraitsForIntegrationKit(i2)
+       if err != nil {
+               return false, err
+       }
+
+       return Equals(c1, c2), nil
+}
+
+func NewUnstructuredTraitsForIntegration(i *v1.Integration) (Unstructured, 
error) {
+       m1, err := ToTraitMap(i.Spec.Traits)
+       if err != nil {
+               return nil, err
+       }
+
+       m2, err := FromAnnotations(&i.ObjectMeta)
+       if err != nil {
+               return nil, err
+       }
+
+       for k, v := range m2 {
+               m1[k] = v
+       }
+
+       return m1, nil
+}
+
+func NewUnstructuredTraitsForIntegrationKit(i *v1.IntegrationKit) 
(Unstructured, error) {
+       m1, err := ToTraitMap(i.Spec.Traits)
+       if err != nil {
+               return nil, err
+       }
+
+       m2, err := FromAnnotations(&i.ObjectMeta)
+       if err != nil {
+               return nil, err
+       }
+
+       for k, v := range m2 {
+               m1[k] = v
+       }
+
+       return m1, nil
+}
+
+func NewUnstructuredTraitsForIntegrationPlatform(i *v1.IntegrationPlatform) 
(Unstructured, error) {
+       m1, err := ToTraitMap(i.Spec.Traits)
+       if err != nil {
+               return nil, err
+       }
+
+       m2, err := FromAnnotations(&i.ObjectMeta)
+       if err != nil {
+               return nil, err
+       }
+
+       for k, v := range m2 {
+               m1[k] = v
+       }
+
+       return m1, nil
+}
+
+func NewUnstructuredTraitsForKameletBinding(i *v1alpha1.KameletBinding) 
(Unstructured, error) {
+       if i.Spec.Integration != nil {
+               m1, err := ToTraitMap(i.Spec.Integration.Traits)
+               if err != nil {
+                       return nil, err
+               }
+
+               m2, err := FromAnnotations(&i.ObjectMeta)
+               if err != nil {
+                       return nil, err
+               }
+
+               for k, v := range m2 {
+                       m1[k] = v
+               }
+
+               return m1, nil
+       }
+
+       m1, err := FromAnnotations(&i.ObjectMeta)
+       if err != nil {
+               return nil, err
+       }
+
+       return m1, nil
+}
+
+func FromAnnotations(meta *metav1.ObjectMeta) (Unstructured, error) {
+       options := make(Unstructured)
+
+       for k, v := range meta.Annotations {
+               if strings.HasPrefix(k, v1.TraitAnnotationPrefix) {
+                       configKey := strings.TrimPrefix(k, 
v1.TraitAnnotationPrefix)
+                       if strings.Contains(configKey, ".") {
+                               parts := strings.SplitN(configKey, ".", 2)
+                               id := parts[0]
+                               prop := parts[1]
+                               if _, ok := options[id]; !ok {
+                                       options[id] = 
make(map[string]interface{})
+                               }
+
+                               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 options, err
+                                       }
+                                       if cc, ok := 
c.(map[string]interface{}); ok {
+                                               current = cc
+                                       } else {
+                                               return options, 
errors.New(`invalid array specification: to set an array value use the ["v1", 
"v2"] format`)
+                                       }
+                               }
+                               current[prop] = v
+
+                       } else {
+                               return options, fmt.Errorf("wrong format for 
trait annotation %q: missing trait ID", k)
+                       }
+               }
+       }
+
+       return options, nil
+}
diff --git a/pkg/trait/util_test.go b/pkg/trait/util_test.go
index c44a3e26b..192943147 100644
--- a/pkg/trait/util_test.go
+++ b/pkg/trait/util_test.go
@@ -20,10 +20,12 @@ package trait
 import (
        "testing"
 
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "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/apache/camel-k/pkg/apis/camel/v1alpha1"
 
        "github.com/stretchr/testify/assert"
 )
@@ -56,7 +58,7 @@ func TestToTraitMap(t *testing.T) {
                        }),
                },
        }
-       expected := map[string]map[string]interface{}{
+       expected := Unstructured{
                "container": {
                        "enabled":         true,
                        "auto":            false,
@@ -201,3 +203,176 @@ func TestToTrait(t *testing.T) {
        assert.NoError(t, err)
        assert.Equal(t, expected, trait)
 }
+
+func TestSameTraits(t *testing.T) {
+       t.Run("empty traits", func(t *testing.T) {
+               oldKlb := &v1alpha1.KameletBinding{
+                       Spec: v1alpha1.KameletBindingSpec{
+                               Integration: &v1.IntegrationSpec{
+                                       Traits: v1.Traits{},
+                               },
+                       },
+               }
+               newKlb := &v1alpha1.KameletBinding{
+                       Spec: v1alpha1.KameletBindingSpec{
+                               Integration: &v1.IntegrationSpec{
+                                       Traits: v1.Traits{},
+                               },
+                       },
+               }
+
+               ok, err := KameletBindingsHaveSameTraits(oldKlb, newKlb)
+               assert.NoError(t, err)
+               assert.True(t, ok)
+       })
+
+       t.Run("same traits", func(t *testing.T) {
+               oldKlb := &v1alpha1.KameletBinding{
+                       Spec: v1alpha1.KameletBindingSpec{
+                               Integration: &v1.IntegrationSpec{
+                                       Traits: v1.Traits{
+                                               Container: 
&traitv1.ContainerTrait{
+                                                       Image: "foo/bar:1",
+                                               },
+                                       },
+                               },
+                       },
+               }
+               newKlb := &v1alpha1.KameletBinding{
+                       Spec: v1alpha1.KameletBindingSpec{
+                               Integration: &v1.IntegrationSpec{
+                                       Traits: v1.Traits{
+                                               Container: 
&traitv1.ContainerTrait{
+                                                       Image: "foo/bar:1",
+                                               },
+                                       },
+                               },
+                       },
+               }
+
+               ok, err := KameletBindingsHaveSameTraits(oldKlb, newKlb)
+               assert.NoError(t, err)
+               assert.True(t, ok)
+       })
+
+       t.Run("not same traits", func(t *testing.T) {
+               oldKlb := &v1alpha1.KameletBinding{
+                       Spec: v1alpha1.KameletBindingSpec{
+                               Integration: &v1.IntegrationSpec{
+                                       Traits: v1.Traits{
+                                               Container: 
&traitv1.ContainerTrait{
+                                                       Image: "foo/bar:1",
+                                               },
+                                       },
+                               },
+                       },
+               }
+               newKlb := &v1alpha1.KameletBinding{
+                       Spec: v1alpha1.KameletBindingSpec{
+                               Integration: &v1.IntegrationSpec{
+                                       Traits: v1.Traits{
+                                               Owner: &traitv1.OwnerTrait{
+                                                       TargetAnnotations: 
[]string{"foo"},
+                                               },
+                                       },
+                               },
+                       },
+               }
+
+               ok, err := KameletBindingsHaveSameTraits(oldKlb, newKlb)
+               assert.NoError(t, err)
+               assert.False(t, ok)
+       })
+
+       t.Run("same traits with annotations", func(t *testing.T) {
+               oldKlb := &v1alpha1.KameletBinding{
+                       Spec: v1alpha1.KameletBindingSpec{
+                               Integration: &v1.IntegrationSpec{
+                                       Traits: v1.Traits{
+                                               Container: 
&traitv1.ContainerTrait{
+                                                       Image: "foo/bar:1",
+                                               },
+                                       },
+                               },
+                       },
+               }
+               newKlb := &v1alpha1.KameletBinding{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Annotations: map[string]string{
+                                       v1.TraitAnnotationPrefix + 
"container.image": "foo/bar:1",
+                               },
+                       },
+               }
+
+               ok, err := KameletBindingsHaveSameTraits(oldKlb, newKlb)
+               assert.NoError(t, err)
+               assert.True(t, ok)
+       })
+
+       t.Run("same traits with annotations only", func(t *testing.T) {
+               oldKlb := &v1alpha1.KameletBinding{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Annotations: map[string]string{
+                                       v1.TraitAnnotationPrefix + 
"container.image": "foo/bar:1",
+                               },
+                       },
+               }
+               newKlb := &v1alpha1.KameletBinding{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Annotations: map[string]string{
+                                       v1.TraitAnnotationPrefix + 
"container.image": "foo/bar:1",
+                               },
+                       },
+               }
+
+               ok, err := KameletBindingsHaveSameTraits(oldKlb, newKlb)
+               assert.NoError(t, err)
+               assert.True(t, ok)
+       })
+
+       t.Run("not same traits with annotations", func(t *testing.T) {
+               oldKlb := &v1alpha1.KameletBinding{
+                       Spec: v1alpha1.KameletBindingSpec{
+                               Integration: &v1.IntegrationSpec{
+                                       Traits: v1.Traits{
+                                               Container: 
&traitv1.ContainerTrait{
+                                                       Image: "foo/bar:1",
+                                               },
+                                       },
+                               },
+                       },
+               }
+               newKlb := &v1alpha1.KameletBinding{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Annotations: map[string]string{
+                                       v1.TraitAnnotationPrefix + 
"container.image": "foo/bar:2",
+                               },
+                       },
+               }
+
+               ok, err := KameletBindingsHaveSameTraits(oldKlb, newKlb)
+               assert.NoError(t, err)
+               assert.False(t, ok)
+       })
+
+       t.Run("not same traits with annotations only", func(t *testing.T) {
+               oldKlb := &v1alpha1.KameletBinding{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Annotations: map[string]string{
+                                       v1.TraitAnnotationPrefix + 
"container.image": "foo/bar:1",
+                               },
+                       },
+               }
+               newKlb := &v1alpha1.KameletBinding{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Annotations: map[string]string{
+                                       v1.TraitAnnotationPrefix + 
"container.image": "foo/bar:2",
+                               },
+                       },
+               }
+
+               ok, err := KameletBindingsHaveSameTraits(oldKlb, newKlb)
+               assert.NoError(t, err)
+               assert.False(t, ok)
+       })
+}
diff --git a/pkg/util/digest/digest.go b/pkg/util/digest/digest.go
index 02a060411..2c1ea63b0 100644
--- a/pkg/util/digest/digest.go
+++ b/pkg/util/digest/digest.go
@@ -202,6 +202,10 @@ func ComputeForIntegrationKit(kit *v1.IntegrationKit) 
(string, error) {
                return "", err
        }
 
+       if _, err := hash.Write([]byte(kit.Spec.Image)); err != nil {
+               return "", err
+       }
+
        for _, item := range kit.Spec.Dependencies {
                if _, err := hash.Write([]byte(item)); err != nil {
                        return "", err
diff --git a/pkg/util/kubernetes/util.go b/pkg/util/kubernetes/util.go
index 3d029c202..2816ea15d 100644
--- a/pkg/util/kubernetes/util.go
+++ b/pkg/util/kubernetes/util.go
@@ -18,10 +18,9 @@ limitations under the License.
 package kubernetes
 
 import (
+       "github.com/apache/camel-k/pkg/util"
        "k8s.io/apimachinery/pkg/runtime"
        "k8s.io/apimachinery/pkg/util/json"
-
-       "github.com/apache/camel-k/pkg/util"
 )
 
 // ToJSON marshal to json format.

Reply via email to