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

[camel-k] 03/09: feat(trait): enable custom tasks via builder

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

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

commit c864d653d569354704780d027e9a442af341cfea
Author: Pasquale Congiusti <pa...@gmail.com>
AuthorDate: Thu May 18 17:05:21 2023 +0200

    feat(trait): enable custom tasks via builder
---
 .../bases/camel.apache.org_integrationkits.yaml    |  6 +++
 .../camel.apache.org_integrationplatforms.yaml     | 12 ++++++
 .../crd/bases/camel.apache.org_integrations.yaml   |  6 +++
 .../bases/camel.apache.org_kameletbindings.yaml    |  6 +++
 config/crd/bases/camel.apache.org_pipes.yaml       |  6 +++
 config/rbac/operator-role.yaml                     |  1 +
 docs/modules/ROOT/partials/apis/camel-k-crds.adoc  | 41 ++++++++++++++++++
 e2e/common/traits/builder_test.go                  | 24 +++++++++++
 helm/camel-k/crds/crd-integration-kit.yaml         |  6 +++
 helm/camel-k/crds/crd-integration-platform.yaml    | 12 ++++++
 helm/camel-k/crds/crd-integration.yaml             |  6 +++
 helm/camel-k/crds/crd-kamelet-binding.yaml         |  6 +++
 helm/camel-k/crds/crd-pipe.yaml                    |  6 +++
 pkg/apis/camel/v1/trait/builder.go                 | 12 ++++++
 pkg/apis/camel/v1/trait/zz_generated.deepcopy.go   | 20 +++++++++
 pkg/controller/build/build_pod.go                  | 15 +++++++
 pkg/controller/build/monitor_pod.go                | 49 +++++++++++-----------
 pkg/trait/builder.go                               | 27 +++++++++++-
 pkg/trait/builder_test.go                          | 29 +++++++++++++
 pkg/util/kubernetes/log/util.go                    | 21 ++++++++++
 20 files changed, 285 insertions(+), 26 deletions(-)

diff --git a/config/crd/bases/camel.apache.org_integrationkits.yaml b/config/crd/bases/camel.apache.org_integrationkits.yaml
index 530927802..8ff5668a5 100644
--- a/config/crd/bases/camel.apache.org_integrationkits.yaml
+++ b/config/crd/bases/camel.apache.org_integrationkits.yaml
@@ -229,6 +229,12 @@ spec:
                         description: The strategy to use, either `pod` or `routine`
                           (default routine)
                         type: string
+                      tasks:
+                        description: A list of tasks to be executed (available only
+                          when using `pod` strategy) with format <name>;<container-image>;<container-command>
+                        items:
+                          type: string
+                        type: array
                       verbose:
                         description: Enable verbose logging on build components that
                           support it (e.g. Kaniko build pod).
diff --git a/config/crd/bases/camel.apache.org_integrationplatforms.yaml b/config/crd/bases/camel.apache.org_integrationplatforms.yaml
index 45fbbb428..94b37247d 100644
--- a/config/crd/bases/camel.apache.org_integrationplatforms.yaml
+++ b/config/crd/bases/camel.apache.org_integrationplatforms.yaml
@@ -727,6 +727,12 @@ spec:
                         description: The strategy to use, either `pod` or `routine`
                           (default routine)
                         type: string
+                      tasks:
+                        description: A list of tasks to be executed (available only
+                          when using `pod` strategy) with format <name>;<container-image>;<container-command>
+                        items:
+                          type: string
+                        type: array
                       verbose:
                         description: Enable verbose logging on build components that
                           support it (e.g. Kaniko build pod).
@@ -2653,6 +2659,12 @@ spec:
                         description: The strategy to use, either `pod` or `routine`
                           (default routine)
                         type: string
+                      tasks:
+                        description: A list of tasks to be executed (available only
+                          when using `pod` strategy) with format <name>;<container-image>;<container-command>
+                        items:
+                          type: string
+                        type: array
                       verbose:
                         description: Enable verbose logging on build components that
                           support it (e.g. Kaniko build pod).
