You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by lb...@apache.org on 2022/08/05 13:21:13 UTC

[camel-k] 01/03: fix: camel-k ignores changes to traits configured using annotations #3479

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 <lb...@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.