You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by as...@apache.org on 2019/12/18 12:34:46 UTC

[camel-k] 01/13: feat(build): Task-based builds

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

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

commit 7447c265fd87d03d08065ce01393c65b0345333c
Author: Antonin Stefanutti <an...@stefanutti.fr>
AuthorDate: Mon Dec 16 09:57:06 2019 +0100

    feat(build): Task-based builds
---
 pkg/apis/camel/v1alpha1/build_types.go             |  58 +++-
 pkg/apis/camel/v1alpha1/build_types_support.go     |  10 +
 pkg/apis/camel/v1alpha1/zz_generated.deepcopy.go   | 203 +++++++++++---
 pkg/builder/builder.go                             |  43 +--
 pkg/builder/builder_steps.go                       |   6 +-
 pkg/builder/builder_steps_test.go                  |  40 ++-
 pkg/builder/builder_test.go                        |  20 +-
 pkg/builder/builder_types.go                       |  15 +-
 pkg/builder/kaniko/publisher.go                    | 241 +----------------
 pkg/builder/runtime/main.go                        |   6 +-
 pkg/builder/runtime/main_test.go                   |  28 +-
 pkg/builder/runtime/quarkus.go                     |   6 +-
 pkg/builder/s2i/publisher.go                       |   7 +-
 pkg/cmd/builder.go                                 |   4 +-
 pkg/cmd/builder/builder.go                         |  55 ++--
 pkg/controller/build/build_controller.go           |  34 ++-
 pkg/controller/build/initialize_pod.go             |  45 +++-
 pkg/controller/build/initialize_routine.go         |   3 +-
 pkg/controller/build/monitor_pod.go                |  50 ++--
 pkg/controller/build/monitor_routine.go            |   4 +-
 pkg/controller/build/schedule_pod.go               | 182 +++++--------
 pkg/controller/build/schedule_routine.go           |  63 +++--
 pkg/controller/build/util_pod.go                   |  73 -----
 pkg/controller/integrationkit/build.go             |  27 +-
 pkg/controller/integrationplatform/initialize.go   |  14 +-
 pkg/controller/integrationplatform/kaniko_cache.go |   6 +-
 pkg/trait/builder.go                               | 295 ++++++++++++++++++++-
 pkg/trait/builder_test.go                          |  32 ++-
 pkg/trait/deployer.go                              |  77 +-----
 pkg/trait/quarkus.go                               |   7 +-
 pkg/trait/trait_types.go                           |   4 +-
 pkg/util/patch/patch.go                            |  91 +++++++
 32 files changed, 951 insertions(+), 798 deletions(-)

