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.