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

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


The following commit(s) were added to refs/heads/main by this push:
     new 9c14037ae fix: Fix garbage collection trait
9c14037ae is described below

commit 9c14037ae4e30465982f1d844c2a3e2eb1ddbe82
Author: Christoph Deppisch <cdeppi...@redhat.com>
AuthorDate: Fri Apr 12 17:03:46 2024 +0200

    fix: Fix garbage collection trait
    
    - Run garbage collection trait also in Integration initialization phase
    - Fallback to minimal set of deletable types when auto discovery fails 
(e.g. in OpenShift)
    - Add some unit and E2E test
---
 e2e/common/config/config_test.go               |  42 +++--
 e2e/common/traits/gc_test.go                   |  61 +++++++
 e2e/knative/files/PlatformHttpServer.java      |  26 +++
 e2e/knative/gc_test.go                         |  65 ++++++++
 pkg/apis/camel/v1/integration_types_support.go |   3 +
 pkg/trait/gc.go                                | 105 ++++++++++--
 pkg/trait/gc_test.go                           | 218 +++++++++++++++++++++++++
 pkg/trait/trait.go                             |   7 +-
 pkg/util/test/client.go                        |  25 +++
 9 files changed, 524 insertions(+), 28 deletions(-)

diff --git a/e2e/common/config/config_test.go b/e2e/common/config/config_test.go
index c60267d75..58a3578a9 100644
--- a/e2e/common/config/config_test.go
+++ b/e2e/common/config/config_test.go
@@ -73,7 +73,8 @@ func TestRunConfigExamples(t *testing.T) {
                t.Run("Property from ConfigMap", func(t *testing.T) {
                        var cmData = make(map[string]string)
                        cmData["my.message"] = "my-configmap-property-value"
-                       CreatePlainTextConfigmap(t, ctx, ns, 
"my-cm-test-property", cmData)
+                       err := CreatePlainTextConfigmap(t, ctx, ns, 
"my-cm-test-property", cmData)
+                       g.Expect(err).To(BeNil())
 
                        g.Expect(KamelRunWithID(t, ctx, operatorID, ns, 
"./files/property-route.groovy", "-p", 
"configmap:my-cm-test-property").Execute()).To(Succeed())
                        g.Eventually(IntegrationPodPhase(t, ctx, ns, 
"property-route"), TestTimeoutLong).Should(Equal(corev1.PodRunning))
@@ -85,7 +86,8 @@ func TestRunConfigExamples(t *testing.T) {
                t.Run("Property from ConfigMap as property file", func(t 
*testing.T) {
                        var cmData = make(map[string]string)
                        cmData["my.properties"] = 
"my.message=my-configmap-property-entry"
-                       CreatePlainTextConfigmap(t, ctx, ns, 
"my-cm-test-properties", cmData)
+                       err := CreatePlainTextConfigmap(t, ctx, ns, 
"my-cm-test-properties", cmData)
+                       g.Expect(err).To(BeNil())
 
                        g.Expect(KamelRunWithID(t, ctx, operatorID, ns, 
"./files/property-route.groovy", "-p", 
"configmap:my-cm-test-properties").Execute()).To(Succeed())
                        g.Eventually(IntegrationPodPhase(t, ctx, ns, 
"property-route"), TestTimeoutLong).Should(Equal(corev1.PodRunning))
@@ -97,7 +99,8 @@ func TestRunConfigExamples(t *testing.T) {
                t.Run("Property from Secret", func(t *testing.T) {
                        var secData = make(map[string]string)
                        secData["my.message"] = "my-secret-property-value"
-                       CreatePlainTextSecret(t, ctx, ns, 
"my-sec-test-property", secData)
+                       err := CreatePlainTextSecret(t, ctx, ns, 
"my-sec-test-property", secData)
+                       g.Expect(err).To(BeNil())
 
                        g.Expect(KamelRunWithID(t, ctx, operatorID, ns, 
"./files/property-route.groovy", "-p", 
"secret:my-sec-test-property").Execute()).To(Succeed())
                        g.Eventually(IntegrationPodPhase(t, ctx, ns, 
"property-route"), TestTimeoutLong).Should(Equal(corev1.PodRunning))
@@ -109,7 +112,8 @@ func TestRunConfigExamples(t *testing.T) {
                t.Run("Property from Secret as property file", func(t 
*testing.T) {
                        var secData = make(map[string]string)
                        secData["my.properties"] = 
"my.message=my-secret-property-entry"
-                       CreatePlainTextSecret(t, ctx, ns, 
"my-sec-test-properties", secData)
+                       err := CreatePlainTextSecret(t, ctx, ns, 
"my-sec-test-properties", secData)
+                       g.Expect(err).To(BeNil())
 
                        g.Expect(KamelRunWithID(t, ctx, operatorID, ns, 
"./files/property-route.groovy", "--name", "property-route-secret", "-p", 
"secret:my-sec-test-properties").Execute()).To(Succeed())
                        g.Eventually(IntegrationPodPhase(t, ctx, ns, 
"property-route-secret"), TestTimeoutLong).Should(Equal(corev1.PodRunning))
@@ -120,7 +124,8 @@ func TestRunConfigExamples(t *testing.T) {
                t.Run("Property from Secret inlined", func(t *testing.T) {
                        var secData = make(map[string]string)
                        secData["my-message"] = "my-secret-external-value"
-                       CreatePlainTextSecret(t, ctx, ns, "my-sec-inlined", 
secData)
+                       err := CreatePlainTextSecret(t, ctx, ns, 
"my-sec-inlined", secData)
+                       g.Expect(err).To(BeNil())
 
                        // TODO: remove jvm.options trait as soon as 
CAMEL-20054 gets fixed
                        g.Expect(KamelRunWithID(t, ctx, operatorID, ns, 
"./files/property-secret-route.groovy", "-t", 
"mount.configs=secret:my-sec-inlined", "-t", 
"jvm.options=-Dcamel.k.mount-path.secrets=/etc/camel/conf.d/_secrets").Execute()).To(Succeed())
@@ -145,13 +150,15 @@ func TestRunConfigExamples(t *testing.T) {
                // Store a configmap on the cluster
                var cmData = make(map[string]string)
                cmData["my-configmap-key"] = "my-configmap-content"
-               CreatePlainTextConfigmap(t, ctx, ns, "my-cm", cmData)
+               err := CreatePlainTextConfigmap(t, ctx, ns, "my-cm", cmData)
+               g.Expect(err).To(BeNil())
 
                // Store a configmap with multiple values
                var cmDataMulti = make(map[string]string)
                cmDataMulti["my-configmap-key"] = "should-not-see-it"
                cmDataMulti["my-configmap-key-2"] = "my-configmap-content-2"
-               CreatePlainTextConfigmap(t, ctx, ns, "my-cm-multi", cmDataMulti)
+               err = CreatePlainTextConfigmap(t, ctx, ns, "my-cm-multi", 
cmDataMulti)
+               g.Expect(err).To(BeNil())
 
                t.Run("Config configmap", func(t *testing.T) {
                        g.Expect(KamelRunWithID(t, ctx, operatorID, ns, 
"./files/config-configmap-route.groovy", "--config", 
"configmap:my-cm").Execute()).To(Succeed())
@@ -192,7 +199,8 @@ func TestRunConfigExamples(t *testing.T) {
                        // Store a configmap as property file
                        var cmDataProps = make(map[string]string)
                        cmDataProps["my.properties"] = 
"my.key.1=hello\nmy.key.2=world"
-                       CreatePlainTextConfigmap(t, ctx, ns, 
"my-cm-properties", cmDataProps)
+                       err = CreatePlainTextConfigmap(t, ctx, ns, 
"my-cm-properties", cmDataProps)
+                       g.Expect(err).To(BeNil())
 
                        g.Expect(KamelRunWithID(t, ctx, operatorID, ns, 
"./files/config-configmap-properties-route.groovy", "--config", 
"configmap:my-cm-properties").Execute()).To(Succeed())
                        g.Eventually(IntegrationPodPhase(t, ctx, ns, 
"config-configmap-properties-route"), 
TestTimeoutLong).Should(Equal(corev1.PodRunning))
@@ -205,13 +213,15 @@ func TestRunConfigExamples(t *testing.T) {
                // Store a secret on the cluster
                var secData = make(map[string]string)
                secData["my-secret-key"] = "very top secret"
-               CreatePlainTextSecret(t, ctx, ns, "my-sec", secData)
+               err = CreatePlainTextSecret(t, ctx, ns, "my-sec", secData)
+               g.Expect(err).To(BeNil())
 
                // Store a secret with multi values
                var secDataMulti = make(map[string]string)
                secDataMulti["my-secret-key"] = "very top secret"
                secDataMulti["my-secret-key-2"] = "even more secret"
-               CreatePlainTextSecret(t, ctx, ns, "my-sec-multi", secDataMulti)
+               err = CreatePlainTextSecret(t, ctx, ns, "my-sec-multi", 
secDataMulti)
+               g.Expect(err).To(BeNil())
 
                t.Run("Config secret", func(t *testing.T) {
                        g.Expect(KamelRunWithID(t, ctx, operatorID, ns, 
"./files/config-secret-route.groovy", "--config", 
"secret:my-sec").Execute()).To(Succeed())
@@ -277,7 +287,8 @@ func TestRunConfigExamples(t *testing.T) {
                t.Run("Build time property from ConfigMap", func(t *testing.T) {
                        var cmData = make(map[string]string)
                        cmData["quarkus.application.name"] = 
"my-cool-application"
-                       CreatePlainTextConfigmap(t, ctx, ns, 
"my-cm-test-build-property", cmData)
+                       err = CreatePlainTextConfigmap(t, ctx, ns, 
"my-cm-test-build-property", cmData)
+                       g.Expect(err).To(BeNil())
 
                        g.Expect(KamelRunWithID(t, ctx, operatorID, ns, 
"./files/build-property-file-route.groovy", "--build-property", 
"configmap:my-cm-test-build-property").Execute()).To(Succeed())
                        g.Eventually(IntegrationPodPhase(t, ctx, ns, 
"build-property-file-route"), TestTimeoutLong).Should(Equal(corev1.PodRunning))
@@ -289,7 +300,8 @@ func TestRunConfigExamples(t *testing.T) {
                t.Run("Build time property from ConfigMap as property file", 
func(t *testing.T) {
                        var cmData = make(map[string]string)
                        cmData["my.properties"] = 
"quarkus.application.name=my-super-cool-application"
-                       CreatePlainTextConfigmap(t, ctx, ns, 
"my-cm-test-build-properties", cmData)
+                       err = CreatePlainTextConfigmap(t, ctx, ns, 
"my-cm-test-build-properties", cmData)
+                       g.Expect(err).To(BeNil())
 
                        g.Expect(KamelRunWithID(t, ctx, operatorID, ns, 
"./files/build-property-file-route.groovy", "--name", 
"build-property-file-route-cm", "--build-property", 
"configmap:my-cm-test-build-properties").Execute()).To(Succeed())
                        g.Eventually(IntegrationPodPhase(t, ctx, ns, 
"build-property-file-route-cm"), 
TestTimeoutLong).Should(Equal(corev1.PodRunning))
@@ -301,7 +313,8 @@ func TestRunConfigExamples(t *testing.T) {
                t.Run("Build time property from Secret", func(t *testing.T) {
                        var secData = make(map[string]string)
                        secData["quarkus.application.name"] = 
"my-great-application"
-                       CreatePlainTextSecret(t, ctx, ns, 
"my-sec-test-build-property", secData)
+                       err = CreatePlainTextSecret(t, ctx, ns, 
"my-sec-test-build-property", secData)
+                       g.Expect(err).To(BeNil())
 
                        g.Expect(KamelRunWithID(t, ctx, operatorID, ns, 
"./files/build-property-file-route.groovy", "--build-property", 
"secret:my-sec-test-build-property").Execute()).To(Succeed())
                        g.Eventually(IntegrationPodPhase(t, ctx, ns, 
"build-property-file-route"), TestTimeoutLong).Should(Equal(corev1.PodRunning))
@@ -313,7 +326,8 @@ func TestRunConfigExamples(t *testing.T) {
                t.Run("Build time property from Secret as property file", 
func(t *testing.T) {
                        var secData = make(map[string]string)
                        secData["my.properties"] = 
"quarkus.application.name=my-awsome-application"
-                       CreatePlainTextSecret(t, ctx, ns, 
"my-sec-test-build-properties", secData)
+                       err = CreatePlainTextSecret(t, ctx, ns, 
"my-sec-test-build-properties", secData)
+                       g.Expect(err).To(BeNil())
 
                        g.Expect(KamelRunWithID(t, ctx, operatorID, ns, 
"./files/build-property-file-route.groovy", "--name", 
"build-property-file-route-secret", "--build-property", 
"secret:my-sec-test-build-properties").Execute()).To(Succeed())
                        g.Eventually(IntegrationPodPhase(t, ctx, ns, 
"build-property-file-route-secret"), 
TestTimeoutLong).Should(Equal(corev1.PodRunning))
diff --git a/e2e/common/traits/gc_test.go b/e2e/common/traits/gc_test.go
new file mode 100644
index 000000000..6709181f1
--- /dev/null
+++ b/e2e/common/traits/gc_test.go
@@ -0,0 +1,61 @@
+//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 traits
+
+import (
+       "context"
+       "testing"
+
+       . "github.com/onsi/gomega"
+
+       . "github.com/apache/camel-k/v2/e2e/support"
+       v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
+       corev1 "k8s.io/api/core/v1"
+)
+
+func TestGarbageCollectorTrait(t *testing.T) {
+       t.Parallel()
+
+       WithNewTestNamespace(t, func(ctx context.Context, g *WithT, ns string) {
+               operatorID := "camel-k-traits-gc"
+               g.Expect(CopyCamelCatalog(t, ctx, ns, operatorID)).To(Succeed())
+               g.Expect(CopyIntegrationKits(t, ctx, ns, 
operatorID)).To(Succeed())
+               g.Expect(KamelInstallWithID(t, ctx, operatorID, 
ns)).To(Succeed())
+
+               g.Eventually(SelectedPlatformPhase(t, ctx, ns, operatorID), 
TestTimeoutMedium).Should(Equal(v1.IntegrationPlatformPhaseReady))
+
+               t.Run("Delete outdated resources", func(t *testing.T) {
+                       g.Expect(KamelRunWithID(t, ctx, operatorID, ns, 
"files/PlatformHttpServer.java").Execute()).To(Succeed())
+                       g.Eventually(IntegrationPodPhase(t, ctx, ns, 
"platform-http-server"), TestTimeoutLong).Should(Equal(corev1.PodRunning))
+
+                       g.Eventually(ServicesByType(t, ctx, ns, 
corev1.ServiceTypeClusterIP), TestTimeoutLong).ShouldNot(BeEmpty())
+
+                       // Update integration and disable service trait - 
existing service should be garbage collected
+                       g.Expect(KamelRunWithID(t, ctx, operatorID, ns, 
"files/PlatformHttpServer.java", "-t", 
"service.enabled=false").Execute()).To(Succeed())
+
+                       g.Eventually(ServicesByType(t, ctx, ns, 
corev1.ServiceTypeClusterIP), TestTimeoutLong).Should(BeEmpty())
+
+                       g.Expect(Kamel(t, ctx, "delete", "--all", "-n", 
ns).Execute()).To(Succeed())
+               })
+       })
+}
diff --git a/e2e/knative/files/PlatformHttpServer.java 
b/e2e/knative/files/PlatformHttpServer.java
new file mode 100644
index 000000000..31d8d19f3
--- /dev/null
+++ b/e2e/knative/files/PlatformHttpServer.java
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+import org.apache.camel.builder.RouteBuilder;
+
+public class PlatformHttpServer extends RouteBuilder {
+    @Override
+    public void configure() throws Exception {
+        from("platform-http:/hello?httpMethodRestrict=GET")
+            .setBody(simple("Hello ${header.name}"));
+    }
+}
diff --git a/e2e/knative/gc_test.go b/e2e/knative/gc_test.go
new file mode 100644
index 000000000..b5df6dc3c
--- /dev/null
+++ b/e2e/knative/gc_test.go
@@ -0,0 +1,65 @@
+//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 knative
+
+import (
+       v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
+       corev1 "k8s.io/api/core/v1"
+       "testing"
+
+       . "github.com/apache/camel-k/v2/e2e/support"
+       . "github.com/onsi/gomega"
+)
+
+func TestGarbageCollectResources(t *testing.T) {
+       ctx := TestContext()
+       g := NewWithT(t)
+
+       integration := "platform-http-server"
+       g.Expect(KamelRunWithID(t, ctx, operatorID, ns, 
"files/PlatformHttpServer.java", "-t", 
"knative-service.enabled=false").Execute()).To(Succeed())
+       g.Eventually(IntegrationPodPhase(t, ctx, ns, integration), 
TestTimeoutLong).Should(Equal(corev1.PodRunning))
+       g.Eventually(IntegrationConditionStatus(t, ctx, ns, integration, 
v1.IntegrationConditionReady), 
TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
+
+       g.Eventually(KnativeService(t, ctx, ns, integration), 
TestTimeoutMedium).Should(BeNil())
+       g.Eventually(ServiceType(t, ctx, ns, integration), 
TestTimeoutMedium).Should(Equal(corev1.ServiceTypeClusterIP))
+
+       // Update integration and enable knative service trait - existing 
arbitrary service should be garbage collected
+       g.Expect(KamelRunWithID(t, ctx, operatorID, ns, 
"files/PlatformHttpServer.java").Execute()).To(Succeed())
+
+       g.Eventually(KnativeService(t, ctx, ns, integration), 
TestTimeoutShort).ShouldNot(BeNil())
+       g.Eventually(ServiceType(t, ctx, ns, integration), 
TestTimeoutShort).Should(Equal(corev1.ServiceTypeExternalName))
+
+       g.Eventually(IntegrationPodPhase(t, ctx, ns, integration), 
TestTimeoutMedium).Should(Equal(corev1.PodRunning))
+       g.Eventually(IntegrationConditionStatus(t, ctx, ns, integration, 
v1.IntegrationConditionReady), 
TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
+
+       // Disable knative service trait again - this time knative service 
should be garbage collected
+       g.Expect(KamelRunWithID(t, ctx, operatorID, ns, 
"files/PlatformHttpServer.java", "-t", 
"knative-service.enabled=false").Execute()).To(Succeed())
+
+       g.Eventually(KnativeService(t, ctx, ns, integration), 
TestTimeoutMedium).Should(BeNil())
+       g.Eventually(ServiceType(t, ctx, ns, integration), 
TestTimeoutMedium).Should(Equal(corev1.ServiceTypeClusterIP))
+
+       g.Eventually(IntegrationPodPhase(t, ctx, ns, integration), 
TestTimeoutMedium).Should(Equal(corev1.PodRunning))
+       g.Eventually(IntegrationConditionStatus(t, ctx, ns, integration, 
v1.IntegrationConditionReady), 
TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
+
+       g.Expect(Kamel(t, ctx, "delete", "--all", "-n", 
ns).Execute()).To(Succeed())
+}
diff --git a/pkg/apis/camel/v1/integration_types_support.go 
b/pkg/apis/camel/v1/integration_types_support.go
index 3705c5a05..ca0de0626 100644
--- a/pkg/apis/camel/v1/integration_types_support.go
+++ b/pkg/apis/camel/v1/integration_types_support.go
@@ -28,6 +28,9 @@ import (
 // IntegrationLabel is used to tag k8s object created by a given Integration.
 const IntegrationLabel = "camel.apache.org/integration"
 
+// IntegrationGenerationLabel is used to check on outdated integration 
resources that can be removed by garbage collection.
+const IntegrationGenerationLabel = "camel.apache.org/generation"
+
 // IntegrationSyntheticLabel is used to tag k8s synthetic Integrations.
 const IntegrationSyntheticLabel = "camel.apache.org/is-synthetic"
 
diff --git a/pkg/trait/gc.go b/pkg/trait/gc.go
index 5598c1c70..e3e5e79e8 100644
--- a/pkg/trait/gc.go
+++ b/pkg/trait/gc.go
@@ -20,6 +20,7 @@ package trait
 import (
        "context"
        "fmt"
+       "maps"
        "strconv"
        "strings"
        "sync"
@@ -27,7 +28,11 @@ import (
 
        "golang.org/x/time/rate"
 
+       appsv1 "k8s.io/api/apps/v1"
        authorization "k8s.io/api/authorization/v1"
+       batchv1 "k8s.io/api/batch/v1"
+       corev1 "k8s.io/api/core/v1"
+
        k8serrors "k8s.io/apimachinery/pkg/api/errors"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -42,12 +47,59 @@ import (
        v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
        traitv1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1/trait"
        "github.com/apache/camel-k/v2/pkg/util"
+       "github.com/apache/camel-k/v2/pkg/util/log"
 )
 
 var (
-       lock            sync.Mutex
-       rateLimiter     = rate.NewLimiter(rate.Every(time.Minute), 1)
-       collectableGVKs = make(map[schema.GroupVersionKind]struct{})
+       lock                  sync.Mutex
+       rateLimiter           = rate.NewLimiter(rate.Every(time.Minute), 1)
+       collectableGVKs       = make(map[schema.GroupVersionKind]struct{})
+       defaultDeletableTypes = map[schema.GroupVersionKind]struct{}{
+               {
+                       Kind:    "ConfigMap",
+                       Group:   corev1.SchemeGroupVersion.Group,
+                       Version: corev1.SchemeGroupVersion.Version,
+               }: {},
+               {
+                       Kind:    "Deployment",
+                       Group:   appsv1.SchemeGroupVersion.Group,
+                       Version: appsv1.SchemeGroupVersion.Version,
+               }: {},
+               {
+                       Kind:    "Secret",
+                       Group:   corev1.SchemeGroupVersion.Group,
+                       Version: corev1.SchemeGroupVersion.Version,
+               }: {},
+               {
+                       Kind:    "Service",
+                       Group:   corev1.SchemeGroupVersion.Group,
+                       Version: corev1.SchemeGroupVersion.Version,
+               }: {},
+               {
+                       Kind:    "CronJob",
+                       Group:   batchv1.SchemeGroupVersion.Group,
+                       Version: batchv1.SchemeGroupVersion.Version,
+               }: {},
+               {
+                       Kind:    "Job",
+                       Group:   batchv1.SchemeGroupVersion.Group,
+                       Version: batchv1.SchemeGroupVersion.Version,
+               }: {},
+       }
+       deletableTypesByProfile = 
map[v1.TraitProfile]map[schema.GroupVersionKind]struct{}{
+               v1.TraitProfileKnative: {
+                       schema.GroupVersionKind{
+                               Kind:    "Service",
+                               Group:   "serving.knative.dev",
+                               Version: "v1",
+                       }: {},
+                       schema.GroupVersionKind{
+                               Kind:    "Trigger",
+                               Group:   "eventing.knative.dev",
+                               Version: "v1",
+                       }: {},
+               },
+       }
 )
 
 type gcTrait struct {
@@ -73,7 +125,7 @@ func (t *gcTrait) Configure(e *Environment) (bool, 
*TraitCondition, error) {
 }
 
 func (t *gcTrait) Apply(e *Environment) error {
-       if e.IntegrationInRunningPhases() && e.Integration.GetGeneration() > 1 {
+       if e.Integration.GetGeneration() > 1 {
                // Register a post action that deletes the existing resources 
that are labelled
                // with the previous integration generation(s).
                // We make the assumption generation is a monotonically 
increasing strictly positive integer,
@@ -88,12 +140,12 @@ func (t *gcTrait) Apply(e *Environment) error {
        e.PostProcessors = append(e.PostProcessors, func(env *Environment) 
error {
                generation := 
strconv.FormatInt(env.Integration.GetGeneration(), 10)
                env.Resources.VisitMetaObject(func(resource metav1.Object) {
-                       labels := resource.GetLabels()
+                       resourceLabels := resource.GetLabels()
                        // Label the resource with the current integration 
generation
-                       labels["camel.apache.org/generation"] = generation
+                       resourceLabels[v1.IntegrationGenerationLabel] = 
generation
                        // Make sure the integration label is set
-                       labels[v1.IntegrationLabel] = env.Integration.Name
-                       resource.SetLabels(labels)
+                       resourceLabels[v1.IntegrationLabel] = 
env.Integration.Name
+                       resource.SetLabels(resourceLabels)
                })
                return nil
        })
@@ -107,8 +159,18 @@ func (t *gcTrait) garbageCollectResources(e *Environment) 
error {
                return fmt.Errorf("cannot discover GVK types: %w", err)
        }
 
+       profile := e.DetermineProfile()
+       if profileDeletableTypes, ok := deletableTypesByProfile[profile]; ok {
+               // copy profile related deletable types if not already present
+               for key, value := range profileDeletableTypes {
+                       if _, found := deletableGVKs[key]; !found {
+                               deletableGVKs[key] = value
+                       }
+               }
+       }
+
        integration, _ := labels.NewRequirement(v1.IntegrationLabel, 
selection.Equals, []string{e.Integration.Name})
-       generation, err := labels.NewRequirement("camel.apache.org/generation", 
selection.LessThan, []string{strconv.FormatInt(e.Integration.GetGeneration(), 
10)})
+       generation, err := labels.NewRequirement(v1.IntegrationGenerationLabel, 
selection.LessThan, []string{strconv.FormatInt(e.Integration.GetGeneration(), 
10)})
        if err != nil {
                return fmt.Errorf("cannot determine generation requirement: 
%w", err)
        }
@@ -172,10 +234,14 @@ func (t *gcTrait) getDeletableTypes(e *Environment) 
(map[schema.GroupVersionKind
        lock.Lock()
        defer lock.Unlock()
 
+       // Return a fresh map even when returning cached collectables
+       GVKs := make(map[schema.GroupVersionKind]struct{})
+
        // Rate limit to avoid Discovery and SelfSubjectRulesReview requests at 
every reconciliation.
        if !rateLimiter.Allow() {
                // Return the cached set of garbage collectable GVKs.
-               return collectableGVKs, nil
+               maps.Copy(GVKs, collectableGVKs)
+               return GVKs, nil
        }
 
        // We rely on the discovery API to retrieve all the resources GVK,
@@ -204,7 +270,6 @@ func (t *gcTrait) getDeletableTypes(e *Environment) 
(map[schema.GroupVersionKind
                return nil, err
        }
 
-       GVKs := make(map[schema.GroupVersionKind]struct{})
        for _, APIResourceList := range APIResourceLists {
                for _, resource := range APIResourceList.APIResources {
                        resourceGroup := resource.Group
@@ -233,7 +298,21 @@ func (t *gcTrait) getDeletableTypes(e *Environment) 
(map[schema.GroupVersionKind
                        }
                }
        }
-       collectableGVKs = GVKs
 
-       return collectableGVKs, nil
+       if len(GVKs) == 0 {
+               // Auto discovery of deletable types has no results (probably 
an error)
+               // Make sure to at least use a minimal set of deletable types 
for garbage collection
+               t.L.ForIntegration(e.Integration).Debugf("Auto discovery of 
deletable types returned no results. " +
+                       "Using default minimal set of deletable types for 
garbage collection")
+               maps.Copy(GVKs, defaultDeletableTypes)
+       }
+
+       collectableGVKs = make(map[schema.GroupVersionKind]struct{})
+       maps.Copy(collectableGVKs, GVKs)
+
+       for gvk := range GVKs {
+               log.Debugf("Found deletable type: %s", gvk.String())
+       }
+
+       return GVKs, nil
 }
diff --git a/pkg/trait/gc_test.go b/pkg/trait/gc_test.go
index 406b00e45..fa21a8531 100644
--- a/pkg/trait/gc_test.go
+++ b/pkg/trait/gc_test.go
@@ -18,15 +18,23 @@ limitations under the License.
 package trait
 
 import (
+       "context"
+       "strconv"
        "testing"
 
        v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
+       "github.com/apache/camel-k/v2/pkg/util/test"
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
 
+       appsv1 "k8s.io/api/apps/v1"
        corev1 "k8s.io/api/core/v1"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/utils/pointer"
+       eventingv1 "knative.dev/eventing/pkg/apis/eventing/v1"
+       servingv1 "knative.dev/serving/pkg/apis/serving/v1"
+       ctrl "sigs.k8s.io/controller-runtime/pkg/client"
+       "sigs.k8s.io/controller-runtime/pkg/client/interceptor"
 )
 
 func TestConfigureGCTraitDoesSucceed(t *testing.T) {
@@ -89,6 +97,215 @@ func 
TestApplyGCTraitDuringInitializationPhaseSkipPostActions(t *testing.T) {
        assert.Len(t, environment.PostActions, 0)
 }
 
+func TestGetDefaultMinimalGarbageCollectableTypes(t *testing.T) {
+       gcTrait, environment := createNominalGCTest()
+       environment.Integration.Generation = 2
+
+       gcTrait.Client, _ = test.NewFakeClient()
+       environment.Client = gcTrait.Client
+
+       deletableTypes, err := gcTrait.getDeletableTypes(environment)
+
+       require.NoError(t, err)
+       assert.Len(t, deletableTypes, 6)
+}
+
+func TestGarbageCollectResources(t *testing.T) {
+       gcTrait, environment := createNominalGCTest()
+       environment.Integration.Generation = 2
+
+       deployment := getIntegrationDeployment(environment.Integration)
+       deployment.Labels[v1.IntegrationGenerationLabel] = "1"
+       gcTrait.Client, _ = test.NewFakeClient(deployment)
+
+       environment.Client = gcTrait.Client
+
+       resourceDeleted := false
+       fakeClient := gcTrait.Client.(*test.FakeClient) //nolint
+       fakeClient.Intercept(&interceptor.Funcs{
+               Delete: func(ctx context.Context, client ctrl.WithWatch, obj 
ctrl.Object, opts ...ctrl.DeleteOption) error {
+                       assert.Equal(t, environment.Integration.Name, 
obj.GetName())
+                       assert.Equal(t, "Deployment", 
obj.GetObjectKind().GroupVersionKind().Kind)
+                       resourceDeleted = true
+                       return nil
+               },
+       })
+       err := gcTrait.garbageCollectResources(environment)
+
+       require.NoError(t, err)
+       assert.True(t, resourceDeleted)
+}
+
+func TestGarbageCollectPreserveResourcesWithSameGeneration(t *testing.T) {
+       gcTrait, environment := createNominalGCTest()
+       environment.Integration.Generation = 2
+
+       deployment := getIntegrationDeployment(environment.Integration)
+       gcTrait.Client, _ = test.NewFakeClient(deployment)
+
+       environment.Client = gcTrait.Client
+
+       resourceDeleted := false
+       fakeClient := gcTrait.Client.(*test.FakeClient) //nolint
+       fakeClient.Intercept(&interceptor.Funcs{
+               Delete: func(ctx context.Context, client ctrl.WithWatch, obj 
ctrl.Object, opts ...ctrl.DeleteOption) error {
+                       resourceDeleted = true
+                       return nil
+               },
+       })
+       err := gcTrait.garbageCollectResources(environment)
+
+       require.NoError(t, err)
+       assert.False(t, resourceDeleted)
+}
+
+func TestGarbageCollectPreserveResourcesOwnerReferenceMismatch(t *testing.T) {
+       gcTrait, environment := createNominalGCTest()
+       environment.Integration.Generation = 2
+
+       deployment := getIntegrationDeployment(environment.Integration)
+       deployment.Labels[v1.IntegrationGenerationLabel] = "1"
+       deployment.OwnerReferences = []metav1.OwnerReference{
+               {
+                       APIVersion: v1.SchemeGroupVersion.String(),
+                       Kind:       "Integration",
+                       Name:       "other-integration-owner",
+               },
+       }
+       gcTrait.Client, _ = test.NewFakeClient(deployment)
+
+       environment.Client = gcTrait.Client
+
+       resourceDeleted := false
+       fakeClient := gcTrait.Client.(*test.FakeClient) //nolint
+       fakeClient.Intercept(&interceptor.Funcs{
+               Delete: func(ctx context.Context, client ctrl.WithWatch, obj 
ctrl.Object, opts ...ctrl.DeleteOption) error {
+                       resourceDeleted = true
+                       return nil
+               },
+       })
+       err := gcTrait.garbageCollectResources(environment)
+
+       require.NoError(t, err)
+       assert.False(t, resourceDeleted)
+}
+
+func TestGarbageCollectKnativeServiceResources(t *testing.T) {
+       gcTrait, environment := createNominalGCTest()
+       environment.Integration.Generation = 2
+       environment.Integration.Spec.Profile = v1.TraitProfileKnative
+
+       gcTrait.Client, _ = test.NewFakeClient(&servingv1.Service{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      environment.Integration.Name,
+                       Namespace: environment.Integration.Namespace,
+                       Labels: map[string]string{
+                               v1.IntegrationLabel:           
environment.Integration.Name,
+                               v1.IntegrationGenerationLabel: "1",
+                       },
+                       OwnerReferences: []metav1.OwnerReference{
+                               {
+                                       APIVersion: 
v1.SchemeGroupVersion.String(),
+                                       Kind:       "Integration",
+                                       Name:       
environment.Integration.Name,
+                               },
+                       },
+               },
+       })
+
+       environment.Client = gcTrait.Client
+
+       resourceDeleted := false
+       fakeClient := gcTrait.Client.(*test.FakeClient) //nolint
+       fakeClient.Intercept(&interceptor.Funcs{
+               Delete: func(ctx context.Context, client ctrl.WithWatch, obj 
ctrl.Object, opts ...ctrl.DeleteOption) error {
+                       assert.Equal(t, environment.Integration.Name, 
obj.GetName())
+                       assert.Equal(t, "Service", 
obj.GetObjectKind().GroupVersionKind().Kind)
+                       assert.Equal(t, servingv1.SchemeGroupVersion, 
obj.GetObjectKind().GroupVersionKind().GroupVersion())
+                       resourceDeleted = true
+                       return nil
+               },
+       })
+       err := gcTrait.garbageCollectResources(environment)
+
+       require.NoError(t, err)
+       assert.True(t, resourceDeleted)
+}
+
+func TestGarbageCollectKnativeTriggerResources(t *testing.T) {
+       gcTrait, environment := createNominalGCTest()
+       environment.Integration.Generation = 2
+       environment.Integration.Spec.Profile = v1.TraitProfileKnative
+
+       gcTrait.Client, _ = test.NewFakeClient(&eventingv1.Trigger{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      environment.Integration.Name,
+                       Namespace: environment.Integration.Namespace,
+                       Labels: map[string]string{
+                               v1.IntegrationLabel:           
environment.Integration.Name,
+                               v1.IntegrationGenerationLabel: "1",
+                       },
+                       OwnerReferences: []metav1.OwnerReference{
+                               {
+                                       APIVersion: 
v1.SchemeGroupVersion.String(),
+                                       Kind:       "Integration",
+                                       Name:       
environment.Integration.Name,
+                               },
+                       },
+               },
+       })
+
+       environment.Client = gcTrait.Client
+
+       resourceDeleted := false
+       fakeClient := gcTrait.Client.(*test.FakeClient) //nolint
+       fakeClient.Intercept(&interceptor.Funcs{
+               Delete: func(ctx context.Context, client ctrl.WithWatch, obj 
ctrl.Object, opts ...ctrl.DeleteOption) error {
+                       assert.Equal(t, environment.Integration.Name, 
obj.GetName())
+                       assert.Equal(t, "Trigger", 
obj.GetObjectKind().GroupVersionKind().Kind)
+                       assert.Equal(t, eventingv1.SchemeGroupVersion, 
obj.GetObjectKind().GroupVersionKind().GroupVersion())
+                       resourceDeleted = true
+                       return nil
+               },
+       })
+       err := gcTrait.garbageCollectResources(environment)
+
+       require.NoError(t, err)
+       assert.True(t, resourceDeleted)
+}
+
+func getIntegrationDeployment(integration *v1.Integration) *appsv1.Deployment {
+       return &appsv1.Deployment{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      integration.Name,
+                       Namespace: integration.Namespace,
+                       Labels: map[string]string{
+                               v1.IntegrationLabel:           integration.Name,
+                               v1.IntegrationGenerationLabel: 
strconv.FormatInt(integration.Generation, 10),
+                       },
+                       OwnerReferences: []metav1.OwnerReference{
+                               {
+                                       APIVersion: 
v1.SchemeGroupVersion.String(),
+                                       Kind:       "Integration",
+                                       Name:       integration.Name,
+                               },
+                       },
+               },
+               Spec: appsv1.DeploymentSpec{
+                       Replicas: new(int32),
+                       Template: corev1.PodTemplateSpec{
+                               Spec: corev1.PodSpec{
+                                       Containers: []corev1.Container{
+                                               {
+                                                       Name: 
defaultContainerName,
+                                               },
+                                       },
+                               },
+                       },
+               },
+       }
+}
+
 func createNominalGCTest() (*gcTrait, *Environment) {
        trait, _ := newGCTrait().(*gcTrait)
        trait.Enabled = pointer.Bool(true)
@@ -98,6 +315,7 @@ func createNominalGCTest() (*gcTrait, *Environment) {
                Integration: &v1.Integration{
                        ObjectMeta: metav1.ObjectMeta{
                                Name:       "integration-name",
+                               Namespace:  "namespace",
                                Generation: 1,
                        },
                        Status: v1.IntegrationStatus{
diff --git a/pkg/trait/trait.go b/pkg/trait/trait.go
index 48e45b4d0..5d9076c13 100644
--- a/pkg/trait/trait.go
+++ b/pkg/trait/trait.go
@@ -74,14 +74,19 @@ func Apply(ctx context.Context, c client.Client, 
integration *v1.Integration, ki
                return nil, fmt.Errorf("error during trait customization: %w", 
err)
        }
 
+       postActionErrors := make([]error, 0)
        // execute post actions registered by traits
        for _, postAction := range environment.PostActions {
                err := postAction(environment)
                if err != nil {
-                       return nil, fmt.Errorf("error executing post actions: 
%w", err)
+                       postActionErrors = append(postActionErrors, err)
                }
        }
 
+       if len(postActionErrors) > 0 {
+               return nil, fmt.Errorf("error executing post actions - %d/%d 
failed: %s", len(postActionErrors), len(environment.PostActions), 
postActionErrors)
+       }
+
        switch {
        case integration != nil:
                ilog.Debug("Applied traits to Integration", "integration", 
integration.Name, "namespace", integration.Namespace)
diff --git a/pkg/util/test/client.go b/pkg/util/test/client.go
index 6c45fc0e1..a821bf81d 100644
--- a/pkg/util/test/client.go
+++ b/pkg/util/test/client.go
@@ -39,12 +39,14 @@ import (
        "k8s.io/client-go/kubernetes"
        fakeclientset "k8s.io/client-go/kubernetes/fake"
        clientscheme "k8s.io/client-go/kubernetes/scheme"
+       authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1"
        "k8s.io/client-go/rest"
        "k8s.io/client-go/scale"
        fakescale "k8s.io/client-go/scale/fake"
        "k8s.io/client-go/testing"
        controller "sigs.k8s.io/controller-runtime/pkg/client"
        "sigs.k8s.io/controller-runtime/pkg/client/fake"
+       "sigs.k8s.io/controller-runtime/pkg/client/interceptor"
 )
 
 // NewFakeClient ---.
@@ -142,6 +144,11 @@ type FakeClient struct {
        enabledOpenshift bool
 }
 
+func (c *FakeClient) Intercept(intercept *interceptor.Funcs) {
+       cw := c.Client.(controller.WithWatch) // nolint: forcetypeassert
+       c.Client = interceptor.NewClient(cw, *intercept)
+}
+
 func (c *FakeClient) AddReactor(verb, resource string, reaction 
testing.ReactionFunc) {
        c.camel.AddReactor(verb, resource, reaction)
 }
@@ -184,6 +191,14 @@ func (c *FakeClient) EnableOpenshiftDiscovery() {
        c.enabledOpenshift = true
 }
 
+func (c *FakeClient) AuthorizationV1() 
authorizationv1.AuthorizationV1Interface {
+       return &FakeAuthorization{
+               AuthorizationV1Interface: c.Interface.AuthorizationV1(),
+               disabledGroups:           c.disabledGroups,
+               enabledOpenshift:         c.enabledOpenshift,
+       }
+}
+
 func (c *FakeClient) Discovery() discovery.DiscoveryInterface {
        return &FakeDiscovery{
                DiscoveryInterface: c.Interface.Discovery(),
@@ -202,6 +217,16 @@ func (c *FakeClient) ScalesClient() (scale.ScalesGetter, 
error) {
        return c.scales, nil
 }
 
+type FakeAuthorization struct {
+       authorizationv1.AuthorizationV1Interface
+       disabledGroups   []string
+       enabledOpenshift bool
+}
+
+func (f *FakeAuthorization) SelfSubjectRulesReviews() 
authorizationv1.SelfSubjectRulesReviewInterface {
+       return f.AuthorizationV1Interface.SelfSubjectRulesReviews()
+}
+
 type FakeDiscovery struct {
        discovery.DiscoveryInterface
        disabledGroups   []string


Reply via email to