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.