diff --git a/config/crd/bases/camel.apache.org_integrations.yaml b/config/crd/bases/camel.apache.org_integrations.yaml
index b29aac0b2..0912bebc7 100644
--- a/config/crd/bases/camel.apache.org_integrations.yaml
+++ b/config/crd/bases/camel.apache.org_integrations.yaml
@@ -6212,6 +6212,12 @@ spec:
                         description: The strategy to use, either `pod` or `routine`
                           (default routine)
                         type: string
+                      tasks:
+                        description: A list of tasks to be executed (available only
+                          when using `pod` strategy) with format <name>;<container-image>;<container-command>
+                        items:
+                          type: string
+                        type: array
                       verbose:
                         description: Enable verbose logging on build components that
                           support it (e.g. Kaniko build pod).
diff --git a/config/crd/bases/camel.apache.org_kameletbindings.yaml b/config/crd/bases/camel.apache.org_kameletbindings.yaml
index 5707985ca..93b4eba21 100644
--- a/config/crd/bases/camel.apache.org_kameletbindings.yaml
+++ b/config/crd/bases/camel.apache.org_kameletbindings.yaml
@@ -6487,6 +6487,12 @@ spec:
                             description: The strategy to use, either `pod` or `routine`
                               (default routine)
                             type: string
+                          tasks:
+                            description: A list of tasks to be executed (available
+                              only when using `pod` strategy) with format <name>;<container-image>;<container-command>
+                            items:
+                              type: string
+                            type: array
                           verbose:
                             description: Enable verbose logging on build components
                               that support it (e.g. Kaniko build pod).
diff --git a/config/crd/bases/camel.apache.org_pipes.yaml b/config/crd/bases/camel.apache.org_pipes.yaml
index c1f04c4e2..6b166be6b 100644
--- a/config/crd/bases/camel.apache.org_pipes.yaml
+++ b/config/crd/bases/camel.apache.org_pipes.yaml
@@ -6484,6 +6484,12 @@ spec:
                             description: The strategy to use, either `pod` or `routine`
                               (default routine)
                             type: string
+                          tasks:
+                            description: A list of tasks to be executed (available
+                              only when using `pod` strategy) with format <name>;<container-image>;<container-command>
+                            items:
+                              type: string
+                            type: array
                           verbose:
                             description: Enable verbose logging on build components
                               that support it (e.g. Kaniko build pod).
diff --git a/config/rbac/operator-role.yaml b/config/rbac/operator-role.yaml
index 47ac7d44b..975028317 100644
--- a/config/rbac/operator-role.yaml
+++ b/config/rbac/operator-role.yaml
@@ -95,6 +95,7 @@ rules:
   - ""
   resources:
   - pods/proxy
+  - pods/log
   verbs:
   - get
 - apiGroups:
diff --git a/docs/modules/ROOT/partials/apis/camel-k-crds.adoc b/docs/modules/ROOT/partials/apis/camel-k-crds.adoc
index 59e476879..6967c654f 100644
--- a/docs/modules/ROOT/partials/apis/camel-k-crds.adoc
+++ b/docs/modules/ROOT/partials/apis/camel-k-crds.adoc
@@ -5495,6 +5495,13 @@ string
 
 When using `pod` strategy, the maximum amount of memory required by the pod builder.
 
+|`tasks` +
+[]string
+|
+
+
+A list of tasks to be executed (available only when using `pod` strategy) with format <name>;<container-image>;<container-command>
+
 
 |===
 
@@ -7504,6 +7511,40 @@ The type of service to be used, either 'ClusterIP', 'NodePort' or 'LoadBalancer'
 
 
 