diff --git a/pkg/apis/camel/v1alpha1/build_types.go b/pkg/apis/camel/v1alpha1/build_types.go
index 9f13e60..e694b92 100644
--- a/pkg/apis/camel/v1alpha1/build_types.go
+++ b/pkg/apis/camel/v1alpha1/build_types.go
@@ -29,17 +29,52 @@ import (
 type BuildSpec struct {
 	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
 	// Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
-	Meta            metav1.ObjectMeta       `json:"meta,omitempty"`
-	Image           string                  `json:"image,omitempty"`
-	Steps           []string                `json:"steps,omitempty"`
-	CamelVersion    string                  `json:"camelVersion,omitempty"`
-	RuntimeVersion  string                  `json:"runtimeVersion,omitempty"`
-	RuntimeProvider *RuntimeProvider        `json:"runtimeProvider,omitempty"`
-	Platform        IntegrationPlatformSpec `json:"platform,omitempty"`
-	Sources         []SourceSpec            `json:"sources,omitempty"`
-	Resources       []ResourceSpec          `json:"resources,omitempty"`
-	Dependencies    []string                `json:"dependencies,omitempty"`
-	BuildDir        string                  `json:"buildDir,omitempty"`
+	Tasks []Task `json:"tasks,omitempty"`
+}
+
+type Task struct {
+	Builder *BuilderTask `json:"builder,omitempty"`
+	Kaniko  *KanikoTask  `json:"kaniko,omitempty"`
+}
+
+// BaseTask
+type BaseTask struct {
+	Name         string               `json:"name,omitempty"`
+	Affinity     *corev1.Affinity     `json:"affinity,omitempty"`
+	Volumes      []corev1.Volume      `json:"volumes,omitempty"`
+	VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"`
+}
+
+// ImageTask
+type ImageTask struct {
+	BaseTask `json:",inline"`
+	Image    string          `json:"image,omitempty"`
+	Args     []string        `json:"args,omitempty"`
+	Env      []corev1.EnvVar `json:"env,omitempty"`
+}
+
+// KanikoTask
+type KanikoTask struct {
+	ImageTask  `json:",inline"`
+	BuiltImage string `json:"builtImage,omitempty"`
+}
+
+// BuilderTask
+type BuilderTask struct {
+	BaseTask        `json:",inline"`
+	Meta            metav1.ObjectMeta `json:"meta,omitempty"`
+	BaseImage       string            `json:"baseImage,omitempty"`
+	CamelVersion    string            `json:"camelVersion,omitempty"`
+	RuntimeVersion  string            `json:"runtimeVersion,omitempty"`
+	RuntimeProvider *RuntimeProvider  `json:"runtimeProvider,omitempty"`
+	Sources         []SourceSpec      `json:"sources,omitempty"`
+	Resources       []ResourceSpec    `json:"resources,omitempty"`
+	Dependencies    []string          `json:"dependencies,omitempty"`
+	Steps           []string          `json:"steps,omitempty"`
+	Maven           MavenSpec         `json:"maven,omitempty"`
+	BuildDir        string            `json:"buildDir,omitempty"`
+	Properties      map[string]string `json:"properties,omitempty"`
+	Timeout         metav1.Duration   `json:"timeout,omitempty"`
 }
 
 // BuildStatus defines the observed state of Build
@@ -53,7 +88,6 @@ type BuildStatus struct {
 	StartedAt  metav1.Time      `json:"startedAt,omitempty"`
 	Platform   string           `json:"platform,omitempty"`
 	Conditions []BuildCondition `json:"conditions,omitempty"`
-
 	// Change to Duration / ISO 8601 when CRD uses OpenAPI spec v3
 	// https://github.com/OAI/OpenAPI-Specification/issues/845
 	Duration string `json:"duration,omitempty"`
diff --git a/pkg/apis/camel/v1alpha1/build_types_support.go b/pkg/apis/camel/v1alpha1/build_types_support.go
index 8aae4ad..4fb40b7 100644
--- a/pkg/apis/camel/v1alpha1/build_types_support.go
+++ b/pkg/apis/camel/v1alpha1/build_types_support.go
@@ -22,6 +22,16 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
+// GetName --
+func (t *Task) GetName() string {
+	if t.Builder != nil {
+		return t.Builder.Name
+	} else if t.Kaniko != nil {
+		return t.Kaniko.Name
+	}
+	return ""
+}
+
 // NewBuild --
 func NewBuild(namespace string, name string) Build {
 	return Build{
diff --git a/pkg/apis/camel/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/camel/v1alpha1/zz_generated.deepcopy.go
index a930cce..1ecf9f0 100644
--- a/pkg/apis/camel/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/camel/v1alpha1/zz_generated.deepcopy.go
@@ -5,8 +5,8 @@
 package v1alpha1
 
 import (
-	corev1 "k8s.io/api/core/v1"
-	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 
@@ -27,6 +27,41 @@ func (in *Artifact) DeepCopy() *Artifact {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *BaseTask) DeepCopyInto(out *BaseTask) {
+	*out = *in
+	if in.Affinity != nil {
+		in, out := &in.Affinity, &out.Affinity
+		*out = new(v1.Affinity)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Volumes != nil {
+		in, out := &in.Volumes, &out.Volumes
+		*out = make([]v1.Volume, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.VolumeMounts != nil {
+		in, out := &in.VolumeMounts, &out.VolumeMounts
+		*out = make([]v1.VolumeMount, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BaseTask.
+func (in *BaseTask) DeepCopy() *BaseTask {
+	if in == nil {
+		return nil
+	}
+	out := new(BaseTask)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Build) DeepCopyInto(out *Build) {
 	*out = *in
 	out.TypeMeta = in.TypeMeta
@@ -108,32 +143,12 @@ func (in *BuildList) DeepCopyObject() runtime.Object {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *BuildSpec) DeepCopyInto(out *BuildSpec) {
 	*out = *in
-	in.Meta.DeepCopyInto(&out.Meta)
-	if in.Steps != nil {
-		in, out := &in.Steps, &out.Steps
-		*out = make([]string, len(*in))
-		copy(*out, *in)
-	}
-	if in.RuntimeProvider != nil {
-		in, out := &in.RuntimeProvider, &out.RuntimeProvider
-		*out = new(RuntimeProvider)
-		(*in).DeepCopyInto(*out)
-	}
-	in.Platform.DeepCopyInto(&out.Platform)
-	if in.Sources != nil {
-		in, out := &in.Sources, &out.Sources
-		*out = make([]SourceSpec, len(*in))
-		copy(*out, *in)
-	}
-	if in.Resources != nil {
-		in, out := &in.Resources, &out.Resources
-		*out = make([]ResourceSpec, len(*in))
-		copy(*out, *in)
-	}
-	if in.Dependencies != nil {
-		in, out := &in.Dependencies, &out.Dependencies
-		*out = make([]string, len(*in))
-		copy(*out, *in)
+	if in.Tasks != nil {
+		in, out := &in.Tasks, &out.Tasks
+		*out = make([]Task, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
 	}
 	return
 }
@@ -183,6 +198,58 @@ func (in *BuildStatus) DeepCopy() *BuildStatus {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *BuilderTask) DeepCopyInto(out *BuilderTask) {
+	*out = *in
+	in.BaseTask.DeepCopyInto(&out.BaseTask)
+	in.Meta.DeepCopyInto(&out.Meta)
+	if in.RuntimeProvider != nil {
+		in, out := &in.RuntimeProvider, &out.RuntimeProvider
+		*out = new(RuntimeProvider)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Sources != nil {
+		in, out := &in.Sources, &out.Sources
+		*out = make([]SourceSpec, len(*in))
+		copy(*out, *in)
+	}
+	if in.Resources != nil {
+		in, out := &in.Resources, &out.Resources
+		*out = make([]ResourceSpec, len(*in))
+		copy(*out, *in)
+	}
+	if in.Dependencies != nil {
+		in, out := &in.Dependencies, &out.Dependencies
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.Steps != nil {
+		in, out := &in.Steps, &out.Steps
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	in.Maven.DeepCopyInto(&out.Maven)
+	if in.Properties != nil {
+		in, out := &in.Properties, &out.Properties
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+	out.Timeout = in.Timeout
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BuilderTask.
+func (in *BuilderTask) DeepCopy() *BuilderTask {
+	if in == nil {
+		return nil
+	}
+	out := new(BuilderTask)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CamelArtifact) DeepCopyInto(out *CamelArtifact) {
 	*out = *in
 	in.CamelArtifactDependency.DeepCopyInto(&out.CamelArtifactDependency)
@@ -447,6 +514,35 @@ func (in *FailureRecovery) DeepCopy() *FailureRecovery {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ImageTask) DeepCopyInto(out *ImageTask) {
+	*out = *in
+	in.BaseTask.DeepCopyInto(&out.BaseTask)
+	if in.Args != nil {
+		in, out := &in.Args, &out.Args
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.Env != nil {
+		in, out := &in.Env, &out.Env
+		*out = make([]v1.EnvVar, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageTask.
+func (in *ImageTask) DeepCopy() *ImageTask {
+	if in == nil {
+		return nil
+	}
+	out := new(ImageTask)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Integration) DeepCopyInto(out *Integration) {
 	*out = *in
 	out.TypeMeta = in.TypeMeta
@@ -726,7 +822,7 @@ func (in *IntegrationPlatformBuildSpec) DeepCopyInto(out *IntegrationPlatformBui
 	out.Registry = in.Registry
 	if in.Timeout != nil {
 		in, out := &in.Timeout, &out.Timeout
-		*out = new(v1.Duration)
+		*out = new(metav1.Duration)
 		**out = **in
 	}
 	in.Maven.DeepCopyInto(&out.Maven)
@@ -997,12 +1093,29 @@ func (in *IntegrationStatus) DeepCopy() *IntegrationStatus {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *KanikoTask) DeepCopyInto(out *KanikoTask) {
+	*out = *in
+	in.ImageTask.DeepCopyInto(&out.ImageTask)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KanikoTask.
+func (in *KanikoTask) DeepCopy() *KanikoTask {
+	if in == nil {
+		return nil
+	}
+	out := new(KanikoTask)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *MavenSpec) DeepCopyInto(out *MavenSpec) {
 	*out = *in
 	in.Settings.DeepCopyInto(&out.Settings)
 	if in.Timeout != nil {
 		in, out := &in.Timeout, &out.Timeout
-		*out = new(v1.Duration)
+		*out = new(metav1.Duration)
 		**out = **in
 	}
 	return
@@ -1090,6 +1203,32 @@ func (in *SourceSpec) DeepCopy() *SourceSpec {
 }
 
 // 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
+	if in.Builder != nil {
+		in, out := &in.Builder, &out.Builder
+		*out = new(BuilderTask)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Kaniko != nil {
+		in, out := &in.Kaniko, &out.Kaniko
+		*out = new(KanikoTask)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// 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 *TraitSpec) DeepCopyInto(out *TraitSpec) {
 	*out = *in
 	if in.Configuration != nil {
@@ -1117,12 +1256,12 @@ func (in *ValueSource) DeepCopyInto(out *ValueSource) {
 	*out = *in
 	if in.ConfigMapKeyRef != nil {
 		in, out := &in.ConfigMapKeyRef, &out.ConfigMapKeyRef
-		*out = new(corev1.ConfigMapKeySelector)
+		*out = new(v1.ConfigMapKeySelector)
 		(*in).DeepCopyInto(*out)
 	}
 	if in.SecretKeyRef != nil {
 		in, out := &in.SecretKeyRef, &out.SecretKeyRef
-		*out = new(corev1.SecretKeySelector)
+		*out = new(v1.SecretKeySelector)
 		(*in).DeepCopyInto(*out)
 	}
 	return
diff --git a/pkg/builder/builder.go b/pkg/builder/builder.go
index 250c682..2683386 100644
--- a/pkg/builder/builder.go
+++ b/pkg/builder/builder.go
@@ -24,8 +24,6 @@ import (
 	"sort"
 	"time"
 
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
 	"github.com/apache/camel-k/pkg/client"
 	"github.com/apache/camel-k/pkg/util/cancellable"
@@ -49,18 +47,9 @@ func New(c client.Client) Builder {
 	return &m
 }
 
-// Build --
-func (b *defaultBuilder) Build(build v1alpha1.BuildSpec) <-chan v1alpha1.BuildStatus {
-	channel := make(chan v1alpha1.BuildStatus)
-	go b.build(build, channel)
-	return channel
-}
-
-func (b *defaultBuilder) build(build v1alpha1.BuildSpec, channel chan<- v1alpha1.BuildStatus) {
+// Run --
+func (b *defaultBuilder) Run(build v1alpha1.BuilderTask) v1alpha1.BuildStatus {
 	result := v1alpha1.BuildStatus{}
-	result.Phase = v1alpha1.BuildPhaseRunning
-	result.StartedAt = metav1.Now()
-	channel <- result
 
 	// create tmp path
 	buildDir := build.BuildDir
@@ -82,22 +71,16 @@ func (b *defaultBuilder) build(build v1alpha1.BuildSpec, channel chan<- v1alpha1
 		Path:      builderPath,
 		Namespace: build.Meta.Namespace,
 		Build:     build,
-		Image:     build.Platform.Build.BaseImage,
-	}
-
-	if build.Image != "" {
-		c.Image = build.Image
+		BaseImage: build.BaseImage,
 	}
 
 	// base image is mandatory
-	if c.Image == "" {
+	if c.BaseImage == "" {
 		result.Phase = v1alpha1.BuildPhaseFailed
 		result.Image = ""
 		result.Error = "no base image defined"
 	}
 
-	c.BaseImage = c.Image
-
 	// Add sources
 	for _, data := range build.Sources {
 		c.Resources = append(c.Resources, Resource{
@@ -121,10 +104,7 @@ func (b *defaultBuilder) build(build v1alpha1.BuildSpec, channel chan<- v1alpha1
 	}
 
 	if result.Phase == v1alpha1.BuildPhaseFailed {
-		result.Duration = metav1.Now().Sub(result.StartedAt.Time).String()
-		channel <- result
-		close(channel)
-		return
+		return result
 	}
 
 	steps := make([]Step, 0)
@@ -154,7 +134,7 @@ func (b *defaultBuilder) build(build v1alpha1.BuildSpec, channel chan<- v1alpha1
 			l := b.log.WithValues(
 				"step", step.ID(),
 				"phase", step.Phase(),
-				"kit", build.Meta.Name,
+				"task", build.Name,
 			)
 
 			l.Infof("executing step")
@@ -170,10 +150,7 @@ func (b *defaultBuilder) build(build v1alpha1.BuildSpec, channel chan<- v1alpha1
 		}
 	}
 
-	result.Duration = metav1.Now().Sub(result.StartedAt.Time).String()
-
 	if result.Phase != v1alpha1.BuildPhaseInterrupted {
-		result.Phase = v1alpha1.BuildPhaseSucceeded
 		result.BaseImage = c.BaseImage
 		result.Image = c.Image
 
@@ -185,17 +162,15 @@ func (b *defaultBuilder) build(build v1alpha1.BuildSpec, channel chan<- v1alpha1
 		result.Artifacts = make([]v1alpha1.Artifact, 0, len(c.Artifacts))
 		result.Artifacts = append(result.Artifacts, c.Artifacts...)
 
-		b.log.Infof("build request %s executed in %s", build.Meta.Name, result.Duration)
 		b.log.Infof("dependencies: %s", build.Dependencies)
 		b.log.Infof("artifacts: %s", artifactIDs(c.Artifacts))
 		b.log.Infof("artifacts selected: %s", artifactIDs(c.SelectedArtifacts))
-		b.log.Infof("requested image: %s", build.Image)
+		b.log.Infof("requested image: %s", build.BaseImage)
 		b.log.Infof("base image: %s", c.BaseImage)
 		b.log.Infof("resolved image: %s", c.Image)
 	} else {
-		b.log.Infof("build request %s interrupted after %s", build.Meta.Name, result.Duration)
+		b.log.Infof("build task %s interrupted", build.Name)
 	}
 
-	channel <- result
-	close(channel)
+	return result
 }
diff --git a/pkg/builder/builder_steps.go b/pkg/builder/builder_steps.go
index d44d579..4b84f9f 100644
--- a/pkg/builder/builder_steps.go
+++ b/pkg/builder/builder_steps.go
@@ -109,7 +109,7 @@ func registerStep(steps ...Step) {
 }
 
 func generateProjectSettings(ctx *Context) error {
-	val, err := kubernetes.ResolveValueSource(ctx.C, ctx.Client, ctx.Namespace, &ctx.Build.Platform.Build.Maven.Settings)
+	val, err := kubernetes.ResolveValueSource(ctx.C, ctx.Client, ctx.Namespace, &ctx.Build.Maven.Settings)
 	if err != nil {
 		return err
 	}
@@ -283,7 +283,7 @@ func incrementalPackager(ctx *Context) error {
 			}
 
 			ctx.BaseImage = bestImage.Image
-			ctx.Image = bestImage.Image
+			//ctx.Image = bestImage.Image
 			ctx.SelectedArtifacts = selectedArtifacts
 		}
 
@@ -297,7 +297,7 @@ func packager(ctx *Context, selector artifactsSelector) error {
 		return err
 	}
 
-	tarFileName := path.Join(ctx.Path, "package", "occi.tar")
+	tarFileName := path.Join(ctx.Build.BuildDir, "package", ctx.Build.Meta.Name)
 	tarFileDir := path.Dir(tarFileName)
 
 	err = os.MkdirAll(tarFileDir, 0777)
diff --git a/pkg/builder/builder_steps_test.go b/pkg/builder/builder_steps_test.go
index 1985dee..b7f6dff 100644
--- a/pkg/builder/builder_steps_test.go
+++ b/pkg/builder/builder_steps_test.go
@@ -76,20 +76,16 @@ func TestMavenSettingsFromConfigMap(t *testing.T) {
 		Catalog:   catalog,
 		Client:    c,
 		Namespace: "ns",
-		Build: v1alpha1.BuildSpec{
+		Build: v1alpha1.BuilderTask{
 			RuntimeVersion: catalog.RuntimeVersion,
-			Platform: v1alpha1.IntegrationPlatformSpec{
-				Build: v1alpha1.IntegrationPlatformBuildSpec{
-					CamelVersion: catalog.Version,
-					Maven: v1alpha1.MavenSpec{
-						Settings: v1alpha1.ValueSource{
-							ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
-								LocalObjectReference: corev1.LocalObjectReference{
-									Name: "maven-settings",
-								},
-								Key: "settings.xml",
-							},
+			CamelVersion:   catalog.Version,
+			Maven: v1alpha1.MavenSpec{
+				Settings: v1alpha1.ValueSource{
+					ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
+						LocalObjectReference: corev1.LocalObjectReference{
+							Name: "maven-settings",
 						},
+						Key: "settings.xml",
 					},
 				},
 			},
@@ -128,20 +124,16 @@ func TestMavenSettingsFromSecret(t *testing.T) {
 		Catalog:   catalog,
 		Client:    c,
 		Namespace: "ns",
-		Build: v1alpha1.BuildSpec{
+		Build: v1alpha1.BuilderTask{
 			RuntimeVersion: catalog.RuntimeVersion,
-			Platform: v1alpha1.IntegrationPlatformSpec{
-				Build: v1alpha1.IntegrationPlatformBuildSpec{
-					CamelVersion: catalog.Version,
-					Maven: v1alpha1.MavenSpec{
-						Settings: v1alpha1.ValueSource{
-							SecretKeyRef: &corev1.SecretKeySelector{
-								LocalObjectReference: corev1.LocalObjectReference{
-									Name: "maven-settings",
-								},
-								Key: "settings.xml",
-							},
+			CamelVersion:   catalog.Version,
+			Maven: v1alpha1.MavenSpec{
+				Settings: v1alpha1.ValueSource{
+					SecretKeyRef: &corev1.SecretKeySelector{
+						LocalObjectReference: corev1.LocalObjectReference{
+							Name: "maven-settings",
 						},
+						Key: "settings.xml",
 					},
 				},
 			},
diff --git a/pkg/builder/builder_test.go b/pkg/builder/builder_test.go
index b2a64f1..f1f90c1 100644
--- a/pkg/builder/builder_test.go
+++ b/pkg/builder/builder_test.go
@@ -53,27 +53,15 @@ func TestFailure(t *testing.T) {
 
 	RegisterSteps(steps)
 
-	r := v1alpha1.BuildSpec{
+	r := v1alpha1.BuilderTask{
 		Steps: StepIDsFor(
 			steps.Step1,
 			steps.Step2,
 		),
 		RuntimeVersion: catalog.RuntimeVersion,
-		Platform: v1alpha1.IntegrationPlatformSpec{
-			Build: v1alpha1.IntegrationPlatformBuildSpec{
-				CamelVersion: catalog.Version,
-			},
-		},
+		CamelVersion:   catalog.Version,
 	}
 
-	progress := b.Build(r)
-
-	status := make([]v1alpha1.BuildStatus, 0)
-	for s := range progress {
-		status = append(status, s)
-	}
-
-	assert.Len(t, status, 2)
-	assert.Equal(t, v1alpha1.BuildPhaseRunning, status[0].Phase)
-	assert.Equal(t, v1alpha1.BuildPhaseFailed, status[1].Phase)
+	status := b.Run(r)
+	assert.Equal(t, v1alpha1.BuildPhaseFailed, status.Phase)
 }
diff --git a/pkg/builder/builder_types.go b/pkg/builder/builder_types.go
index 9a21ed0..52e6462 100644
--- a/pkg/builder/builder_types.go
+++ b/pkg/builder/builder_types.go
@@ -45,7 +45,7 @@ const (
 
 // Builder --
 type Builder interface {
-	Build(build v1alpha1.BuildSpec) <-chan v1alpha1.BuildStatus
+	Run(build v1alpha1.BuilderTask) v1alpha1.BuildStatus
 }
 
 // Step --
@@ -101,7 +101,7 @@ type Context struct {
 	client.Client
 	C                 cancellable.Context
 	Catalog           *camel.RuntimeCatalog
-	Build             v1alpha1.BuildSpec
+	Build             v1alpha1.BuilderTask
 	BaseImage         string
 	Image             string
 	Error             error
@@ -120,16 +120,7 @@ type Context struct {
 
 // HasRequiredImage --
 func (c *Context) HasRequiredImage() bool {
-	return c.Build.Image != ""
-}
-
-// GetImage --
-func (c *Context) GetImage() string {
-	if c.Build.Image != "" {
-		return c.Build.Image
-	}
-
-	return c.Image
+	return c.Build.BaseImage != ""
 }
 
 type publishedImage struct {
diff --git a/pkg/builder/kaniko/publisher.go b/pkg/builder/kaniko/publisher.go
index 734638b..f34f2fb 100644
--- a/pkg/builder/kaniko/publisher.go
+++ b/pkg/builder/kaniko/publisher.go
@@ -18,64 +18,19 @@ limitations under the License.
 package kaniko
 
 import (
-	"fmt"
 	"io/ioutil"
 	"os"
 	"path"
-	"strconv"
-	"time"
 
 	"github.com/apache/camel-k/pkg/builder"
-	"github.com/apache/camel-k/pkg/util/defaults"
-	"github.com/apache/camel-k/pkg/util/kubernetes"
 	"github.com/apache/camel-k/pkg/util/tar"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-
-	"github.com/pkg/errors"
-
-	corev1 "k8s.io/api/core/v1"
-	apierrors "k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-)
-
-type secretKind struct {
-	fileName    string
-	mountPath   string
-	destination string
-	refEnv      string
-}
-
-var (
-	secretKindGCR = secretKind{
-		fileName:    "kaniko-secret.json",
-		mountPath:   "/secret",
-		destination: "kaniko-secret.json",
-		refEnv:      "GOOGLE_APPLICATION_CREDENTIALS",
-	}
-	secretKindPlainDocker = secretKind{
-		fileName:    "config.json",
-		mountPath:   "/kaniko/.docker",
-		destination: "config.json",
-	}
-	secretKindStandardDocker = secretKind{
-		fileName:    corev1.DockerConfigJsonKey,
-		mountPath:   "/kaniko/.docker",
-		destination: "config.json",
-	}
-
-	allSecretKinds = []secretKind{secretKindGCR, secretKindPlainDocker, secretKindStandardDocker}
 )
 
 func publisher(ctx *builder.Context) error {
-	organization := ctx.Build.Platform.Build.Registry.Organization
-	if organization == "" {
-		organization = ctx.Namespace
-	}
-	image := ctx.Build.Platform.Build.Registry.Address + "/" + organization + "/camel-k-" + ctx.Build.Meta.Name + ":" + ctx.Build.Meta.ResourceVersion
 	baseDir, _ := path.Split(ctx.Archive)
 	contextDir := path.Join(baseDir, "context")
 
-	err := os.Mkdir(contextDir, 0777)
+	err := os.MkdirAll(contextDir, 0777)
 	if err != nil {
 		return err
 	}
@@ -86,7 +41,7 @@ func publisher(ctx *builder.Context) error {
 
 	// #nosec G202
 	dockerFileContent := []byte(`
-		FROM ` + ctx.Image + `
+		FROM ` + ctx.BaseImage + `
 		ADD . /deployments
 	`)
 
@@ -95,197 +50,5 @@ func publisher(ctx *builder.Context) error {
 		return err
 	}
 
-	volumes := []corev1.Volume{
-		{
-			Name: "camel-k-builder",
-			VolumeSource: corev1.VolumeSource{
-				PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
-					ClaimName: ctx.Build.Platform.Build.PersistentVolumeClaim,
-				},
-			},
-		},
-	}
-	volumeMounts := []corev1.VolumeMount{
-		{
-			Name:      "camel-k-builder",
-			MountPath: BuildDir,
-		},
-	}
-	envs := make([]corev1.EnvVar, 0)
-	baseArgs := []string{
-		"--dockerfile=Dockerfile",
-		"--context=" + contextDir,
-		"--destination=" + image,
-		"--cache=" + strconv.FormatBool(ctx.Build.Platform.Build.IsKanikoCacheEnabled()),
-		"--cache-dir=/workspace/cache",
-	}
-
-	args := make([]string, 0, len(baseArgs))
-	args = append(args, baseArgs...)
-
-	if ctx.Build.Platform.Build.Registry.Insecure {
-		args = append(args, "--insecure")
-		args = append(args, "--insecure-pull")
-	}
-
-	if ctx.Build.Platform.Build.Registry.Secret != "" {
-		secretKind, err := getSecretKind(ctx, ctx.Build.Platform.Build.Registry.Secret)
-		if err != nil {
-			return err
-		}
-
-		volumes = append(volumes, corev1.Volume{
-			Name: "kaniko-secret",
-			VolumeSource: corev1.VolumeSource{
-				Secret: &corev1.SecretVolumeSource{
-					SecretName: ctx.Build.Platform.Build.Registry.Secret,
-					Items: []corev1.KeyToPath{
-						{
-							Key:  secretKind.fileName,
-							Path: secretKind.destination,
-						},
-					},
-				},
-			},
-		})
-
-		volumeMounts = append(volumeMounts, corev1.VolumeMount{
-			Name:      "kaniko-secret",
-			MountPath: secretKind.mountPath,
-		})
-
-		if secretKind.refEnv != "" {
-			envs = append(envs, corev1.EnvVar{
-				Name:  secretKind.refEnv,
-				Value: path.Join(secretKind.mountPath, secretKind.destination),
-			})
-		}
-		args = baseArgs
-	}
-
-	if ctx.Build.Platform.Build.HTTPProxySecret != "" {
-		optional := true
-		envs = append(envs, corev1.EnvVar{
-			Name: "HTTP_PROXY",
-			ValueFrom: &corev1.EnvVarSource{
-				SecretKeyRef: &corev1.SecretKeySelector{
-					LocalObjectReference: corev1.LocalObjectReference{
-						Name: ctx.Build.Platform.Build.HTTPProxySecret,
-					},
-					Key:      "HTTP_PROXY",
-					Optional: &optional,
-				},
-			},
-		})
-		envs = append(envs, corev1.EnvVar{
-			Name: "HTTPS_PROXY",
-			ValueFrom: &corev1.EnvVarSource{
-				SecretKeyRef: &corev1.SecretKeySelector{
-					LocalObjectReference: corev1.LocalObjectReference{
-						Name: ctx.Build.Platform.Build.HTTPProxySecret,
-					},
-					Key:      "HTTPS_PROXY",
-					Optional: &optional,
-				},
-			},
-		})
-		envs = append(envs, corev1.EnvVar{
-			Name: "NO_PROXY",
-			ValueFrom: &corev1.EnvVarSource{
-				SecretKeyRef: &corev1.SecretKeySelector{
-					LocalObjectReference: corev1.LocalObjectReference{
-						Name: ctx.Build.Platform.Build.HTTPProxySecret,
-					},
-					Key:      "NO_PROXY",
-					Optional: &optional,
-				},
-			},
-		})
-	}
-
-	pod := corev1.Pod{
-		TypeMeta: metav1.TypeMeta{
-			APIVersion: corev1.SchemeGroupVersion.String(),
-			Kind:       "Pod",
-		},
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: ctx.Namespace,
-			Name:      "camel-k-" + ctx.Build.Meta.Name,
-			Labels: map[string]string{
-				"app": "camel-k",
-			},
-		},
-		Spec: corev1.PodSpec{
-			Containers: []corev1.Container{
-				{
-					Name:         "kaniko",
-					Image:        fmt.Sprintf("gcr.io/kaniko-project/executor:v%s", defaults.KanikoVersion),
-					Args:         args,
-					Env:          envs,
-					VolumeMounts: volumeMounts,
-				},
-			},
-			RestartPolicy: corev1.RestartPolicyNever,
-			Volumes:       volumes,
-		},
-	}
-
-	// Co-locate with the build pod for sharing the volume
-	pod.Spec.Affinity = &corev1.Affinity{
-		PodAffinity: &corev1.PodAffinity{
-			RequiredDuringSchedulingIgnoredDuringExecution: []corev1.PodAffinityTerm{
-				{
-					LabelSelector: &metav1.LabelSelector{
-						MatchLabels: map[string]string{
-							"camel.apache.org/build": ctx.Build.Meta.Name,
-						},
-					},
-					TopologyKey: "kubernetes.io/hostname",
-				},
-			},
-		},
-	}
-
-	err = ctx.Client.Delete(ctx.C, &pod)
-	if err != nil && !apierrors.IsNotFound(err) {
-		return errors.Wrap(err, "cannot delete kaniko builder pod")
-	}
-
-	err = ctx.Client.Create(ctx.C, &pod)
-	if err != nil {
-		return errors.Wrap(err, "cannot create kaniko builder pod")
-	}
-
-	err = kubernetes.WaitCondition(ctx.C, ctx.Client, &pod, func(obj interface{}) (bool, error) {
-		if val, ok := obj.(*corev1.Pod); ok {
-			if val.Status.Phase == corev1.PodSucceeded {
-				return true, nil
-			}
-			if val.Status.Phase == corev1.PodFailed {
-				return false, fmt.Errorf("build failed: %s", val.Status.Message)
-			}
-		}
-		return false, nil
-	}, 10*time.Minute)
-
-	if err != nil {
-		return err
-	}
-
-	ctx.Image = image
 	return nil
 }
-
-func getSecretKind(ctx *builder.Context, name string) (secretKind, error) {
-	secret := corev1.Secret{}
-	key := client.ObjectKey{Namespace: ctx.Namespace, Name: name}
-	if err := ctx.Client.Get(ctx.C, key, &secret); err != nil {
-		return secretKind{}, err
-	}
-	for _, k := range allSecretKinds {
-		if _, ok := secret.Data[k.fileName]; ok {
-			return k, nil
-		}
-	}
-	return secretKind{}, errors.New("unsupported secret type for registry authentication")
-}
diff --git a/pkg/builder/runtime/main.go b/pkg/builder/runtime/main.go
index a191afd..c210819 100644
--- a/pkg/builder/runtime/main.go
+++ b/pkg/builder/runtime/main.go
@@ -58,7 +58,7 @@ func loadCamelCatalog(ctx *builder.Context) error {
 
 func generateProject(ctx *builder.Context) error {
 	p := maven.NewProjectWithGAV("org.apache.camel.k.integration", "camel-k-integration", defaults.Version)
-	p.Properties = ctx.Build.Platform.Build.Properties
+	p.Properties = ctx.Build.Properties
 	p.DependencyManagement = &maven.DependencyManagement{Dependencies: make([]maven.Dependency, 0)}
 	p.Dependencies = make([]maven.Dependency, 0)
 
@@ -86,8 +86,8 @@ func generateProject(ctx *builder.Context) error {
 func computeDependencies(ctx *builder.Context) error {
 	mc := maven.NewContext(path.Join(ctx.Path, "maven"), ctx.Maven.Project)
 	mc.SettingsContent = ctx.Maven.SettingsData
-	mc.LocalRepository = ctx.Build.Platform.Build.Maven.LocalRepository
-	mc.Timeout = ctx.Build.Platform.Build.Maven.GetTimeout().Duration
+	mc.LocalRepository = ctx.Build.Maven.LocalRepository
+	mc.Timeout = ctx.Build.Maven.GetTimeout().Duration
 	mc.AddArgumentf("org.apache.camel.k:camel-k-maven-plugin:%s:generate-dependency-list", ctx.Catalog.RuntimeVersion)
 
 	if err := maven.Run(mc); err != nil {
diff --git a/pkg/builder/runtime/main_test.go b/pkg/builder/runtime/main_test.go
index f149325..4c26360 100644
--- a/pkg/builder/runtime/main_test.go
+++ b/pkg/builder/runtime/main_test.go
@@ -34,14 +34,9 @@ func TestNewProject(t *testing.T) {
 
 	ctx := builder.Context{
 		Catalog: catalog,
-		Build: v1alpha1.BuildSpec{
+		Build: v1alpha1.BuilderTask{
 			CamelVersion:   catalog.Version,
 			RuntimeVersion: catalog.RuntimeVersion,
-			Platform: v1alpha1.IntegrationPlatformSpec{
-				Build: v1alpha1.IntegrationPlatformBuildSpec{
-					CamelVersion: catalog.Version,
-				},
-			},
 			Dependencies: []string{
 				"camel-k:runtime-main",
 				"bom:my.company/my-artifact-1/1.0.0",
@@ -97,14 +92,9 @@ func TestGenerateJvmProject(t *testing.T) {
 
 	ctx := builder.Context{
 		Catalog: catalog,
-		Build: v1alpha1.BuildSpec{
+		Build: v1alpha1.BuilderTask{
 			CamelVersion:   catalog.Version,
 			RuntimeVersion: catalog.RuntimeVersion,
-			Platform: v1alpha1.IntegrationPlatformSpec{
-				Build: v1alpha1.IntegrationPlatformBuildSpec{
-					CamelVersion: catalog.Version,
-				},
-			},
 			Dependencies: []string{
 				"camel-k:runtime-main",
 			},
@@ -154,14 +144,9 @@ func TestGenerateGroovyProject(t *testing.T) {
 
 	ctx := builder.Context{
 		Catalog: catalog,
-		Build: v1alpha1.BuildSpec{
+		Build: v1alpha1.BuilderTask{
 			CamelVersion:   catalog.Version,
 			RuntimeVersion: catalog.RuntimeVersion,
-			Platform: v1alpha1.IntegrationPlatformSpec{
-				Build: v1alpha1.IntegrationPlatformBuildSpec{
-					CamelVersion: catalog.Version,
-				},
-			},
 			Dependencies: []string{
 				"camel-k:runtime-main",
 				"camel-k:loader-groovy",
@@ -215,14 +200,9 @@ func TestSanitizeDependencies(t *testing.T) {
 
 	ctx := builder.Context{
 		Catalog: catalog,
-		Build: v1alpha1.BuildSpec{
+		Build: v1alpha1.BuilderTask{
 			CamelVersion:   catalog.Version,
 			RuntimeVersion: catalog.RuntimeVersion,
-			Platform: v1alpha1.IntegrationPlatformSpec{
-				Build: v1alpha1.IntegrationPlatformBuildSpec{
-					CamelVersion: catalog.Version,
-				},
-			},
 			Dependencies: []string{
 				"camel:undertow",
 				"mvn:org.apache.camel/camel-core/2.18.0",
diff --git a/pkg/builder/runtime/quarkus.go b/pkg/builder/runtime/quarkus.go
index f9c9809..e7dea56 100644
--- a/pkg/builder/runtime/quarkus.go
+++ b/pkg/builder/runtime/quarkus.go
@@ -59,7 +59,7 @@ func loadCamelQuarkusCatalog(ctx *builder.Context) error {
 
 func generateQuarkusProject(ctx *builder.Context) error {
 	p := maven.NewProjectWithGAV("org.apache.camel.k.integration", "camel-k-integration", defaults.Version)
-	p.Properties = ctx.Build.Platform.Build.Properties
+	p.Properties = ctx.Build.Properties
 	p.DependencyManagement = &maven.DependencyManagement{Dependencies: make([]maven.Dependency, 0)}
 	p.Dependencies = make([]maven.Dependency, 0)
 	p.Build = &maven.Build{Plugins: make([]maven.Plugin, 0)}
@@ -106,8 +106,8 @@ func generateQuarkusProject(ctx *builder.Context) error {
 func computeQuarkusDependencies(ctx *builder.Context) error {
 	mc := maven.NewContext(path.Join(ctx.Path, "maven"), ctx.Maven.Project)
 	mc.SettingsContent = ctx.Maven.SettingsData
-	mc.LocalRepository = ctx.Build.Platform.Build.Maven.LocalRepository
-	mc.Timeout = ctx.Build.Platform.Build.Maven.GetTimeout().Duration
+	mc.LocalRepository = ctx.Build.Maven.LocalRepository
+	mc.Timeout = ctx.Build.Maven.GetTimeout().Duration
 
 	// Build the project
 	mc.AddArgument("package")
diff --git a/pkg/builder/s2i/publisher.go b/pkg/builder/s2i/publisher.go
index eb8c598..88c0192 100644
--- a/pkg/builder/s2i/publisher.go
+++ b/pkg/builder/s2i/publisher.go
@@ -19,6 +19,8 @@ package s2i
 
 import (
 	"io/ioutil"
+	"os"
+	"path"
 
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -117,6 +119,9 @@ func publisher(ctx *builder.Context) error {
 		return errors.Wrap(err, "cannot fully read tar file "+ctx.Archive)
 	}
 
+	baseDir, _ := path.Split(ctx.Archive)
+	defer os.RemoveAll(baseDir)
+
 	restClient, err := customclient.GetClientFor(ctx.Client, "build.openshift.io", "v1")
 	if err != nil {
 		return err
@@ -156,7 +161,7 @@ func publisher(ctx *builder.Context) error {
 			}
 		}
 		return false, nil
-	}, ctx.Build.Platform.Build.GetTimeout().Duration)
+	}, ctx.Build.Timeout.Duration)
 
 	if err != nil {
 		return err
diff --git a/pkg/cmd/builder.go b/pkg/cmd/builder.go
index 5c541f7..ac93a47 100644
--- a/pkg/cmd/builder.go
+++ b/pkg/cmd/builder.go
@@ -36,6 +36,7 @@ func newCmdBuilder(rootCmdOptions *RootCmdOptions) (*cobra.Command, *builderCmdO
 	}
 
 	cmd.Flags().String("build-name", "", "The name of the build resource")
+	cmd.Flags().String("task-name", "", "The name of task to execute")
 
 	return &cmd, &options
 }
@@ -43,8 +44,9 @@ func newCmdBuilder(rootCmdOptions *RootCmdOptions) (*cobra.Command, *builderCmdO
 type builderCmdOptions struct {
 	*RootCmdOptions
 	BuildName string `mapstructure:"build-name"`
+	TaskName  string `mapstructure:"task-name"`
 }
 
 func (o *builderCmdOptions) run(_ *cobra.Command, _ []string) {
-	builder.Run(o.Namespace, o.BuildName)
+	builder.Run(o.Namespace, o.BuildName, o.TaskName)
 }
diff --git a/pkg/cmd/builder/builder.go b/pkg/cmd/builder/builder.go
index 7e06126..9fa9e9b 100644
--- a/pkg/cmd/builder/builder.go
+++ b/pkg/cmd/builder/builder.go
@@ -21,10 +21,12 @@ import (
 	"fmt"
 	"math/rand"
 	"os"
+	"reflect"
 	"runtime"
 	"time"
 
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"github.com/pkg/errors"
+
 	"k8s.io/apimachinery/pkg/types"
 
 	controller "sigs.k8s.io/controller-runtime/pkg/client"
@@ -37,6 +39,7 @@ import (
 	"github.com/apache/camel-k/pkg/util/cancellable"
 	"github.com/apache/camel-k/pkg/util/defaults"
 	logger "github.com/apache/camel-k/pkg/util/log"
+	"github.com/apache/camel-k/pkg/util/patch"
 )
 
 var log = logger.WithName("builder")
@@ -47,8 +50,8 @@ func printVersion() {
 	log.Info(fmt.Sprintf("Camel K Version: %v", defaults.Version))
 }
 
-// Run creates a build resource in the specified namespace
-func Run(namespace string, buildName string) {
+// Run a build resource in the specified namespace
+func Run(namespace string, buildName string, taskName string) {
 	logf.SetLogger(zap.New(func(o *zap.Options) {
 		o.Development = false
 	}))
@@ -61,33 +64,39 @@ func Run(namespace string, buildName string) {
 
 	ctx := cancellable.NewContext()
 
-	build := &v1alpha1.Build{
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: namespace,
-			Name:      buildName,
-		},
-	}
-
+	build := &v1alpha1.Build{}
 	exitOnError(
-		c.Get(ctx, types.NamespacedName{Namespace: build.Namespace, Name: build.Name}, build),
+		c.Get(ctx, types.NamespacedName{Namespace: namespace, Name: buildName}, build),
 	)
 
-	progress := builder.New(c).Build(build.Spec)
-	for status := range progress {
-		target := build.DeepCopy()
-		target.Status = status
-		// Copy the failure field from the build to persist recovery state
-		target.Status.Failure = build.Status.Failure
-		// Patch the build status with the current progress
-		exitOnError(c.Status().Patch(ctx, target, controller.MergeFrom(build)))
-		build.Status = target.Status
+	var task *v1alpha1.BuilderTask
+	for _, t := range build.Spec.Tasks {
+		if t.Builder != nil && t.Builder.Name == taskName {
+			task = t.Builder
+		}
 	}
+	if task == nil {
+		exitOnError(errors.Errorf("No task of type [%s] with name [%s] in build [%s/%s]",
+			reflect.TypeOf(v1alpha1.BuilderTask{}).Name(), taskName, namespace, buildName))
+	}
+
+	status := builder.New(c).Run(*task)
+	target := build.DeepCopy()
+	target.Status = status
+	// Copy the failure field from the build to persist recovery state
+	target.Status.Failure = build.Status.Failure
+	// Patch the build status with the result
+	p, err := patch.PositiveMergePatch(build, target)
+	exitOnError(err)
+	exitOnError(c.Status().Patch(ctx, target, controller.ConstantPatch(types.MergePatchType, p)))
+	build.Status = target.Status
 
 	switch build.Status.Phase {
-	case v1alpha1.BuildPhaseSucceeded:
-		os.Exit(0)
-	default:
+	case v1alpha1.BuildPhaseFailed:
+		log.Error(nil, build.Status.Error)
 		os.Exit(1)
+	default:
+		os.Exit(0)
 	}
 }
 
diff --git a/pkg/controller/build/build_controller.go b/pkg/controller/build/build_controller.go
index b6d227d..f9ef70c 100644
--- a/pkg/controller/build/build_controller.go
+++ b/pkg/controller/build/build_controller.go
@@ -134,7 +134,7 @@ func (r *ReconcileBuild) Reconcile(request reconcile.Request) (reconcile.Result,
 
 	ctx := context.TODO()
 
-	// Fetch the Integration instance
+	// Fetch the Build instance
 	var instance v1alpha1.Build
 
 	if err := r.client.Get(ctx, request.NamespacedName, &instance); err != nil {
@@ -151,8 +151,8 @@ func (r *ReconcileBuild) Reconcile(request reconcile.Request) (reconcile.Result,
 	target := instance.DeepCopy()
 	targetLog := rlog.ForBuild(target)
 
+	pl, err := platform.GetOrLookupCurrent(ctx, r.client, target.Namespace, target.Status.Platform)
 	if target.Status.Phase == v1alpha1.BuildPhaseNone || target.Status.Phase == v1alpha1.BuildPhaseWaitingForPlatform {
-		pl, err := platform.GetOrLookupCurrent(ctx, r.client, target.Namespace, target.Status.Platform)
 		if err != nil || pl.Status.Phase != v1alpha1.IntegrationPlatformPhaseReady {
 			target.Status.Phase = v1alpha1.BuildPhaseWaitingForPlatform
 		} else {
@@ -174,15 +174,25 @@ func (r *ReconcileBuild) Reconcile(request reconcile.Request) (reconcile.Result,
 		return reconcile.Result{}, err
 	}
 
-	actions := []Action{
-		NewInitializeRoutineAction(),
-		NewInitializePodAction(),
-		NewScheduleRoutineAction(r.reader, r.builder, &r.routines),
-		NewSchedulePodAction(r.reader),
-		NewMonitorRoutineAction(&r.routines),
-		NewMonitorPodAction(),
-		NewErrorRecoveryAction(),
-		NewErrorAction(),
+	var actions []Action
+
+	switch pl.Spec.Build.BuildStrategy {
+	case v1alpha1.IntegrationPlatformBuildStrategyPod:
+		actions = []Action{
+			NewInitializePodAction(),
+			NewSchedulePodAction(r.reader),
+			NewMonitorPodAction(),
+			NewErrorRecoveryAction(),
+			NewErrorAction(),
+		}
+	case v1alpha1.IntegrationPlatformBuildStrategyRoutine:
+		actions = []Action{
+			NewInitializeRoutineAction(),
+			NewScheduleRoutineAction(r.reader, r.builder, &r.routines),
+			NewMonitorRoutineAction(&r.routines),
+			NewErrorRecoveryAction(),
+			NewErrorAction(),
+		}
 	}
 
 	for _, a := range actions {
@@ -219,7 +229,7 @@ func (r *ReconcileBuild) Reconcile(request reconcile.Request) (reconcile.Result,
 		}
 	}
 
-	// Requeue scheduling build so that it re-enters the build working queue
+	// Requeue scheduling (resp. failed) build so that it re-enters the build (resp. recovery) working queue
 	if target.Status.Phase == v1alpha1.BuildPhaseScheduling || target.Status.Phase == v1alpha1.BuildPhaseFailed {
 		return reconcile.Result{
 			RequeueAfter: 5 * time.Second,
diff --git a/pkg/controller/build/initialize_pod.go b/pkg/controller/build/initialize_pod.go
index 4576872..15def69 100644
--- a/pkg/controller/build/initialize_pod.go
+++ b/pkg/controller/build/initialize_pod.go
@@ -20,13 +20,16 @@ package build
 import (
 	"context"
 
-	"github.com/apache/camel-k/pkg/install"
 	"github.com/pkg/errors"
+
 	corev1 "k8s.io/api/core/v1"
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+	"github.com/apache/camel-k/pkg/install"
 )
 
 // NewInitializePodAction creates a new initialize action
@@ -45,8 +48,7 @@ func (action *initializePodAction) Name() string {
 
 // CanHandle tells whether this action can handle the build
 func (action *initializePodAction) CanHandle(build *v1alpha1.Build) bool {
-	return build.Status.Phase == v1alpha1.BuildPhaseInitialization &&
-		build.Spec.Platform.Build.BuildStrategy == v1alpha1.IntegrationPlatformBuildStrategyPod
+	return build.Status.Phase == v1alpha1.BuildPhaseInitialization
 }
 
 // Handle handles the builds
@@ -87,3 +89,40 @@ func (action *initializePodAction) ensureServiceAccount(ctx context.Context, bui
 
 	return err
 }
+
+func deleteBuilderPod(ctx context.Context, client k8sclient.Writer, build *v1alpha1.Build) error {
+	pod := corev1.Pod{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: corev1.SchemeGroupVersion.String(),
+			Kind:       "Pod",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: build.Namespace,
+			Name:      buildPodName(build),
+		},
+	}
+
+	err := client.Delete(ctx, &pod)
+	if err != nil && k8serrors.IsNotFound(err) {
+		return nil
+	}
+
+	return err
+}
+
+func getBuilderPod(ctx context.Context, client k8sclient.Reader, build *v1alpha1.Build) (*corev1.Pod, error) {
+	pod := corev1.Pod{}
+	err := client.Get(ctx, k8sclient.ObjectKey{Namespace: build.Namespace, Name: buildPodName(build)}, &pod)
+	if err != nil && k8serrors.IsNotFound(err) {
+		return nil, nil
+	}
+	if err != nil {
+		return nil, err
+	}
+
+	return &pod, nil
+}
+
+func buildPodName(build *v1alpha1.Build) string {
+	return "camel-k-" + build.Name + "-builder"
+}
diff --git a/pkg/controller/build/initialize_routine.go b/pkg/controller/build/initialize_routine.go
index 0f37b03..df48624 100644
--- a/pkg/controller/build/initialize_routine.go
+++ b/pkg/controller/build/initialize_routine.go
@@ -39,8 +39,7 @@ func (action *initializeRoutineAction) Name() string {
 
 // CanHandle tells whether this action can handle the build
 func (action *initializeRoutineAction) CanHandle(build *v1alpha1.Build) bool {
-	return build.Status.Phase == v1alpha1.BuildPhaseInitialization &&
-		build.Spec.Platform.Build.BuildStrategy == v1alpha1.IntegrationPlatformBuildStrategyRoutine
+	return build.Status.Phase == v1alpha1.BuildPhaseInitialization
 }
 
 // Handle handles the builds
diff --git a/pkg/controller/build/monitor_pod.go b/pkg/controller/build/monitor_pod.go
index 3b7a185..40d7bee 100644
--- a/pkg/controller/build/monitor_pod.go
+++ b/pkg/controller/build/monitor_pod.go
@@ -20,8 +20,10 @@ package build
 import (
 	"context"
 
-	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
 	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
 )
 
 // NewMonitorPodAction creates a new monitor action for scheduled pod
@@ -40,37 +42,51 @@ func (action *monitorPodAction) Name() string {
 
 // CanHandle tells whether this action can handle the build
 func (action *monitorPodAction) CanHandle(build *v1alpha1.Build) bool {
-	return (build.Status.Phase == v1alpha1.BuildPhasePending ||
-		build.Status.Phase == v1alpha1.BuildPhaseRunning) &&
-		build.Spec.Platform.Build.BuildStrategy == v1alpha1.IntegrationPlatformBuildStrategyPod
+	return build.Status.Phase == v1alpha1.BuildPhasePending || build.Status.Phase == v1alpha1.BuildPhaseRunning
 }
 
 // Handle handles the builds
 func (action *monitorPodAction) Handle(ctx context.Context, build *v1alpha1.Build) (*v1alpha1.Build, error) {
-	// Get the build pod
 	pod, err := getBuilderPod(ctx, action.client, build)
 	if err != nil {
 		return nil, err
 	}
-	var buildPhase v1alpha1.BuildPhase
 
 	switch {
 	case pod == nil:
 		build.Status.Phase = v1alpha1.BuildPhaseScheduling
+
+	// Pod remains in pending phase when init containers execute
+	case pod.Status.Phase == corev1.PodPending && action.isPodScheduled(pod),
+		pod.Status.Phase == corev1.PodRunning:
+		build.Status.Phase = v1alpha1.BuildPhaseRunning
+		if build.Status.StartedAt.Time.IsZero() {
+			build.Status.StartedAt = metav1.Now()
+		}
+
 	case pod.Status.Phase == corev1.PodSucceeded:
-		buildPhase = v1alpha1.BuildPhaseSucceeded
-	case pod.Status.Phase == corev1.PodFailed:
-		buildPhase = v1alpha1.BuildPhaseFailed
-	default:
-		buildPhase = build.Status.Phase
-	}
+		build.Status.Phase = v1alpha1.BuildPhaseSucceeded
+		build.Status.Duration = metav1.Now().Sub(build.Status.StartedAt.Time).String()
+		for _, task := range build.Spec.Tasks {
+			if task.Kaniko != nil {
+				build.Status.Image = task.Kaniko.BuiltImage
+				break
+			}
+		}
 
-	if build.Status.Phase == buildPhase {
-		// Status is already up-to-date
-		return nil, nil
+	case pod.Status.Phase == corev1.PodFailed:
+		build.Status.Phase = v1alpha1.BuildPhaseFailed
+		build.Status.Duration = metav1.Now().Sub(build.Status.StartedAt.Time).String()
 	}
 
-	build.Status.Phase = buildPhase
-
 	return build, nil
 }
+
+func (action *monitorPodAction) isPodScheduled(pod *corev1.Pod) bool {
+	for _, condition := range pod.Status.Conditions {
+		if condition.Type == corev1.PodScheduled && condition.Status == corev1.ConditionTrue {
+			return true
+		}
+	}
+	return false
+}
diff --git a/pkg/controller/build/monitor_routine.go b/pkg/controller/build/monitor_routine.go
index 170a575..c1fc7da 100644
--- a/pkg/controller/build/monitor_routine.go
+++ b/pkg/controller/build/monitor_routine.go
@@ -43,9 +43,7 @@ func (action *monitorRoutineAction) Name() string {
 
 // CanHandle tells whether this action can handle the build
 func (action *monitorRoutineAction) CanHandle(build *v1alpha1.Build) bool {
-	return (build.Status.Phase == v1alpha1.BuildPhasePending ||
-		build.Status.Phase == v1alpha1.BuildPhaseRunning) &&
-		build.Spec.Platform.Build.BuildStrategy == v1alpha1.IntegrationPlatformBuildStrategyRoutine
+	return build.Status.Phase == v1alpha1.BuildPhasePending || build.Status.Phase == v1alpha1.BuildPhaseRunning
 }
 
 // Handle handles the builds
diff --git a/pkg/controller/build/schedule_pod.go b/pkg/controller/build/schedule_pod.go
index 7d18aa4..74ff938 100644
--- a/pkg/controller/build/schedule_pod.go
+++ b/pkg/controller/build/schedule_pod.go
@@ -21,20 +21,21 @@ import (
 	"context"
 	"sync"
 
+	"github.com/pkg/errors"
+
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
 	"github.com/apache/camel-k/pkg/platform"
 	"github.com/apache/camel-k/pkg/util/defaults"
-
-	"github.com/pkg/errors"
 )
 
 // NewSchedulePodAction creates a new schedule action
-func NewSchedulePodAction(reader k8sclient.Reader) Action {
+func NewSchedulePodAction(reader client.Reader) Action {
 	return &schedulePodAction{
 		reader: reader,
 	}
@@ -42,8 +43,9 @@ func NewSchedulePodAction(reader k8sclient.Reader) Action {
 
 type schedulePodAction struct {
 	baseAction
-	lock   sync.Mutex
-	reader k8sclient.Reader
+	lock          sync.Mutex
+	reader        client.Reader
+	operatorImage string
 }
 
 // Name returns a common name of the action
@@ -53,8 +55,7 @@ func (action *schedulePodAction) Name() string {
 
 // CanHandle tells whether this action can handle the build
 func (action *schedulePodAction) CanHandle(build *v1alpha1.Build) bool {
-	return build.Status.Phase == v1alpha1.BuildPhaseScheduling &&
-		build.Spec.Platform.Build.BuildStrategy == v1alpha1.IntegrationPlatformBuildStrategyPod
+	return build.Status.Phase == v1alpha1.BuildPhaseScheduling
 }
 
 // Handle handles the builds
@@ -66,7 +67,7 @@ func (action *schedulePodAction) Handle(ctx context.Context, build *v1alpha1.Bui
 	builds := &v1alpha1.BuildList{}
 	// We use the non-caching client as informers cache is not invalidated nor updated
 	// atomically by write operations
-	err := action.reader.List(ctx, builds, k8sclient.InNamespace(build.Namespace))
+	err := action.reader.List(ctx, builds, client.InNamespace(build.Namespace))
 	if err != nil {
 		return nil, err
 	}
@@ -86,15 +87,9 @@ func (action *schedulePodAction) Handle(ctx context.Context, build *v1alpha1.Bui
 	}
 
 	if pod == nil {
-		// Try to get operator image name before starting the build
-		operatorImage, err := platform.GetCurrentOperatorImage(ctx, action.client)
-		if err != nil {
-			return nil, err
-		}
-
 		// We may want to explicitly manage build priority as opposed to relying on
 		// the reconcile loop to handle the queuing
-		pod, err = action.newBuildPod(ctx, build, operatorImage)
+		pod, err = action.newBuildPod(ctx, build)
 		if err != nil {
 			return nil, err
 		}
@@ -114,11 +109,7 @@ func (action *schedulePodAction) Handle(ctx context.Context, build *v1alpha1.Bui
 	return build, nil
 }
 
-func (action *schedulePodAction) newBuildPod(ctx context.Context, build *v1alpha1.Build, operatorImage string) (*corev1.Pod, error) {
-	builderImage := operatorImage
-	if builderImage == "" {
-		builderImage = defaults.ImageName + ":" + defaults.Version
-	}
+func (action *schedulePodAction) newBuildPod(ctx context.Context, build *v1alpha1.Build) (*corev1.Pod, error) {
 	pod := &corev1.Pod{
 		TypeMeta: metav1.TypeMeta{
 			APIVersion: corev1.SchemeGroupVersion.String(),
@@ -126,7 +117,7 @@ func (action *schedulePodAction) newBuildPod(ctx context.Context, build *v1alpha
 		},
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace: build.Namespace,
-			Name:      buildPodName(build.Spec.Meta),
+			Name:      buildPodName(build),
 			Labels: map[string]string{
 				"camel.apache.org/build":     build.Name,
 				"camel.apache.org/component": "builder",
@@ -134,103 +125,74 @@ func (action *schedulePodAction) newBuildPod(ctx context.Context, build *v1alpha
 		},
 		Spec: corev1.PodSpec{
 			ServiceAccountName: "camel-k-builder",
-			Containers: []corev1.Container{
-				{
-					Name:            "builder",
-					Image:           builderImage,
-					ImagePullPolicy: "IfNotPresent",
-					Command: []string{
-						"kamel",
-						"builder",
-						"--namespace",
-						build.Namespace,
-						"--build-name",
-						build.Name,
-					},
-				},
-			},
-			RestartPolicy: corev1.RestartPolicyNever,
+			RestartPolicy:      corev1.RestartPolicyNever,
 		},
 	}
 
-	if build.Spec.Platform.Build.PublishStrategy == v1alpha1.IntegrationPlatformBuildPublishStrategyKaniko {
-		// Mount persistent volume used to coordinate build output with Kaniko cache and image build input
-		pod.Spec.Volumes = []corev1.Volume{
-			{
-				Name: "camel-k-builder",
-				VolumeSource: corev1.VolumeSource{
-					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
-						ClaimName: build.Spec.Platform.Build.PersistentVolumeClaim,
-					},
-				},
-			},
-		}
-		pod.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{
-			{
-				Name:      "camel-k-builder",
-				MountPath: build.Spec.BuildDir,
-			},
-		}
-
-		// In case the kaniko cache has not run, the /workspace dir needs to have the right permissions set
-		pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{
-			Name:            "prepare-kaniko-workspace",
-			Image:           "busybox",
-			ImagePullPolicy: corev1.PullIfNotPresent,
-			Command:         []string{"/bin/sh", "-c"},
-			Args:            []string{"chmod -R a+rwx /workspace"},
-			VolumeMounts: []corev1.VolumeMount{
-				{
-					Name:      "camel-k-builder",
-					MountPath: "/workspace",
-				},
-			},
-		})
-
-		// Use affinity when Kaniko cache warming is enabled
-		if build.Spec.Platform.Build.IsKanikoCacheEnabled() {
-			// Co-locate with the Kaniko warmer pod for sharing the host path volume as the current
-			// persistent volume claim uses the default storage class which is likely relying
-			// on the host path provisioner.
-			// This has to be done manually by retrieving the Kaniko warmer pod node name and using
-			// node affinity as pod affinity only works for running pods and the Kaniko warmer pod
-			// has already completed at that stage.
-
-			// Locate the kaniko warmer pod
-			pods := &corev1.PodList{}
-			err := action.client.List(ctx, pods,
-				k8sclient.InNamespace(build.Namespace),
-				k8sclient.MatchingLabels{
-					"camel.apache.org/component": "kaniko-warmer",
-				})
+	for _, task := range build.Spec.Tasks {
+		if task.Builder != nil {
+			// TODO: Move the retrieval of the operator image into the controller
+			operatorImage, err := platform.GetCurrentOperatorImage(ctx, action.client)
 			if err != nil {
 				return nil, err
 			}
-
-			if len(pods.Items) != 1 {
-				return nil, errors.New("failed to locate the Kaniko cache warmer pod")
-			}
-
-			// Use node affinity with the Kaniko warmer pod node name
-			pod.Spec.Affinity = &corev1.Affinity{
-				NodeAffinity: &corev1.NodeAffinity{
-					RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
-						NodeSelectorTerms: []corev1.NodeSelectorTerm{
-							{
-								MatchExpressions: []corev1.NodeSelectorRequirement{
-									{
-										Key:      "kubernetes.io/hostname",
-										Operator: "In",
-										Values:   []string{pods.Items[0].Spec.NodeName},
-									},
-								},
-							},
-						},
-					},
-				},
+			if operatorImage == "" {
+				action.operatorImage = defaults.ImageName + ":" + defaults.Version
+			} else {
+				action.operatorImage = operatorImage
 			}
+			action.addCamelTaskToPod(build, task.Builder, pod)
+		} else if task.Kaniko != nil {
+			action.addKanikoTaskToPod(task.Kaniko, pod)
 		}
 	}
 
+	// Make sure there is one container defined
+	pod.Spec.Containers = pod.Spec.InitContainers[len(pod.Spec.InitContainers)-1 : len(pod.Spec.InitContainers)]
+	pod.Spec.InitContainers = pod.Spec.InitContainers[:len(pod.Spec.InitContainers)-1]
+
 	return pod, nil
 }
+
+func (action *schedulePodAction) addCamelTaskToPod(build *v1alpha1.Build, task *v1alpha1.BuilderTask, pod *corev1.Pod) {
+	pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{
+		Name:            task.Name,
+		Image:           action.operatorImage,
+		ImagePullPolicy: "IfNotPresent",
+		Command: []string{
+			"kamel",
+			"builder",
+			"--namespace",
+			pod.Namespace,
+			"--build-name",
+			build.Name,
+			"--task-name",
+			task.Name,
+		},
+		VolumeMounts: task.VolumeMounts,
+	})
+
+	action.addBaseTaskToPod(&task.BaseTask, pod)
+}
+
+func (action *schedulePodAction) addKanikoTaskToPod(task *v1alpha1.KanikoTask, pod *corev1.Pod) {
+	pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{
+		Name:            task.Name,
+		Image:           task.Image,
+		ImagePullPolicy: "IfNotPresent",
+		Args:            task.Args,
+		Env:             task.Env,
+		VolumeMounts:    task.VolumeMounts,
+	})
+
+	action.addBaseTaskToPod(&task.BaseTask, pod)
+}
+
+func (action *schedulePodAction) addBaseTaskToPod(task *v1alpha1.BaseTask, pod *corev1.Pod) {
+	pod.Spec.Volumes = append(pod.Spec.Volumes, task.Volumes...)
+
+	if task.Affinity != nil {
+		// We may want to handle possible conflicts
+		pod.Spec.Affinity = task.Affinity
+	}
+}
diff --git a/pkg/controller/build/schedule_routine.go b/pkg/controller/build/schedule_routine.go
index aa591a0..6535687 100644
--- a/pkg/controller/build/schedule_routine.go
+++ b/pkg/controller/build/schedule_routine.go
@@ -19,8 +19,11 @@ package build
 
 import (
 	"context"
+	"fmt"
 	"sync"
 
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
@@ -51,8 +54,7 @@ func (action *scheduleRoutineAction) Name() string {
 
 // CanHandle tells whether this action can handle the build
 func (action *scheduleRoutineAction) CanHandle(build *v1alpha1.Build) bool {
-	return build.Status.Phase == v1alpha1.BuildPhaseScheduling &&
-		build.Spec.Platform.Build.BuildStrategy == v1alpha1.IntegrationPlatformBuildStrategyRoutine
+	return build.Status.Phase == v1alpha1.BuildPhaseScheduling
 }
 
 // Handle handles the builds
@@ -88,21 +90,37 @@ func (action *scheduleRoutineAction) Handle(ctx context.Context, build *v1alpha1
 		return nil, err
 	}
 
-	// Start the build
-	progress := action.builder.Build(build.Spec)
-	// And follow the build progress asynchronously to avoid blocking the reconcile loop
+	// Start the build asynchronously to avoid blocking the reconcile loop
 	go func() {
-		for status := range progress {
-			target := build.DeepCopy()
-			target.Status = status
-			// Copy the failure field from the build to persist recovery state
-			target.Status.Failure = build.Status.Failure
-			// Patch the build status with the current progress
-			err := action.client.Status().Patch(ctx, target, client.MergeFrom(build))
-			if err != nil {
-				action.L.Errorf(err, "Error while updating build status: %s", build.Name)
+		defer action.routines.Delete(build.Name)
+
+		status := v1alpha1.BuildStatus{
+			Phase:     v1alpha1.BuildPhaseRunning,
+			StartedAt: metav1.Now(),
+		}
+		if err := action.updateBuildStatus(ctx, build, status); err != nil {
+			return
+		}
+
+		for i, task := range build.Spec.Tasks {
+			if task.Builder == nil {
+				status := v1alpha1.BuildStatus{
+					// Error the build directly as we know recovery won't work over ill-defined tasks
+					Phase: v1alpha1.BuildPhaseError,
+					Error: fmt.Sprintf("task cannot be executed using the routine strategy: %s", task.GetName()),
+				}
+				if err := action.updateBuildStatus(ctx, build, status); err != nil {
+					break
+				}
+			} else {
+				status := action.builder.Run(*task.Builder)
+				if i == len(build.Spec.Tasks)-1 {
+					status.Duration = metav1.Now().Sub(build.Status.StartedAt.Time).String()
+				}
+				if err := action.updateBuildStatus(ctx, build, status); err != nil {
+					break
+				}
 			}
-			build.Status = target.Status
 		}
 	}()
 
@@ -110,3 +128,18 @@ func (action *scheduleRoutineAction) Handle(ctx context.Context, build *v1alpha1
 
 	return nil, nil
 }
+
+func (action *scheduleRoutineAction) updateBuildStatus(ctx context.Context, build *v1alpha1.Build, status v1alpha1.BuildStatus) error {
+	target := build.DeepCopy()
+	target.Status = status
+	// Copy the failure field from the build to persist recovery state
+	target.Status.Failure = build.Status.Failure
+	// Patch the build status with the current progress
+	err := action.client.Status().Patch(ctx, target, client.MergeFrom(build))
+	if err != nil {
+		action.L.Errorf(err, "Cannot update build status: %s", build.Name)
+		return err
+	}
+	build.Status = target.Status
+	return nil
+}
diff --git a/pkg/controller/build/util_pod.go b/pkg/controller/build/util_pod.go
deleted file mode 100644
index 6189e38..0000000
--- a/pkg/controller/build/util_pod.go
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
-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 build
-
-import (
-	"context"
-
-	corev1 "k8s.io/api/core/v1"
-	k8serrors "k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
-
-	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
-)
-
-// deleteBuilderPod --
-func deleteBuilderPod(ctx context.Context, client k8sclient.Writer, build *v1alpha1.Build) error {
-	pod := corev1.Pod{
-		TypeMeta: metav1.TypeMeta{
-			APIVersion: corev1.SchemeGroupVersion.String(),
-			Kind:       "Pod",
-		},
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: build.Namespace,
-			Name:      buildPodName(build.Spec.Meta),
-			Labels: map[string]string{
-				"camel.apache.org/build": build.Name,
-			},
-		},
-	}
-
-	err := client.Delete(ctx, &pod)
-	if err != nil && k8serrors.IsNotFound(err) {
-		return nil
-	}
-
-	return err
-}
-
-// getBuilderPod --
-func getBuilderPod(ctx context.Context, client k8sclient.Reader, build *v1alpha1.Build) (*corev1.Pod, error) {
-	key := k8sclient.ObjectKey{Namespace: build.Namespace, Name: buildPodName(build.ObjectMeta)}
-	pod := corev1.Pod{}
-
-	err := client.Get(ctx, key, &pod)
-	if err != nil && k8serrors.IsNotFound(err) {
-		return nil, nil
-	}
-	if err != nil {
-		return nil, err
-	}
-
-	return &pod, nil
-}
-
-func buildPodName(object metav1.ObjectMeta) string {
-	return "camel-k-" + object.Name + "-builder"
-}
diff --git a/pkg/controller/integrationkit/build.go b/pkg/controller/integrationkit/build.go
index ce43068..f234343 100644
--- a/pkg/controller/integrationkit/build.go
+++ b/pkg/controller/integrationkit/build.go
@@ -21,17 +21,16 @@ import (
 	"context"
 	"fmt"
 
-	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
-	"github.com/apache/camel-k/pkg/builder"
-	"github.com/apache/camel-k/pkg/trait"
-	"github.com/apache/camel-k/pkg/util/kubernetes"
+	"github.com/pkg/errors"
 
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 
-	"github.com/pkg/errors"
+	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+	"github.com/apache/camel-k/pkg/trait"
+	"github.com/apache/camel-k/pkg/util/kubernetes"
 )
 
 // NewBuildAction creates a new build request handling action for the kit
@@ -84,23 +83,15 @@ func (action *buildAction) handleBuildSubmitted(ctx context.Context, kit *v1alph
 
 		build = &v1alpha1.Build{
 			TypeMeta: metav1.TypeMeta{
-				APIVersion: "camel.apache.org/v1alpha1",
-				Kind:       "Build",
+				APIVersion: v1alpha1.SchemeGroupVersion.String(),
+				Kind:       v1alpha1.BuildKind,
 			},
 			ObjectMeta: metav1.ObjectMeta{
 				Namespace: kit.Namespace,
 				Name:      kit.Name,
 			},
 			Spec: v1alpha1.BuildSpec{
-				Meta:            kit.ObjectMeta,
-				CamelVersion:    env.CamelCatalog.Version,
-				RuntimeVersion:  env.CamelCatalog.RuntimeVersion,
-				RuntimeProvider: env.CamelCatalog.RuntimeProvider,
-				Platform:        env.Platform.Status.IntegrationPlatformSpec,
-				Dependencies:    kit.Spec.Dependencies,
-				// TODO: sort for easy read
-				Steps:    builder.StepIDsFor(env.Steps...),
-				BuildDir: env.BuildDir,
+				Tasks: env.BuildTasks,
 			},
 		}
 
@@ -142,7 +133,7 @@ func (action *buildAction) handleBuildRunning(ctx context.Context, kit *v1alpha1
 		// if not there is a chance that the kit has been modified by the user
 		if kit.Status.Phase != v1alpha1.IntegrationKitPhaseBuildRunning {
 			return nil, fmt.Errorf("found kit %s not in the expected phase (expectd=%s, found=%s)",
-				build.Spec.Meta.Name,
+				kit.Name,
 				string(v1alpha1.IntegrationKitPhaseBuildRunning),
 				string(kit.Status.Phase),
 			)
@@ -168,7 +159,7 @@ func (action *buildAction) handleBuildRunning(ctx context.Context, kit *v1alpha1
 		// if not there is a chance that the kit has been modified by the user
 		if kit.Status.Phase != v1alpha1.IntegrationKitPhaseBuildRunning {
 			return nil, fmt.Errorf("found kit %s not the an expected phase (expectd=%s, found=%s)",
-				build.Spec.Meta.Name,
+				kit.Name,
 				string(v1alpha1.IntegrationKitPhaseBuildRunning),
 				string(kit.Status.Phase),
 			)
diff --git a/pkg/controller/integrationplatform/initialize.go b/pkg/controller/integrationplatform/initialize.go
index ce90c2e..dd50ef2 100644
--- a/pkg/controller/integrationplatform/initialize.go
+++ b/pkg/controller/integrationplatform/initialize.go
@@ -70,15 +70,13 @@ func (action *initializeAction) Handle(ctx context.Context, platform *v1alpha1.I
 	}
 
 	if platform.Status.Build.PublishStrategy == v1alpha1.IntegrationPlatformBuildPublishStrategyKaniko {
-		// Create the persistent volume claim used to coordinate build pod output
-		// with Kaniko cache and build input
-		action.L.Info("Create persistent volume claim")
-		err := createPersistentVolumeClaim(ctx, action.client, platform)
-		if err != nil {
-			return nil, err
-		}
-
 		if platform.Status.Build.IsKanikoCacheEnabled() {
+			// Create the persistent volume claim used by the Kaniko cache
+			action.L.Info("Create persistent volume claim")
+			err := createPersistentVolumeClaim(ctx, action.client, platform)
+			if err != nil {
+				return nil, err
+			}
 			// Create the Kaniko warmer pod that caches the base image into the Camel K builder volume
 			action.L.Info("Create Kaniko cache warmer pod")
 			err = createKanikoCacheWarmerPod(ctx, action.client, platform)
diff --git a/pkg/controller/integrationplatform/kaniko_cache.go b/pkg/controller/integrationplatform/kaniko_cache.go
index 7ff25f6..2d5674d 100644
--- a/pkg/controller/integrationplatform/kaniko_cache.go
+++ b/pkg/controller/integrationplatform/kaniko_cache.go
@@ -21,6 +21,8 @@ import (
 	"context"
 	"fmt"
 
+	"github.com/pkg/errors"
+
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -29,8 +31,6 @@ import (
 	"github.com/apache/camel-k/pkg/builder/kaniko"
 	"github.com/apache/camel-k/pkg/client"
 	"github.com/apache/camel-k/pkg/util/defaults"
-
-	"github.com/pkg/errors"
 )
 
 func createKanikoCacheWarmerPod(ctx context.Context, client client.Client, platform *v1alpha1.IntegrationPlatform) error {
@@ -80,7 +80,7 @@ func createKanikoCacheWarmerPod(ctx context.Context, client client.Client, platf
 					VolumeMounts: []corev1.VolumeMount{
 						{
 							Name:      "camel-k-builder",
-							MountPath: "/workspace",
+							MountPath: kaniko.BuildDir,
 						},
 					},
 				},
diff --git a/pkg/trait/builder.go b/pkg/trait/builder.go
index 096fc07..3001ca2 100644
--- a/pkg/trait/builder.go
+++ b/pkg/trait/builder.go
@@ -18,12 +18,23 @@ limitations under the License.
 package trait
 
 import (
+	"fmt"
+	"path"
+	"strconv"
+
+	"github.com/pkg/errors"
+
+	corev1 "k8s.io/api/core/v1"
+
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
 	"github.com/apache/camel-k/pkg/builder"
 	"github.com/apache/camel-k/pkg/builder/kaniko"
 	"github.com/apache/camel-k/pkg/builder/runtime"
 	"github.com/apache/camel-k/pkg/builder/s2i"
 	"github.com/apache/camel-k/pkg/platform"
+	"github.com/apache/camel-k/pkg/util/defaults"
 )
 
 // The builder trait is internally used to determine the best strategy to
@@ -49,22 +60,80 @@ func (t *builderTrait) Configure(e *Environment) (bool, error) {
 }
 
 func (t *builderTrait) Apply(e *Environment) error {
-	e.Steps = append(e.Steps, builder.DefaultSteps...)
-	if platform.SupportsS2iPublishStrategy(e.Platform) {
-		e.Steps = append(e.Steps, s2i.S2iSteps...)
-	} else if platform.SupportsKanikoPublishStrategy(e.Platform) {
-		e.Steps = append(e.Steps, kaniko.KanikoSteps...)
-		e.BuildDir = kaniko.BuildDir
-	}
+	camelTask := t.camelTask(e)
+	e.BuildTasks = append(e.BuildTasks, v1alpha1.Task{Builder: camelTask})
 
-	quarkus := e.Catalog.GetTrait("quarkus").(*quarkusTrait)
+	if platform.SupportsKanikoPublishStrategy(e.Platform) {
+		kanikoTask, err := t.kanikoTask(e)
+		if err != nil {
+			return err
+		}
+		mount := corev1.VolumeMount{Name: "camel-k-builder", MountPath: kaniko.BuildDir}
+		camelTask.VolumeMounts = append(camelTask.VolumeMounts, mount)
+		kanikoTask.VolumeMounts = append(kanikoTask.VolumeMounts, mount)
 
-	if quarkus.isEnabled() {
-		// Add build steps for Quarkus runtime
-		quarkus.addBuildSteps(e)
-	} else {
-		// Add build steps for default runtime
-		e.Steps = append(e.Steps, runtime.MainSteps...)
+		if e.Platform.Status.Build.IsKanikoCacheEnabled() {
+			// Co-locate with the Kaniko warmer pod for sharing the host path volume as the current
+			// persistent volume claim uses the default storage class which is likely relying
+			// on the host path provisioner.
+			// This has to be done manually by retrieving the Kaniko warmer pod node name and using
+			// node affinity as pod affinity only works for running pods and the Kaniko warmer pod
+			// has already completed at that stage.
+
+			// Locate the kaniko warmer pod
+			pods := &corev1.PodList{}
+			err := e.Client.List(e.C, pods,
+				client.InNamespace(e.Platform.Namespace),
+				client.MatchingLabels{
+					"camel.apache.org/component": "kaniko-warmer",
+				})
+			if err != nil {
+				return err
+			}
+
+			if len(pods.Items) != 1 {
+				return errors.New("failed to locate the Kaniko cache warmer pod")
+			}
+
+			// Use node affinity with the Kaniko warmer pod node name
+			kanikoTask.Affinity = &corev1.Affinity{
+				NodeAffinity: &corev1.NodeAffinity{
+					RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
+						NodeSelectorTerms: []corev1.NodeSelectorTerm{
+							{
+								MatchExpressions: []corev1.NodeSelectorRequirement{
+									{
+										Key:      "kubernetes.io/hostname",
+										Operator: "In",
+										Values:   []string{pods.Items[0].Spec.NodeName},
+									},
+								},
+							},
+						},
+					},
+				},
+			}
+			// Use the PVC used to warm the Kaniko cache to coordinate the Camel Maven build and the Kaniko image build
+			camelTask.Volumes = append(camelTask.Volumes, corev1.Volume{
+				Name: "camel-k-builder",
+				VolumeSource: corev1.VolumeSource{
+					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+						ClaimName: e.Platform.Spec.Build.PersistentVolumeClaim,
+					},
+				},
+			})
+		} else {
+			// Use an emptyDir volume to coordinate the Camel Maven build and the Kaniko image build
+			camelTask.Volumes = append(camelTask.Volumes, corev1.Volume{
+				Name: "camel-k-builder",
+				VolumeSource: corev1.VolumeSource{
+					EmptyDir: &corev1.EmptyDirVolumeSource{
+					},
+				},
+			})
+		}
+
+		e.BuildTasks = append(e.BuildTasks, v1alpha1.Task{Kaniko: kanikoTask})
 	}
 
 	return nil
@@ -79,3 +148,201 @@ func (t *builderTrait) IsPlatformTrait() bool {
 func (t *builderTrait) InfluencesKit() bool {
 	return true
 }
+
+func (t *builderTrait) camelTask(e *Environment) *v1alpha1.BuilderTask {
+	task := &v1alpha1.BuilderTask{
+		BaseTask: v1alpha1.BaseTask{
+			Name: "camel",
+		},
+		Meta:            e.IntegrationKit.ObjectMeta,
+		BaseImage:       e.Platform.Status.Build.BaseImage,
+		CamelVersion:    e.CamelCatalog.Version,
+		RuntimeVersion:  e.CamelCatalog.RuntimeVersion,
+		RuntimeProvider: e.CamelCatalog.RuntimeProvider,
+		//Sources:		 e.Integration.Spec.Sources,
+		//Resources:     e.Integration.Spec.Resources,
+		Dependencies: e.IntegrationKit.Spec.Dependencies,
+		//TODO: sort steps for easier read
+		Steps:      builder.StepIDsFor(builder.DefaultSteps...),
+		Properties: e.Platform.Status.Build.Properties,
+		Timeout:    e.Platform.Status.Build.GetTimeout(),
+		Maven:      e.Platform.Status.Build.Maven,
+	}
+
+	if platform.SupportsS2iPublishStrategy(e.Platform) {
+		task.Steps = append(task.Steps, builder.StepIDsFor(s2i.S2iSteps...)...)
+	} else if platform.SupportsKanikoPublishStrategy(e.Platform) {
+		task.Steps = append(task.Steps, builder.StepIDsFor(kaniko.KanikoSteps...)...)
+		task.BuildDir = kaniko.BuildDir
+	}
+
+	quarkus := e.Catalog.GetTrait("quarkus").(*quarkusTrait)
+	if quarkus.isEnabled() {
+		// Add build steps for Quarkus runtime
+		quarkus.addBuildSteps(task)
+	} else {
+		// Add build steps for default runtime
+		task.Steps = append(task.Steps, builder.StepIDsFor(runtime.MainSteps...)...)
+	}
+
+	return task
+}
+
+func (t *builderTrait) kanikoTask(e *Environment) (*v1alpha1.KanikoTask, error) {
+	organization := e.Platform.Status.Build.Registry.Organization
+	if organization == "" {
+		organization = e.Platform.Namespace
+	}
+	image := e.Platform.Status.Build.Registry.Address + "/" + organization + "/camel-k-" + e.IntegrationKit.Name + ":" + e.IntegrationKit.ResourceVersion
+
+	env := make([]corev1.EnvVar, 0)
+	baseArgs := []string{
+		"--dockerfile=Dockerfile",
+		"--context=" + path.Join(kaniko.BuildDir, "package", "context"),
+		"--destination=" + image,
+		"--cache=" + strconv.FormatBool(e.Platform.Status.Build.IsKanikoCacheEnabled()),
+		"--cache-dir=" + path.Join(kaniko.BuildDir, "cache"),
+	}
+
+	args := make([]string, 0, len(baseArgs))
+	args = append(args, baseArgs...)
+
+	if e.Platform.Status.Build.Registry.Insecure {
+		args = append(args, "--insecure")
+		args = append(args, "--insecure-pull")
+	}
+
+	volumes := make([]corev1.Volume, 0)
+	volumeMounts := make([]corev1.VolumeMount, 0)
+
+	if e.Platform.Status.Build.Registry.Secret != "" {
+		secretKind, err := getSecretKind(e)
+		if err != nil {
+			return nil, err
+		}
+
+		volumes = append(volumes, corev1.Volume{
+			Name: "kaniko-secret",
+			VolumeSource: corev1.VolumeSource{
+				Secret: &corev1.SecretVolumeSource{
+					SecretName: e.Platform.Status.Build.Registry.Secret,
+					Items: []corev1.KeyToPath{
+						{
+							Key:  secretKind.fileName,
+							Path: secretKind.destination,
+						},
+					},
+				},
+			},
+		})
+
+		volumeMounts = append(volumeMounts, corev1.VolumeMount{
+			Name:      "kaniko-secret",
+			MountPath: secretKind.mountPath,
+		})
+
+		if secretKind.refEnv != "" {
+			env = append(env, corev1.EnvVar{
+				Name:  secretKind.refEnv,
+				Value: path.Join(secretKind.mountPath, secretKind.destination),
+			})
+		}
+		args = baseArgs
+	}
+
+	if e.Platform.Status.Build.HTTPProxySecret != "" {
+		optional := true
+		env = append(env, corev1.EnvVar{
+			Name: "HTTP_PROXY",
+			ValueFrom: &corev1.EnvVarSource{
+				SecretKeyRef: &corev1.SecretKeySelector{
+					LocalObjectReference: corev1.LocalObjectReference{
+						Name: e.Platform.Status.Build.HTTPProxySecret,
+					},
+					Key:      "HTTP_PROXY",
+					Optional: &optional,
+				},
+			},
+		})
+		env = append(env, corev1.EnvVar{
+			Name: "HTTPS_PROXY",
+			ValueFrom: &corev1.EnvVarSource{
+				SecretKeyRef: &corev1.SecretKeySelector{
+					LocalObjectReference: corev1.LocalObjectReference{
+						Name: e.Platform.Status.Build.HTTPProxySecret,
+					},
+					Key:      "HTTPS_PROXY",
+					Optional: &optional,
+				},
+			},
+		})
+		env = append(env, corev1.EnvVar{
+			Name: "NO_PROXY",
+			ValueFrom: &corev1.EnvVarSource{
+				SecretKeyRef: &corev1.SecretKeySelector{
+					LocalObjectReference: corev1.LocalObjectReference{
+						Name: e.Platform.Status.Build.HTTPProxySecret,
+					},
+					Key:      "NO_PROXY",
+					Optional: &optional,
+				},
+			},
+		})
+	}
+
+	return &v1alpha1.KanikoTask{
+		ImageTask: v1alpha1.ImageTask{
+			BaseTask: v1alpha1.BaseTask{
+				Name:         "kaniko",
+				Volumes:      volumes,
+				VolumeMounts: volumeMounts,
+			},
+			Image: fmt.Sprintf("gcr.io/kaniko-project/executor:v%s", defaults.KanikoVersion),
+			Args:  args,
+			Env:   env,
+		},
+		BuiltImage: image,
+	}, nil
+}
+
+type secretKind struct {
+	fileName    string
+	mountPath   string
+	destination string
+	refEnv      string
+}
+
+var (
+	secretKindGCR = secretKind{
+		fileName:    "kaniko-secret.json",
+		mountPath:   "/secret",
+		destination: "kaniko-secret.json",
+		refEnv:      "GOOGLE_APPLICATION_CREDENTIALS",
+	}
+	secretKindPlainDocker = secretKind{
+		fileName:    "config.json",
+		mountPath:   "/kaniko/.docker",
+		destination: "config.json",
+	}
+	secretKindStandardDocker = secretKind{
+		fileName:    corev1.DockerConfigJsonKey,
+		mountPath:   "/kaniko/.docker",
+		destination: "config.json",
+	}
+
+	allSecretKinds = []secretKind{secretKindGCR, secretKindPlainDocker, secretKindStandardDocker}
+)
+
+func getSecretKind(e *Environment) (secretKind, error) {
+	secret := corev1.Secret{}
+	err := e.Client.Get(e.C, client.ObjectKey{Namespace: e.Platform.Namespace, Name: e.Platform.Status.Build.Registry.Secret}, &secret)
+	if err != nil {
+		return secretKind{}, err
+	}
+	for _, k := range allSecretKinds {
+		if _, ok := secret.Data[k.fileName]; ok {
+			return k, nil
+		}
+	}
+	return secretKind{}, errors.New("unsupported secret type for registry authentication")
+}
diff --git a/pkg/trait/builder_test.go b/pkg/trait/builder_test.go
index d2ec6c3..ebe6f9f 100644
--- a/pkg/trait/builder_test.go
+++ b/pkg/trait/builder_test.go
@@ -28,7 +28,6 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
-	"github.com/apache/camel-k/pkg/builder"
 	"github.com/apache/camel-k/pkg/builder/kaniko"
 	"github.com/apache/camel-k/pkg/builder/s2i"
 	"github.com/apache/camel-k/pkg/util/camel"
@@ -52,7 +51,7 @@ func TestBuilderTraitNotAppliedBecauseOfNilKit(t *testing.T) {
 			assert.Nil(t, err)
 			assert.NotEmpty(t, e.ExecutedTraits)
 			assert.Nil(t, e.GetTrait("builder"))
-			assert.Empty(t, e.Steps)
+			assert.Empty(t, e.BuildTasks)
 		})
 	}
 }
@@ -73,7 +72,7 @@ func TestBuilderTraitNotAppliedBecauseOfNilPhase(t *testing.T) {
 			assert.Nil(t, err)
 			assert.NotEmpty(t, e.ExecutedTraits)
 			assert.Nil(t, e.GetTrait("builder"))
-			assert.Empty(t, e.Steps)
+			assert.Empty(t, e.BuildTasks)
 		})
 	}
 }
@@ -85,11 +84,12 @@ func TestS2IBuilderTrait(t *testing.T) {
 	assert.Nil(t, err)
 	assert.NotEmpty(t, env.ExecutedTraits)
 	assert.NotNil(t, env.GetTrait("builder"))
-	assert.NotEmpty(t, env.Steps)
-	assert.Len(t, env.Steps, 8)
+	assert.NotEmpty(t, env.BuildTasks)
+	assert.Len(t, env.BuildTasks, 1)
+	assert.NotNil(t, env.BuildTasks[0].Builder)
 	assert.Condition(t, func() bool {
-		for _, s := range env.Steps {
-			if s == s2i.Steps.Publisher && s.Phase() == builder.ApplicationPublishPhase {
+		for _, s := range env.BuildTasks[0].Builder.Steps {
+			if s == s2i.Steps.Publisher.ID() {
 				return true
 			}
 		}
@@ -105,17 +105,19 @@ func TestKanikoBuilderTrait(t *testing.T) {
 	assert.Nil(t, err)
 	assert.NotEmpty(t, env.ExecutedTraits)
 	assert.NotNil(t, env.GetTrait("builder"))
-	assert.NotEmpty(t, env.Steps)
-	assert.Len(t, env.Steps, 8)
+	assert.NotEmpty(t, env.BuildTasks)
+	assert.Len(t, env.BuildTasks, 2)
+	assert.NotNil(t, env.BuildTasks[0].Builder)
 	assert.Condition(t, func() bool {
-		for _, s := range env.Steps {
-			if s == kaniko.Steps.Publisher && s.Phase() == builder.ApplicationPublishPhase {
+		for _, s := range env.BuildTasks[0].Builder.Steps {
+			if s == kaniko.Steps.Publisher.ID() {
 				return true
 			}
 		}
 
 		return false
 	})
+	assert.NotNil(t, env.BuildTasks[1].Kaniko)
 }
 
 func createBuilderTestEnv(cluster v1alpha1.IntegrationPlatformCluster, strategy v1alpha1.IntegrationPlatformBuildPublishStrategy) *Environment {
@@ -124,6 +126,7 @@ func createBuilderTestEnv(cluster v1alpha1.IntegrationPlatformCluster, strategy
 		panic(err)
 	}
 
+	kanikoCache := false
 	res := &Environment{
 		C:            context.TODO(),
 		CamelCatalog: c,
@@ -146,9 +149,10 @@ func createBuilderTestEnv(cluster v1alpha1.IntegrationPlatformCluster, strategy
 			Spec: v1alpha1.IntegrationPlatformSpec{
 				Cluster: cluster,
 				Build: v1alpha1.IntegrationPlatformBuildSpec{
-					PublishStrategy: strategy,
-					Registry:        v1alpha1.IntegrationPlatformRegistrySpec{Address: "registry"},
-					CamelVersion:    defaults.DefaultCamelVersion,
+					PublishStrategy:  strategy,
+					Registry:         v1alpha1.IntegrationPlatformRegistrySpec{Address: "registry"},
+					CamelVersion:     defaults.DefaultCamelVersion,
+					KanikoBuildCache: &kanikoCache,
 				},
 			},
 		},
diff --git a/pkg/trait/deployer.go b/pkg/trait/deployer.go
index 6ba76a1..33d288a 100644
--- a/pkg/trait/deployer.go
+++ b/pkg/trait/deployer.go
@@ -18,20 +18,15 @@ limitations under the License.
 package trait
 
 import (
-	"reflect"
-
 	"github.com/pkg/errors"
 
-	jsonpatch "github.com/evanphx/json-patch"
-
-	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/json"
 
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
 	"github.com/apache/camel-k/pkg/util/kubernetes"
+	"github.com/apache/camel-k/pkg/util/patch"
 )
 
 // The deployer trait can be used to explicitly select the kind of high level resource that
@@ -87,15 +82,15 @@ func (t *deployerTrait) Apply(e *Environment) error {
 					return err
 				}
 
-				patch, err := positiveMergePatch(object, resource)
+				p, err := patch.PositiveMergePatch(object, resource)
 				if err != nil {
 					return err
-				} else if len(patch) == 0 {
+				} else if len(p) == 0 {
 					// Avoid triggering a patch request for nothing
 					continue
 				}
 
-				err = env.Client.Patch(env.C, resource, client.ConstantPatch(types.MergePatchType, patch))
+				err = env.Client.Patch(env.C, resource, client.ConstantPatch(types.MergePatchType, p))
 				if err != nil {
 					return errors.Wrap(err, "error during patch resource")
 				}
@@ -116,67 +111,3 @@ func (t *deployerTrait) IsPlatformTrait() bool {
 func (t *deployerTrait) RequiresIntegrationPlatform() bool {
 	return false
 }
-
-func positiveMergePatch(source runtime.Object, target runtime.Object) ([]byte, error) {
-	sourceJSON, err := json.Marshal(source)
-	if err != nil {
-		return nil, err
-	}
-
-	targetJSON, err := json.Marshal(target)
-	if err != nil {
-		return nil, err
-	}
-
-	mergePatch, err := jsonpatch.CreateMergePatch(sourceJSON, targetJSON)
-	if err != nil {
-		return nil, err
-	}
-
-	var positivePatch map[string]interface{}
-	err = json.Unmarshal(mergePatch, &positivePatch)
-	if err != nil {
-		return nil, err
-	}
-
-	// The following is a work-around to remove null fields from the JSON merge patch,
-	// so that values defaulted by controllers server-side are not deleted.
-	// It's generally acceptable as these values are orthogonal to the values managed
-	// by the traits.
-	removeNilValues(reflect.ValueOf(positivePatch), reflect.Value{})
-
-	// Return an empty patch if no keys remain
-	if len(positivePatch) == 0 {
-		return make([]byte, 0), nil
-	}
-
-	return json.Marshal(positivePatch)
-}
-
-func removeNilValues(v reflect.Value, parent reflect.Value) {
-	for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
-		v = v.Elem()
-	}
-	switch v.Kind() {
-	case reflect.Array, reflect.Slice:
-		for i := 0; i < v.Len(); i++ {
-			removeNilValues(v.Index(i), v)
-		}
-	case reflect.Map:
-		for _, k := range v.MapKeys() {
-			switch c := v.MapIndex(k); {
-			case !c.IsValid():
-				// Skip keys previously deleted
-				continue
-			case c.IsNil(), c.Elem().Kind() == reflect.Map && len(c.Elem().MapKeys()) == 0:
-				v.SetMapIndex(k, reflect.Value{})
-			default:
-				removeNilValues(c, v)
-			}
-		}
-		// Back process the parent map in case it has been emptied so that it's deleted as well
-		if len(v.MapKeys()) == 0 && parent.Kind() == reflect.Map {
-			removeNilValues(parent, reflect.Value{})
-		}
-	}
-}
diff --git a/pkg/trait/quarkus.go b/pkg/trait/quarkus.go
index 97d7395..3b75eed 100644
--- a/pkg/trait/quarkus.go
+++ b/pkg/trait/quarkus.go
@@ -26,6 +26,7 @@ import (
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+	"github.com/apache/camel-k/pkg/builder"
 	"github.com/apache/camel-k/pkg/builder/runtime"
 	"github.com/apache/camel-k/pkg/metadata"
 	"github.com/apache/camel-k/pkg/util"
@@ -142,11 +143,11 @@ func (t *quarkusTrait) loadOrCreateCatalog(e *Environment, camelVersion string,
 	return nil
 }
 
-func (t *quarkusTrait) addBuildSteps(e *Environment) {
-	e.Steps = append(e.Steps, runtime.QuarkusSteps...)
+func (t *quarkusTrait) addBuildSteps(task *v1alpha1.BuilderTask) {
+	task.Steps = append(task.Steps, builder.StepIDsFor(runtime.QuarkusSteps...)...)
 }
 
-func (t *quarkusTrait) addClasspath(e *Environment) {
+func (t *quarkusTrait) addClasspath(_ *Environment) {
 	// No-op as we rely on the Quarkus runner
 }
 
diff --git a/pkg/trait/trait_types.go b/pkg/trait/trait_types.go
index 6afb47f..fa7401a 100644
--- a/pkg/trait/trait_types.go
+++ b/pkg/trait/trait_types.go
@@ -33,7 +33,6 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
-	"github.com/apache/camel-k/pkg/builder"
 	"github.com/apache/camel-k/pkg/client"
 	"github.com/apache/camel-k/pkg/metadata"
 	"github.com/apache/camel-k/pkg/platform"
@@ -142,8 +141,7 @@ type Environment struct {
 	Resources      *kubernetes.Collection
 	PostActions    []func(*Environment) error
 	PostProcessors []func(*Environment) error
-	Steps          []builder.Step
-	BuildDir       string
+	BuildTasks     []v1alpha1.Task
 	ExecutedTraits []Trait
 	EnvVars        []corev1.EnvVar
 	Classpath      *strset.Set
diff --git a/pkg/util/patch/patch.go b/pkg/util/patch/patch.go
new file mode 100644
index 0000000..e0ae632
--- /dev/null
+++ b/pkg/util/patch/patch.go
@@ -0,0 +1,91 @@
+/*
+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 patch
+
+import (
+	"reflect"
+
+	jsonpatch "github.com/evanphx/json-patch"
+
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/util/json"
+)
+
+func PositiveMergePatch(source runtime.Object, target runtime.Object) ([]byte, error) {
+	sourceJSON, err := json.Marshal(source)
+	if err != nil {
+		return nil, err
+	}
+
+	targetJSON, err := json.Marshal(target)
+	if err != nil {
+		return nil, err
+	}
+
+	mergePatch, err := jsonpatch.CreateMergePatch(sourceJSON, targetJSON)
+	if err != nil {
+		return nil, err
+	}
+
+	var positivePatch map[string]interface{}
+	err = json.Unmarshal(mergePatch, &positivePatch)
+	if err != nil {
+		return nil, err
+	}
+
+	// The following is a work-around to remove null fields from the JSON merge patch,
+	// so that values defaulted by controllers server-side are not deleted.
+	// It's generally acceptable as these values are orthogonal to the values managed
+	// by the traits.
+	removeNilValues(reflect.ValueOf(positivePatch), reflect.Value{})
+
+	// Return an empty patch if no keys remain
+	if len(positivePatch) == 0 {
+		return make([]byte, 0), nil
+	}
+
+	return json.Marshal(positivePatch)
+}
+
+func removeNilValues(v reflect.Value, parent reflect.Value) {
+	for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
+		v = v.Elem()
+	}
+	switch v.Kind() {
+	case reflect.Array, reflect.Slice:
+		for i := 0; i < v.Len(); i++ {
+			removeNilValues(v.Index(i), v)
+		}
+	case reflect.Map:
+		for _, k := range v.MapKeys() {
+			switch c := v.MapIndex(k); {
+			case !c.IsValid():
+				// Skip keys previously deleted
+				continue
+			case c.IsNil(), c.Elem().Kind() == reflect.Map && len(c.Elem().MapKeys()) == 0:
+				v.SetMapIndex(k, reflect.Value{})
+			default:
+				removeNilValues(c, v)
+			}
+		}
+		// Back process the parent map in case it has been emptied so that it's deleted as well
+		if len(v.MapKeys()) == 0 && parent.Kind() == reflect.Map {
+			removeNilValues(parent, reflect.Value{})
+		}
+	}
+}