+[#_camel_apache_org_v1_trait_Task]
+=== Task
+
+A Task is a generic operation run on the project
+
+[cols="2,2a",options="header"]
+|===
+|Field
+|Description
+
+|`name` +
+string
+|
+
+
+The name of the task to execute
+
+|`image` +
+string
+|
+
+
+The container image to use
+
+|`command` +
+string
+|
+
+
+The command to execute
+
+
+|===
+
 [#_camel_apache_org_v1_trait_TolerationTrait]
 === TolerationTrait
 
diff --git a/e2e/common/traits/builder_test.go b/e2e/common/traits/builder_test.go
index 8eeb0c834..9b47e48ff 100644
--- a/e2e/common/traits/builder_test.go
+++ b/e2e/common/traits/builder_test.go
@@ -95,4 +95,28 @@ func TestBuilderTrait(t *testing.T) {
 
 		Expect(Kamel("delete", "--all", "-n", ns).Execute()).To(Succeed())
 	})
+
+	t.Run("Run custom pipeline task", func(t *testing.T) {
+		Expect(KamelRunWithID(operatorID, ns, "files/Java.java",
+			"--name", name,
+			"-t", "builder.tasks=custom1;alpine;tree",
+			"-t", "builder.tasks=custom2;alpine;cat maven/pom.xml",
+		).Execute()).To(Succeed())
+
+		Eventually(IntegrationPodPhase(ns, name), TestTimeoutLong).Should(Equal(corev1.PodRunning))
+		Eventually(IntegrationConditionStatus(ns, name, v1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(corev1.ConditionTrue))
+		Eventually(IntegrationLogs(ns, name), TestTimeoutShort).Should(ContainSubstring("Magicstring!"))
+
+		integrationKitName := IntegrationKit(ns, name)()
+		builderKitName := fmt.Sprintf("camel-k-%s-builder", integrationKitName)
+		Eventually(BuilderPod(ns, builderKitName), TestTimeoutShort).ShouldNot(BeNil())
+		Eventually(len(BuilderPod(ns, builderKitName)().Spec.InitContainers), TestTimeoutShort).Should(Equal(3))
+		Eventually(BuilderPod(ns, builderKitName)().Spec.InitContainers[0].Name, TestTimeoutShort).Should(Equal("builder"))
+		Eventually(BuilderPod(ns, builderKitName)().Spec.InitContainers[1].Name, TestTimeoutShort).Should(Equal("custom1"))
+		Eventually(BuilderPod(ns, builderKitName)().Spec.InitContainers[2].Name, TestTimeoutShort).Should(Equal("custom2"))
+		Eventually(Logs(ns, builderKitName, corev1.PodLogOptions{Container: "custom1"})).Should(ContainSubstring(`generated-bytecode.jar`))
+		Eventually(Logs(ns, builderKitName, corev1.PodLogOptions{Container: "custom2"})).Should(ContainSubstring(`<artifactId>camel-k-runtime-bom</artifactId>`))
+
+		Expect(Kamel("delete", "--all", "-n", ns).Execute()).To(Succeed())
+	})
 }
diff --git a/helm/camel-k/crds/crd-integration-kit.yaml b/helm/camel-k/crds/crd-integration-kit.yaml
index 530927802..8ff5668a5 100644
--- a/helm/camel-k/crds/crd-integration-kit.yaml
+++ b/helm/camel-k/crds/crd-integration-kit.yaml
@@ -229,6 +229,12 @@ spec:
                         description: The strategy to use, either `pod` or `routine`
                           (default routine)
                         type: string
+                      tasks:
+                        description: A list of tasks to be executed (available only
+                          when using `pod` strategy) with format <name>;<container-image>;<container-command>
+                        items:
+                          type: string
+                        type: array
                       verbose:
                         description: Enable verbose logging on build components that
                           support it (e.g. Kaniko build pod).
diff --git a/helm/camel-k/crds/crd-integration-platform.yaml b/helm/camel-k/crds/crd-integration-platform.yaml
index 45fbbb428..94b37247d 100644
--- a/helm/camel-k/crds/crd-integration-platform.yaml
+++ b/helm/camel-k/crds/crd-integration-platform.yaml
@@ -727,6 +727,12 @@ spec:
                         description: The strategy to use, either `pod` or `routine`
                           (default routine)
                         type: string
+                      tasks:
+                        description: A list of tasks to be executed (available only
+                          when using `pod` strategy) with format <name>;<container-image>;<container-command>
+                        items:
+                          type: string
+                        type: array
                       verbose:
                         description: Enable verbose logging on build components that
                           support it (e.g. Kaniko build pod).
@@ -2653,6 +2659,12 @@ spec:
                         description: The strategy to use, either `pod` or `routine`
                           (default routine)
                         type: string
+                      tasks:
+                        description: A list of tasks to be executed (available only
+                          when using `pod` strategy) with format <name>;<container-image>;<container-command>
+                        items:
+                          type: string
+                        type: array
                       verbose:
                         description: Enable verbose logging on build components that
                           support it (e.g. Kaniko build pod).
diff --git a/helm/camel-k/crds/crd-integration.yaml b/helm/camel-k/crds/crd-integration.yaml
index b29aac0b2..0912bebc7 100644
--- a/helm/camel-k/crds/crd-integration.yaml
+++ b/helm/camel-k/crds/crd-integration.yaml
@@ -6212,6 +6212,12 @@ spec:
                         description: The strategy to use, either `pod` or `routine`
                           (default routine)
                         type: string
+                      tasks:
+                        description: A list of tasks to be executed (available only
+                          when using `pod` strategy) with format <name>;<container-image>;<container-command>
+                        items:
+                          type: string
+                        type: array
                       verbose:
                         description: Enable verbose logging on build components that
                           support it (e.g. Kaniko build pod).
diff --git a/helm/camel-k/crds/crd-kamelet-binding.yaml b/helm/camel-k/crds/crd-kamelet-binding.yaml
index 5707985ca..93b4eba21 100644
--- a/helm/camel-k/crds/crd-kamelet-binding.yaml
+++ b/helm/camel-k/crds/crd-kamelet-binding.yaml
@@ -6487,6 +6487,12 @@ spec:
                             description: The strategy to use, either `pod` or `routine`
                               (default routine)
                             type: string
+                          tasks:
+                            description: A list of tasks to be executed (available
+                              only when using `pod` strategy) with format <name>;<container-image>;<container-command>
+                            items:
+                              type: string
+                            type: array
                           verbose:
                             description: Enable verbose logging on build components
                               that support it (e.g. Kaniko build pod).
diff --git a/helm/camel-k/crds/crd-pipe.yaml b/helm/camel-k/crds/crd-pipe.yaml
index c1f04c4e2..6b166be6b 100644
--- a/helm/camel-k/crds/crd-pipe.yaml
+++ b/helm/camel-k/crds/crd-pipe.yaml
@@ -6484,6 +6484,12 @@ spec:
                             description: The strategy to use, either `pod` or `routine`
                               (default routine)
                             type: string
+                          tasks:
+                            description: A list of tasks to be executed (available
+                              only when using `pod` strategy) with format <name>;<container-image>;<container-command>
+                            items:
+                              type: string
+                            type: array
                           verbose:
                             description: Enable verbose logging on build components
                               that support it (e.g. Kaniko build pod).
diff --git a/pkg/apis/camel/v1/trait/builder.go b/pkg/apis/camel/v1/trait/builder.go
index 4f62bc0b9..282065eec 100644
--- a/pkg/apis/camel/v1/trait/builder.go
+++ b/pkg/apis/camel/v1/trait/builder.go
@@ -37,4 +37,16 @@ type BuilderTrait struct {
 	LimitCPU string `property:"limit-cpu" json:"limitCPU,omitempty"`
 	// When using `pod` strategy, the maximum amount of memory required by the pod builder.
 	LimitMemory string `property:"limit-memory" json:"limitMemory,omitempty"`
+	// A list of tasks to be executed (available only when using `pod` strategy) with format <name>;<container-image>;<container-command>
+	Tasks []string `property:"tasks" json:"tasks,omitempty"`
+}
+
+// A Task is a generic operation run on the project
+type Task struct {
+	// The name of the task to execute
+	Name string `property:"name" json:"name,omitempty"`
+	// The container image to use
+	Image string `property:"image" json:"image,omitempty"`
+	// The command to execute
+	Command string `property:"command" json:"command,omitempty"`
 }
diff --git a/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go b/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go
index 6643fcbd7..67f7eb349 100644
--- a/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go
+++ b/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go
@@ -64,6 +64,11 @@ func (in *BuilderTrait) DeepCopyInto(out *BuilderTrait) {
 		*out = make([]string, len(*in))
 		copy(*out, *in)
 	}
+	if in.Tasks != nil {
+		in, out := &in.Tasks, &out.Tasks
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BuilderTrait.
@@ -978,6 +983,21 @@ func (in *ServiceTrait) DeepCopy() *ServiceTrait {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Task) DeepCopyInto(out *Task) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Task.
+func (in *Task) DeepCopy() *Task {
+	if in == nil {
+		return nil
+	}
+	out := new(Task)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *TolerationTrait) DeepCopyInto(out *TolerationTrait) {
 	*out = *in
diff --git a/pkg/controller/build/build_pod.go b/pkg/controller/build/build_pod.go
index cb738edb4..5f6ab09d5 100644
--- a/pkg/controller/build/build_pod.go
+++ b/pkg/controller/build/build_pod.go
@@ -160,6 +160,8 @@ func newBuildPod(ctx context.Context, c ctrl.Reader, build *v1.Build) (*corev1.P
 			addBuildTaskToPod(build, task.S2i.Name, pod)
 		case task.Spectrum != nil:
 			addBuildTaskToPod(build, task.Spectrum.Name, pod)
+		case task.Custom != nil:
+			addCustomTaskToPod(build, task.Custom, pod)
 		}
 	}
 
@@ -530,6 +532,19 @@ func addKanikoTaskToPod(ctx context.Context, c ctrl.Reader, build *v1.Build, tas
 	return nil
 }
 
+func addCustomTaskToPod(build *v1.Build, task *v1.UserTask, pod *corev1.Pod) {
+	container := corev1.Container{
+		Name:            task.Name,
+		Image:           task.ContainerImage,
+		ImagePullPolicy: corev1.PullIfNotPresent,
+		Command:         strings.Split(task.ContainerCommand, " "),
+		WorkingDir:      filepath.Join(builderDir, build.Name),
+		Env:             proxyFromEnvironment(),
+	}
+
+	addContainerToPod(build, container, pod)
+}
+
 func addContainerToPod(build *v1.Build, container corev1.Container, pod *corev1.Pod) {
 	if hasVolume(pod, builderVolume) {
 		container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
diff --git a/pkg/controller/build/monitor_pod.go b/pkg/controller/build/monitor_pod.go
index 687bfb3ca..2e64e667d 100644
--- a/pkg/controller/build/monitor_pod.go
+++ b/pkg/controller/build/monitor_pod.go
@@ -19,7 +19,6 @@ package build
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"os"
 	"time"
@@ -36,6 +35,7 @@ import (
 	v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
 	"github.com/apache/camel-k/v2/pkg/platform"
 	"github.com/apache/camel-k/v2/pkg/util/kubernetes"
+	"github.com/apache/camel-k/v2/pkg/util/kubernetes/log"
 )
 
 const timeoutAnnotation = "camel.apache.org/timeout"
@@ -169,7 +169,7 @@ func (action *monitorPodAction) Handle(ctx context.Context, build *v1.Build) (*v
 	case corev1.PodFailed:
 		phase := v1.BuildPhaseFailed
 		message := "Pod failed"
-		if terminationMessage := action.getTerminationMessage(pod); terminationMessage != "" {
+		if terminationMessage := action.getTerminationMessage(ctx, pod); terminationMessage != "" {
 			message = terminationMessage
 		}
 		if pod.DeletionTimestamp != nil {
@@ -304,37 +304,36 @@ func (action *monitorPodAction) getTerminatedTime(pod *corev1.Pod) metav1.Time {
 	return finishedAt
 }
 
-func (action *monitorPodAction) getTerminationMessage(pod *corev1.Pod) string {
-	var terminationMessages []terminationMessage
-
+func (action *monitorPodAction) getTerminationMessage(ctx context.Context, pod *corev1.Pod) string {
 	var containers []corev1.ContainerStatus
 	containers = append(containers, pod.Status.InitContainerStatuses...)
 	containers = append(containers, pod.Status.ContainerStatuses...)
 
 	for _, container := range containers {
-		if t := container.State.Terminated; t != nil && t.ExitCode != 0 && t.Message != "" {
-			terminationMessages = append(terminationMessages, terminationMessage{
+		if t := container.State.Terminated; t != nil && t.ExitCode != 0 {
+			if t.Message != "" {
+				return fmt.Sprintf("Container %s failed with: %s", container.Name, t.Message)
+			}
+
+			var maxLines int64
+			maxLines = 20
+			logOptions := corev1.PodLogOptions{
 				Container: container.Name,
-				Message:   t.Message,
-			})
-		}
-	}
+				TailLines: &maxLines,
+			}
+			message, err := log.DumpLog(ctx, action.client, pod, logOptions)
+			if err != nil {
+				action.L.Errorf(err, "Dumping log for %s Pod failed", pod.Name)
+				return fmt.Sprintf(
+					"Container %s failed. Operator was not able to retrieve the error message, please, check the container log from %s Pod",
+					container.Name,
+					pod.Name,
+				)
+			}
 
-	switch len(terminationMessages) {
-	case 0:
-		return ""
-	case 1:
-		return terminationMessages[0].Message
-	default:
-		message, err := json.Marshal(terminationMessages)
-		if err != nil {
-			return ""
+			return fmt.Sprintf("Container %s failed with: %s", container.Name, message)
 		}
-		return string(message)
 	}
-}
 
-type terminationMessage struct {
-	Container string `json:"container,omitempty"`
-	Message   string `json:"message,omitempty"`
+	return ""
 }
diff --git a/pkg/trait/builder.go b/pkg/trait/builder.go
index ea583746b..bd20b38f1 100644
--- a/pkg/trait/builder.go
+++ b/pkg/trait/builder.go
@@ -20,6 +20,7 @@ package trait
 import (
 	"fmt"
 	"sort"
+	"strings"
 
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/utils/pointer"
@@ -61,6 +62,7 @@ func (t *builderTrait) Configure(e *Environment) (bool, error) {
 }
 
 func (t *builderTrait) Apply(e *Environment) error {
+	// Building task
 	builderTask, err := t.builderTask(e)
 	if err != nil {
 		e.IntegrationKit.Status.Phase = v1.IntegrationKitPhaseError
@@ -71,9 +73,14 @@ func (t *builderTrait) Apply(e *Environment) error {
 		}
 		return nil
 	}
-
 	e.Pipeline = append(e.Pipeline, v1.Task{Builder: builderTask})
 
+	// Custom tasks
+	if t.Tasks != nil {
+		e.Pipeline = append(e.Pipeline, t.customTasks()...)
+	}
+
+	// Publishing task
 	switch e.Platform.Status.Pipeline.PublishStrategy {
 	case v1.IntegrationPlatformBuildPublishStrategySpectrum:
 		e.Pipeline = append(e.Pipeline, v1.Task{Spectrum: &v1.SpectrumTask{
@@ -236,3 +243,21 @@ func getImageName(e *Environment) string {
 	}
 	return e.Platform.Status.Pipeline.Registry.Address + "/" + organization + "/camel-k-" + e.IntegrationKit.Name + ":" + e.IntegrationKit.ResourceVersion
 }
+
+func (t *builderTrait) customTasks() []v1.Task {
+	var customTasks []v1.Task
+	for _, t := range t.Tasks {
+		// TODO, better strategy than a simple split!
+		splitted := strings.Split(t, ";")
+		customTasks = append(customTasks, v1.Task{
+			Custom: &v1.UserTask{
+				BaseTask: v1.BaseTask{
+					Name: splitted[0],
+				},
+				ContainerImage:   splitted[1],
+				ContainerCommand: splitted[2],
+			},
+		})
+	}
+	return customTasks
+}
diff --git a/pkg/trait/builder_test.go b/pkg/trait/builder_test.go
index caad574aa..7bd87d41d 100644
--- a/pkg/trait/builder_test.go
+++ b/pkg/trait/builder_test.go
@@ -173,3 +173,32 @@ func createNominalBuilderTraitTest() *builderTrait {
 
 	return builderTrait
 }
+
+func TestCustomTaskBuilderTrait(t *testing.T) {
+	env := createBuilderTestEnv(v1.IntegrationPlatformClusterKubernetes, v1.IntegrationPlatformBuildPublishStrategySpectrum)
+	builderTrait := createNominalBuilderTraitTest()
+	builderTrait.Tasks = append(builderTrait.Tasks, "test;alpine;ls")
+
+	err := builderTrait.Apply(env)
+
+	assert.Nil(t, err)
+	builderTask := findCustomTaskByName(env.Pipeline, "builder")
+	publisherTask := findCustomTaskByName(env.Pipeline, "spectrum")
+	customTask := findCustomTaskByName(env.Pipeline, "test")
+	assert.NotNil(t, customTask)
+	assert.NotNil(t, builderTask)
+	assert.NotNil(t, publisherTask)
+	assert.Equal(t, 3, len(env.Pipeline))
+	assert.Equal(t, "test", customTask.Custom.Name)
+	assert.Equal(t, "alpine", customTask.Custom.ContainerImage)
+	assert.Equal(t, "ls", customTask.Custom.ContainerCommand)
+}
+
+func findCustomTaskByName(tasks []v1.Task, name string) v1.Task {
+	for _, t := range tasks {
+		if t.Custom != nil && t.Custom.Name == name {
+			return t
+		}
+	}
+	return v1.Task{}
+}
diff --git a/pkg/util/kubernetes/log/util.go b/pkg/util/kubernetes/log/util.go
index 8f9acdb98..f52f58e16 100644
--- a/pkg/util/kubernetes/log/util.go
+++ b/pkg/util/kubernetes/log/util.go
@@ -18,6 +18,7 @@ limitations under the License.
 package log
 
 import (
+	"bytes"
 	"context"
 	"fmt"
 	"io"
@@ -25,6 +26,7 @@ import (
 	v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1"
 	"github.com/spf13/cobra"
 
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/client-go/kubernetes"
 )
 
@@ -44,3 +46,22 @@ func PrintUsingSelector(ctx context.Context, cmd *cobra.Command, client kubernet
 
 	return nil
 }
+
+// DumpLog extract the full log from a Pod. Recommended when the quantity of log expected is minimum.
+func DumpLog(ctx context.Context, client kubernetes.Interface, pod *corev1.Pod, podLogOpts corev1.PodLogOptions) (string, error) {
+	req := client.CoreV1().Pods(pod.Namespace).GetLogs(pod.Name, &podLogOpts)
+	podLogs, err := req.Stream(ctx)
+	if err != nil {
+		return "", err
+	}
+	defer podLogs.Close()
+
+	buf := new(bytes.Buffer)
+	_, err = io.Copy(buf, podLogs)
+	if err != nil {
+		return "", err
+	}
+	str := buf.String()
+
+	return str, nil
+}