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:45 UTC

[camel-k] branch master updated (f217ed0 -> f4928bf)

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

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


    from f217ed0  Fixed a reference to command options not being propagated correctly.
     new 7447c26  feat(build): Task-based builds
     new b424b95  feat(build): Better handle tasks build directory sharing
     new 9e174ba  fix(build): Fix platform Spec to Status migration leftovers
     new 3f9cab4  fix(build): Disable Kaniko cache warming by default
     new 0bf1db0  fix(build): Fix .SourceStrategy.From.Kind.Name field in S2I BuildConfig
     new e5b2c1e  fix(build): Fix build routine execution
     new bf8e146  chore(build): Directly return when initializing recovery status
     new 97db011  chore(build): Recover the build when in-memory state is inconsistent
     new 11ad807  chore(build): Display build name in builder logger
     new b5a8e17  chore(build): Use positive patching to update build status in routine strategy too
     new dbe76a1  chore(build): Ensure build directory is cleaned-up
     new b27aa14  fix(build): Fix incremental build
     new f4928bf  chore(build): Isolate builder volume from Kaniko cache volume

The 13 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 pkg/apis/camel/v1alpha1/build_types.go             |  59 +++-
 pkg/apis/camel/v1alpha1/build_types_support.go     |  10 +
 .../v1alpha1/integrationplatform_types_support.go  |   4 +-
 pkg/apis/camel/v1alpha1/zz_generated.deepcopy.go   | 203 +++++++++++---
 pkg/builder/builder.go                             |  72 ++---
 pkg/builder/builder_steps.go                       |  17 +-
 pkg/builder/builder_steps_test.go                  |  40 ++-
 pkg/builder/builder_test.go                        |  20 +-
 pkg/builder/builder_types.go                       |  13 +-
 pkg/builder/kaniko/kaniko.go                       |   4 +-
 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                       |   9 +-
 pkg/cmd/builder.go                                 |   4 +-
 pkg/cmd/builder/builder.go                         |  55 ++--
 pkg/cmd/install.go                                 |   2 +-
 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            |  12 +-
 pkg/controller/build/recovery.go                   |   9 +-
 pkg/controller/build/schedule_pod.go               | 182 +++++--------
 pkg/controller/build/schedule_routine.go           |  87 ++++--
 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 |  18 +-
 pkg/platform/defaults.go                           |   6 +-
 pkg/trait/builder.go                               | 301 ++++++++++++++++++++-
 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 +++++++
 37 files changed, 1029 insertions(+), 836 deletions(-)
 delete mode 100644 pkg/controller/build/util_pod.go
 create mode 100644 pkg/util/patch/patch.go


[camel-k] 03/13: fix(build): Fix platform Spec to Status migration leftovers

Posted by as...@apache.org.
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 9e174ba13956cc693cc435ef7a5f4082b01c8be9
Author: Antonin Stefanutti <an...@stefanutti.fr>
AuthorDate: Mon Dec 16 14:07:29 2019 +0100

    fix(build): Fix platform Spec to Status migration leftovers
---
 pkg/controller/build/build_controller.go | 2 +-
 pkg/trait/builder.go                     | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/pkg/controller/build/build_controller.go b/pkg/controller/build/build_controller.go
index f9ef70c..6e9897f 100644
--- a/pkg/controller/build/build_controller.go
+++ b/pkg/controller/build/build_controller.go
@@ -176,7 +176,7 @@ func (r *ReconcileBuild) Reconcile(request reconcile.Request) (reconcile.Result,
 
 	var actions []Action
 
-	switch pl.Spec.Build.BuildStrategy {
+	switch pl.Status.Build.BuildStrategy {
 	case v1alpha1.IntegrationPlatformBuildStrategyPod:
 		actions = []Action{
 			NewInitializePodAction(),
diff --git a/pkg/trait/builder.go b/pkg/trait/builder.go
index b65fd8c..a81f841 100644
--- a/pkg/trait/builder.go
+++ b/pkg/trait/builder.go
@@ -118,7 +118,7 @@ func (t *builderTrait) Apply(e *Environment) error {
 				Name: "camel-k-builder",
 				VolumeSource: corev1.VolumeSource{
 					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
-						ClaimName: e.Platform.Spec.Build.PersistentVolumeClaim,
+						ClaimName: e.Platform.Status.Build.PersistentVolumeClaim,
 					},
 				},
 			})


[camel-k] 09/13: chore(build): Display build name in builder logger

Posted by as...@apache.org.
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 11ad807f355a44a9a5524dfd2efb800d4fac4f8a
Author: Antonin Stefanutti <an...@stefanutti.fr>
AuthorDate: Tue Dec 17 12:45:57 2019 +0100

    chore(build): Display build name in builder logger
---
 pkg/builder/builder.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/pkg/builder/builder.go b/pkg/builder/builder.go
index 4cf5c3d..c54b860 100644
--- a/pkg/builder/builder.go
+++ b/pkg/builder/builder.go
@@ -134,6 +134,7 @@ func (b *defaultBuilder) Run(build v1alpha1.BuilderTask) v1alpha1.BuildStatus {
 			l := b.log.WithValues(
 				"step", step.ID(),
 				"phase", step.Phase(),
+				"name", build.Meta.Name,
 				"task", build.Name,
 			)
 


[camel-k] 12/13: fix(build): Fix incremental build

Posted by as...@apache.org.
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 b27aa1471c57da5b2c91c4a739264702f1f5303c
Author: Antonin Stefanutti <an...@stefanutti.fr>
AuthorDate: Tue Dec 17 18:55:31 2019 +0100

    fix(build): Fix incremental build
---
 pkg/apis/camel/v1alpha1/build_types.go | 1 +
 pkg/builder/builder.go                 | 8 ++++++--
 pkg/builder/builder_steps.go           | 1 -
 pkg/builder/builder_types.go           | 2 +-
 4 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/pkg/apis/camel/v1alpha1/build_types.go b/pkg/apis/camel/v1alpha1/build_types.go
index e694b92..b721036 100644
--- a/pkg/apis/camel/v1alpha1/build_types.go
+++ b/pkg/apis/camel/v1alpha1/build_types.go
@@ -63,6 +63,7 @@ type KanikoTask struct {
 type BuilderTask struct {
 	BaseTask        `json:",inline"`
 	Meta            metav1.ObjectMeta `json:"meta,omitempty"`
+	Image           string            `json:"image,omitempty"`
 	BaseImage       string            `json:"baseImage,omitempty"`
 	CamelVersion    string            `json:"camelVersion,omitempty"`
 	RuntimeVersion  string            `json:"runtimeVersion,omitempty"`
diff --git a/pkg/builder/builder.go b/pkg/builder/builder.go
index c54b860..b98173a 100644
--- a/pkg/builder/builder.go
+++ b/pkg/builder/builder.go
@@ -74,6 +74,10 @@ func (b *defaultBuilder) Run(build v1alpha1.BuilderTask) v1alpha1.BuildStatus {
 		BaseImage: build.BaseImage,
 	}
 
+	if build.Image != "" {
+		c.BaseImage = build.Image
+	}
+
 	// base image is mandatory
 	if c.BaseImage == "" {
 		result.Phase = v1alpha1.BuildPhaseFailed
@@ -166,8 +170,8 @@ func (b *defaultBuilder) Run(build v1alpha1.BuilderTask) v1alpha1.BuildStatus {
 		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.BaseImage)
-		b.log.Infof("base image: %s", c.BaseImage)
+		b.log.Infof("base image: %s", build.BaseImage)
+		b.log.Infof("resolved base image: %s", c.BaseImage)
 		b.log.Infof("resolved image: %s", c.Image)
 	} else {
 		b.log.Infof("build task %s interrupted", build.Name)
diff --git a/pkg/builder/builder_steps.go b/pkg/builder/builder_steps.go
index b5e9f1a..fdee0f9 100644
--- a/pkg/builder/builder_steps.go
+++ b/pkg/builder/builder_steps.go
@@ -297,7 +297,6 @@ func incrementalPackager(ctx *Context) error {
 			}
 
 			ctx.BaseImage = bestImage.Image
-			//ctx.Image = bestImage.Image
 			ctx.SelectedArtifacts = selectedArtifacts
 		}
 
diff --git a/pkg/builder/builder_types.go b/pkg/builder/builder_types.go
index 52e6462..a497d79 100644
--- a/pkg/builder/builder_types.go
+++ b/pkg/builder/builder_types.go
@@ -120,7 +120,7 @@ type Context struct {
 
 // HasRequiredImage --
 func (c *Context) HasRequiredImage() bool {
-	return c.Build.BaseImage != ""
+	return c.Build.Image != ""
 }
 
 type publishedImage struct {


[camel-k] 05/13: fix(build): Fix .SourceStrategy.From.Kind.Name field in S2I BuildConfig

Posted by as...@apache.org.
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 0bf1db093867d6a2f5fdb65c5601dd95c0f4c72b
Author: Antonin Stefanutti <an...@stefanutti.fr>
AuthorDate: Tue Dec 17 12:15:50 2019 +0100

    fix(build): Fix .SourceStrategy.From.Kind.Name field in S2I BuildConfig
---
 pkg/builder/s2i/publisher.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pkg/builder/s2i/publisher.go b/pkg/builder/s2i/publisher.go
index 88c0192..d3afb3a 100644
--- a/pkg/builder/s2i/publisher.go
+++ b/pkg/builder/s2i/publisher.go
@@ -61,7 +61,7 @@ func publisher(ctx *builder.Context) error {
 					SourceStrategy: &buildv1.SourceBuildStrategy{
 						From: corev1.ObjectReference{
 							Kind: "DockerImage",
-							Name: ctx.Image,
+							Name: ctx.BaseImage,
 						},
 					},
 				},


[camel-k] 02/13: feat(build): Better handle tasks build directory sharing

Posted by as...@apache.org.
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 b424b959335ed43f7cb570c37065500cd869ff6c
Author: Antonin Stefanutti <an...@stefanutti.fr>
AuthorDate: Mon Dec 16 12:20:50 2019 +0100

    feat(build): Better handle tasks build directory sharing
---
 pkg/builder/builder.go               | 26 +++++++++++++-------------
 pkg/builder/builder_steps.go         |  2 +-
 pkg/controller/build/schedule_pod.go |  4 ++--
 pkg/trait/builder.go                 | 21 ++++++++++-----------
 4 files changed, 26 insertions(+), 27 deletions(-)

diff --git a/pkg/builder/builder.go b/pkg/builder/builder.go
index 2683386..4cf5c3d 100644
--- a/pkg/builder/builder.go
+++ b/pkg/builder/builder.go
@@ -51,24 +51,24 @@ func New(c client.Client) Builder {
 func (b *defaultBuilder) Run(build v1alpha1.BuilderTask) v1alpha1.BuildStatus {
 	result := v1alpha1.BuildStatus{}
 
-	// create tmp path
-	buildDir := build.BuildDir
-	if buildDir == "" {
-		buildDir = os.TempDir()
-	}
-	builderPath, err := ioutil.TempDir(buildDir, "builder-")
-	if err != nil {
-		log.Error(err, "Unexpected error while creating a temporary dir")
+	var buildDir string
+	if build.BuildDir == "" {
+		tmpDir, err := ioutil.TempDir(os.TempDir(), "builder-")
+		if err != nil {
+			log.Error(err, "Unexpected error while creating a temporary dir")
 
-		result.Phase = v1alpha1.BuildPhaseFailed
-		result.Error = err.Error()
+			result.Phase = v1alpha1.BuildPhaseFailed
+			result.Error = err.Error()
+		}
+		buildDir = tmpDir
+		defer os.RemoveAll(buildDir)
+	} else {
+		buildDir = build.BuildDir
 	}
 
-	defer os.RemoveAll(builderPath)
-
 	c := Context{
 		Client:    b.client,
-		Path:      builderPath,
+		Path:      buildDir,
 		Namespace: build.Meta.Namespace,
 		Build:     build,
 		BaseImage: build.BaseImage,
diff --git a/pkg/builder/builder_steps.go b/pkg/builder/builder_steps.go
index 4b84f9f..429d9b8 100644
--- a/pkg/builder/builder_steps.go
+++ b/pkg/builder/builder_steps.go
@@ -297,7 +297,7 @@ func packager(ctx *Context, selector artifactsSelector) error {
 		return err
 	}
 
-	tarFileName := path.Join(ctx.Build.BuildDir, "package", ctx.Build.Meta.Name)
+	tarFileName := path.Join(ctx.Path, "package", "occi.tar")
 	tarFileDir := path.Dir(tarFileName)
 
 	err = os.MkdirAll(tarFileDir, 0777)
diff --git a/pkg/controller/build/schedule_pod.go b/pkg/controller/build/schedule_pod.go
index 74ff938..30c2c38 100644
--- a/pkg/controller/build/schedule_pod.go
+++ b/pkg/controller/build/schedule_pod.go
@@ -141,7 +141,7 @@ func (action *schedulePodAction) newBuildPod(ctx context.Context, build *v1alpha
 			} else {
 				action.operatorImage = operatorImage
 			}
-			action.addCamelTaskToPod(build, task.Builder, pod)
+			action.addBuilderTaskToPod(build, task.Builder, pod)
 		} else if task.Kaniko != nil {
 			action.addKanikoTaskToPod(task.Kaniko, pod)
 		}
@@ -154,7 +154,7 @@ func (action *schedulePodAction) newBuildPod(ctx context.Context, build *v1alpha
 	return pod, nil
 }
 
-func (action *schedulePodAction) addCamelTaskToPod(build *v1alpha1.Build, task *v1alpha1.BuilderTask, pod *corev1.Pod) {
+func (action *schedulePodAction) addBuilderTaskToPod(build *v1alpha1.Build, task *v1alpha1.BuilderTask, pod *corev1.Pod) {
 	pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{
 		Name:            task.Name,
 		Image:           action.operatorImage,
diff --git a/pkg/trait/builder.go b/pkg/trait/builder.go
index 3001ca2..b65fd8c 100644
--- a/pkg/trait/builder.go
+++ b/pkg/trait/builder.go
@@ -60,8 +60,8 @@ func (t *builderTrait) Configure(e *Environment) (bool, error) {
 }
 
 func (t *builderTrait) Apply(e *Environment) error {
-	camelTask := t.camelTask(e)
-	e.BuildTasks = append(e.BuildTasks, v1alpha1.Task{Builder: camelTask})
+	builderTask := t.builderTask(e)
+	e.BuildTasks = append(e.BuildTasks, v1alpha1.Task{Builder: builderTask})
 
 	if platform.SupportsKanikoPublishStrategy(e.Platform) {
 		kanikoTask, err := t.kanikoTask(e)
@@ -69,7 +69,7 @@ func (t *builderTrait) Apply(e *Environment) error {
 			return err
 		}
 		mount := corev1.VolumeMount{Name: "camel-k-builder", MountPath: kaniko.BuildDir}
-		camelTask.VolumeMounts = append(camelTask.VolumeMounts, mount)
+		builderTask.VolumeMounts = append(builderTask.VolumeMounts, mount)
 		kanikoTask.VolumeMounts = append(kanikoTask.VolumeMounts, mount)
 
 		if e.Platform.Status.Build.IsKanikoCacheEnabled() {
@@ -114,7 +114,7 @@ func (t *builderTrait) Apply(e *Environment) error {
 				},
 			}
 			// 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{
+			builderTask.Volumes = append(builderTask.Volumes, corev1.Volume{
 				Name: "camel-k-builder",
 				VolumeSource: corev1.VolumeSource{
 					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
@@ -124,11 +124,10 @@ func (t *builderTrait) Apply(e *Environment) error {
 			})
 		} else {
 			// Use an emptyDir volume to coordinate the Camel Maven build and the Kaniko image build
-			camelTask.Volumes = append(camelTask.Volumes, corev1.Volume{
+			builderTask.Volumes = append(builderTask.Volumes, corev1.Volume{
 				Name: "camel-k-builder",
 				VolumeSource: corev1.VolumeSource{
-					EmptyDir: &corev1.EmptyDirVolumeSource{
-					},
+					EmptyDir: &corev1.EmptyDirVolumeSource{},
 				},
 			})
 		}
@@ -149,10 +148,10 @@ func (t *builderTrait) InfluencesKit() bool {
 	return true
 }
 
-func (t *builderTrait) camelTask(e *Environment) *v1alpha1.BuilderTask {
+func (t *builderTrait) builderTask(e *Environment) *v1alpha1.BuilderTask {
 	task := &v1alpha1.BuilderTask{
 		BaseTask: v1alpha1.BaseTask{
-			Name: "camel",
+			Name: "builder",
 		},
 		Meta:            e.IntegrationKit.ObjectMeta,
 		BaseImage:       e.Platform.Status.Build.BaseImage,
@@ -173,7 +172,7 @@ func (t *builderTrait) camelTask(e *Environment) *v1alpha1.BuilderTask {
 		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
+		task.BuildDir = path.Join(kaniko.BuildDir, e.IntegrationKit.Name)
 	}
 
 	quarkus := e.Catalog.GetTrait("quarkus").(*quarkusTrait)
@@ -198,7 +197,7 @@ func (t *builderTrait) kanikoTask(e *Environment) (*v1alpha1.KanikoTask, error)
 	env := make([]corev1.EnvVar, 0)
 	baseArgs := []string{
 		"--dockerfile=Dockerfile",
-		"--context=" + path.Join(kaniko.BuildDir, "package", "context"),
+		"--context=" + path.Join(kaniko.BuildDir, e.IntegrationKit.Name, "package", "context"),
 		"--destination=" + image,
 		"--cache=" + strconv.FormatBool(e.Platform.Status.Build.IsKanikoCacheEnabled()),
 		"--cache-dir=" + path.Join(kaniko.BuildDir, "cache"),


[camel-k] 04/13: fix(build): Disable Kaniko cache warming by default

Posted by as...@apache.org.
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 3f9cab433aa7d689f5fd271104c443a74054dc1a
Author: Antonin Stefanutti <an...@stefanutti.fr>
AuthorDate: Mon Dec 16 15:18:43 2019 +0100

    fix(build): Disable Kaniko cache warming by default
---
 pkg/apis/camel/v1alpha1/integrationplatform_types_support.go | 4 ++--
 pkg/cmd/install.go                                           | 2 +-
 pkg/platform/defaults.go                                     | 6 ++++--
 3 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/pkg/apis/camel/v1alpha1/integrationplatform_types_support.go b/pkg/apis/camel/v1alpha1/integrationplatform_types_support.go
index d85cc34..64f8209 100644
--- a/pkg/apis/camel/v1alpha1/integrationplatform_types_support.go
+++ b/pkg/apis/camel/v1alpha1/integrationplatform_types_support.go
@@ -181,8 +181,8 @@ func (in *IntegrationPlatformStatus) RemoveCondition(condType IntegrationPlatfor
 // IsKanikoCacheEnabled tells if the KanikoCache is enabled on the integration platform build spec
 func (b IntegrationPlatformBuildSpec) IsKanikoCacheEnabled() bool {
 	if b.KanikoBuildCache == nil {
-		// Cache is enabled unless explicitly disabled
-		return true
+		// Cache is disabled by default
+		return false
 	}
 	return *b.KanikoBuildCache
 }
diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go
index 96289da..9519777 100644
--- a/pkg/cmd/install.go
+++ b/pkg/cmd/install.go
@@ -89,7 +89,7 @@ func newCmdInstall(rootCmdOptions *RootCmdOptions) (*cobra.Command, *installCmdO
 	cmd.Flags().String("build-strategy", "", "Set the build strategy")
 	cmd.Flags().String("build-timeout", "", "Set how long the build process can last")
 	cmd.Flags().String("trait-profile", "", "The profile to use for traits")
-	cmd.Flags().Bool("kaniko-build-cache", true, "To enable or disable the Kaniko Cache in building phase")
+	cmd.Flags().Bool("kaniko-build-cache", false, "To enable or disable the Kaniko cache")
 	cmd.Flags().String("http-proxy-secret", "", "Configure the source of the secret holding HTTP proxy server details "+
 		"(HTTP_PROXY|HTTPS_PROXY|NO_PROXY)")
 
diff --git a/pkg/platform/defaults.go b/pkg/platform/defaults.go
index b637731..6d6849e 100644
--- a/pkg/platform/defaults.go
+++ b/pkg/platform/defaults.go
@@ -172,8 +172,10 @@ func setPlatformDefaults(ctx context.Context, c client.Client, p *v1alpha1.Integ
 	}
 
 	if p.Status.Build.PublishStrategy == v1alpha1.IntegrationPlatformBuildPublishStrategyKaniko && p.Status.Build.KanikoBuildCache == nil {
-		// Default to using Kaniko cache warmer
-		defaultKanikoBuildCache := true
+		// Default to disabling Kaniko cache warmer
+		// Using the cache warmer pod seems unreliable with the current Kaniko version
+		// and requires relying on a persistent volume.
+		defaultKanikoBuildCache := false
 		p.Status.Build.KanikoBuildCache = &defaultKanikoBuildCache
 		if verbose {
 			log.Log.Infof("Kaniko cache set to %t", *p.Status.Build.KanikoBuildCache)


[camel-k] 07/13: chore(build): Directly return when initializing recovery status

Posted by as...@apache.org.
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 bf8e1462a4e2d4af6fa0952f90667129b5535696
Author: Antonin Stefanutti <an...@stefanutti.fr>
AuthorDate: Tue Dec 17 12:21:37 2019 +0100

    chore(build): Directly return when initializing recovery status
---
 pkg/controller/build/recovery.go | 9 +++------
 1 file changed, 3 insertions(+), 6 deletions(-)

diff --git a/pkg/controller/build/recovery.go b/pkg/controller/build/recovery.go
index 1d8d7a9..cf2dc7e 100644
--- a/pkg/controller/build/recovery.go
+++ b/pkg/controller/build/recovery.go
@@ -21,10 +21,11 @@ import (
 	"context"
 	"time"
 
+	"github.com/jpillora/backoff"
+
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
-	"github.com/jpillora/backoff"
 )
 
 // NewErrorRecoveryAction creates a new error recovering handling action for the build
@@ -63,11 +64,7 @@ func (action *errorRecoveryAction) Handle(ctx context.Context, build *v1alpha1.B
 				AttemptMax: 5,
 			},
 		}
-	}
-
-	err := action.client.Status().Update(ctx, build)
-	if err != nil {
-		return nil, err
+		return build, nil
 	}
 
 	if build.Status.Failure.Recovery.Attempt >= build.Status.Failure.Recovery.AttemptMax {


[camel-k] 13/13: chore(build): Isolate builder volume from Kaniko cache volume

Posted by as...@apache.org.
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 f4928bf7c49c88f994d4ce73074ccc5582f41155
Author: Antonin Stefanutti <an...@stefanutti.fr>
AuthorDate: Wed Dec 18 11:35:07 2019 +0100

    chore(build): Isolate builder volume from Kaniko cache volume
---
 pkg/builder/kaniko/kaniko.go                       |  4 +--
 pkg/controller/integrationplatform/kaniko_cache.go | 14 ++++----
 pkg/trait/builder.go                               | 39 +++++++++++++---------
 3 files changed, 32 insertions(+), 25 deletions(-)

diff --git a/pkg/builder/kaniko/kaniko.go b/pkg/builder/kaniko/kaniko.go
index efc0291..b61638d 100644
--- a/pkg/builder/kaniko/kaniko.go
+++ b/pkg/builder/kaniko/kaniko.go
@@ -42,5 +42,5 @@ var KanikoSteps = []builder.Step{
 	Steps.Publisher,
 }
 
-// BuildDir is the directory where to build artifacts (shared with the Kaniko pod)
-var BuildDir = "/workspace"
+// CacheDir is the cache directory for Kaniko builds (mounted into the Kaniko pod)
+const CacheDir = "/kaniko/cache"
diff --git a/pkg/controller/integrationplatform/kaniko_cache.go b/pkg/controller/integrationplatform/kaniko_cache.go
index 2d5674d..195090e 100644
--- a/pkg/controller/integrationplatform/kaniko_cache.go
+++ b/pkg/controller/integrationplatform/kaniko_cache.go
@@ -58,13 +58,13 @@ func createKanikoCacheWarmerPod(ctx context.Context, client client.Client, platf
 					Name:  "warm-kaniko-cache",
 					Image: fmt.Sprintf("gcr.io/kaniko-project/warmer:v%s", defaults.KanikoVersion),
 					Args: []string{
-						"--cache-dir=/workspace/cache",
+						"--cache-dir=" + kaniko.CacheDir,
 						"--image=" + platform.Status.Build.BaseImage,
 					},
 					VolumeMounts: []corev1.VolumeMount{
 						{
-							Name:      "camel-k-builder",
-							MountPath: kaniko.BuildDir,
+							Name:      "kaniko-cache",
+							MountPath: kaniko.CacheDir,
 						},
 					},
 				},
@@ -76,11 +76,11 @@ func createKanikoCacheWarmerPod(ctx context.Context, client client.Client, platf
 					Image:           "busybox",
 					ImagePullPolicy: corev1.PullIfNotPresent,
 					Command:         []string{"/bin/sh", "-c"},
-					Args:            []string{"mkdir -p /workspace/cache && chmod -R a+rwx /workspace"},
+					Args:            []string{"mkdir -p " + kaniko.CacheDir + "&& chmod -R a+rwx " + kaniko.CacheDir},
 					VolumeMounts: []corev1.VolumeMount{
 						{
-							Name:      "camel-k-builder",
-							MountPath: kaniko.BuildDir,
+							Name:      "kaniko-cache",
+							MountPath: kaniko.CacheDir,
 						},
 					},
 				},
@@ -88,7 +88,7 @@ func createKanikoCacheWarmerPod(ctx context.Context, client client.Client, platf
 			RestartPolicy: corev1.RestartPolicyOnFailure,
 			Volumes: []corev1.Volume{
 				{
-					Name: "camel-k-builder",
+					Name: "kaniko-cache",
 					VolumeSource: corev1.VolumeSource{
 						PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
 							ClaimName: platform.Status.Build.PersistentVolumeClaim,
diff --git a/pkg/trait/builder.go b/pkg/trait/builder.go
index a81f841..e008c8f 100644
--- a/pkg/trait/builder.go
+++ b/pkg/trait/builder.go
@@ -37,6 +37,8 @@ import (
 	"github.com/apache/camel-k/pkg/util/defaults"
 )
 
+const builderDir = "/builder"
+
 // The builder trait is internally used to determine the best strategy to
 // build and configure IntegrationKits.
 //
@@ -68,10 +70,19 @@ func (t *builderTrait) Apply(e *Environment) error {
 		if err != nil {
 			return err
 		}
-		mount := corev1.VolumeMount{Name: "camel-k-builder", MountPath: kaniko.BuildDir}
+
+		mount := corev1.VolumeMount{Name: "camel-k-builder", MountPath: builderDir}
 		builderTask.VolumeMounts = append(builderTask.VolumeMounts, mount)
 		kanikoTask.VolumeMounts = append(kanikoTask.VolumeMounts, mount)
 
+		// Use an emptyDir volume to coordinate the Camel Maven build and the Kaniko image build
+		builderTask.Volumes = append(builderTask.Volumes, corev1.Volume{
+			Name: "camel-k-builder",
+			VolumeSource: corev1.VolumeSource{
+				EmptyDir: &corev1.EmptyDirVolumeSource{},
+			},
+		})
+
 		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
@@ -113,22 +124,18 @@ func (t *builderTrait) Apply(e *Environment) error {
 					},
 				},
 			}
-			// Use the PVC used to warm the Kaniko cache to coordinate the Camel Maven build and the Kaniko image build
-			builderTask.Volumes = append(builderTask.Volumes, corev1.Volume{
-				Name: "camel-k-builder",
+			// Mount the PV used to warm the Kaniko cache into the Kaniko image build
+			kanikoTask.Volumes = append(kanikoTask.Volumes, corev1.Volume{
+				Name: "kaniko-cache",
 				VolumeSource: corev1.VolumeSource{
 					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
 						ClaimName: e.Platform.Status.Build.PersistentVolumeClaim,
 					},
 				},
 			})
-		} else {
-			// Use an emptyDir volume to coordinate the Camel Maven build and the Kaniko image build
-			builderTask.Volumes = append(builderTask.Volumes, corev1.Volume{
-				Name: "camel-k-builder",
-				VolumeSource: corev1.VolumeSource{
-					EmptyDir: &corev1.EmptyDirVolumeSource{},
-				},
+			kanikoTask.VolumeMounts = append(kanikoTask.VolumeMounts, corev1.VolumeMount{
+				Name:      "kaniko-cache",
+				MountPath: kaniko.CacheDir,
 			})
 		}
 
@@ -158,8 +165,8 @@ func (t *builderTrait) builderTask(e *Environment) *v1alpha1.BuilderTask {
 		CamelVersion:    e.CamelCatalog.Version,
 		RuntimeVersion:  e.CamelCatalog.RuntimeVersion,
 		RuntimeProvider: e.CamelCatalog.RuntimeProvider,
-		//Sources:		 e.Integration.Spec.Sources,
-		//Resources:     e.Integration.Spec.Resources,
+		//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...),
@@ -172,7 +179,7 @@ func (t *builderTrait) builderTask(e *Environment) *v1alpha1.BuilderTask {
 		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 = path.Join(kaniko.BuildDir, e.IntegrationKit.Name)
+		task.BuildDir = path.Join(builderDir, e.IntegrationKit.Name)
 	}
 
 	quarkus := e.Catalog.GetTrait("quarkus").(*quarkusTrait)
@@ -197,10 +204,10 @@ func (t *builderTrait) kanikoTask(e *Environment) (*v1alpha1.KanikoTask, error)
 	env := make([]corev1.EnvVar, 0)
 	baseArgs := []string{
 		"--dockerfile=Dockerfile",
-		"--context=" + path.Join(kaniko.BuildDir, e.IntegrationKit.Name, "package", "context"),
+		"--context=" + path.Join(builderDir, e.IntegrationKit.Name, "package", "context"),
 		"--destination=" + image,
 		"--cache=" + strconv.FormatBool(e.Platform.Status.Build.IsKanikoCacheEnabled()),
-		"--cache-dir=" + path.Join(kaniko.BuildDir, "cache"),
+		"--cache-dir=" + kaniko.CacheDir,
 	}
 
 	args := make([]string, 0, len(baseArgs))


[camel-k] 08/13: chore(build): Recover the build when in-memory state is inconsistent

Posted by as...@apache.org.
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 97db011f65b581c0ae53cde95497267df1622227
Author: Antonin Stefanutti <an...@stefanutti.fr>
AuthorDate: Tue Dec 17 12:23:41 2019 +0100

    chore(build): Recover the build when in-memory state is inconsistent
---
 pkg/controller/build/monitor_routine.go | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/pkg/controller/build/monitor_routine.go b/pkg/controller/build/monitor_routine.go
index c1fc7da..3c61cf0 100644
--- a/pkg/controller/build/monitor_routine.go
+++ b/pkg/controller/build/monitor_routine.go
@@ -49,10 +49,10 @@ func (action *monitorRoutineAction) CanHandle(build *v1alpha1.Build) bool {
 // Handle handles the builds
 func (action *monitorRoutineAction) Handle(ctx context.Context, build *v1alpha1.Build) (*v1alpha1.Build, error) {
 	// Check the build routine
-	if _, ok := action.routines.Load(build.Name); !ok {
-		// and reschedule the build if it's missing. This can happen when the operator
-		// stops abruptly and restarts.
-		build.Status.Phase = v1alpha1.BuildPhaseScheduling
+	if _, ok := action.routines.Load(build.Name); !ok && build.Status.Phase != v1alpha1.BuildPhaseFailed {
+		// and recover the build if it's missing. This can happen when the operator
+		// stops abruptly and restarts or the build status update fails.
+		build.Status.Phase = v1alpha1.BuildPhaseFailed
 
 		return build, nil
 	}


[camel-k] 06/13: fix(build): Fix build routine execution

Posted by as...@apache.org.
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 e5b2c1ed9cf8060822bef8e35dfd18681a05cef4
Author: Antonin Stefanutti <an...@stefanutti.fr>
AuthorDate: Tue Dec 17 12:18:35 2019 +0100

    fix(build): Fix build routine execution
---
 pkg/controller/build/schedule_routine.go | 71 ++++++++++++++++++--------------
 1 file changed, 40 insertions(+), 31 deletions(-)

diff --git a/pkg/controller/build/schedule_routine.go b/pkg/controller/build/schedule_routine.go
index 6535687..f4abe59 100644
--- a/pkg/controller/build/schedule_routine.go
+++ b/pkg/controller/build/schedule_routine.go
@@ -91,42 +91,51 @@ func (action *scheduleRoutineAction) Handle(ctx context.Context, build *v1alpha1
 	}
 
 	// Start the build asynchronously to avoid blocking the reconcile loop
-	go func() {
-		defer action.routines.Delete(build.Name)
+	action.routines.Store(build.Name, true)
 
-		status := v1alpha1.BuildStatus{
-			Phase:     v1alpha1.BuildPhaseRunning,
-			StartedAt: metav1.Now(),
-		}
-		if err := action.updateBuildStatus(ctx, build, status); err != nil {
-			return
-		}
+	go action.runBuild(ctx, build)
 
-		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
-				}
+	return nil, nil
+}
+
+func (action *scheduleRoutineAction) runBuild(ctx context.Context, build *v1alpha1.Build) {
+	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()),
+				Duration: metav1.Now().Sub(build.Status.StartedAt.Time).String(),
 			}
+			_ = action.updateBuildStatus(ctx, build, status)
+			break
 		}
-	}()
-
-	action.routines.Store(build.Name, true)
 
-	return nil, nil
+		status := action.builder.Run(*task.Builder)
+		lastTask := i == len(build.Spec.Tasks)-1
+		taskFailed := status.Phase == v1alpha1.BuildPhaseFailed
+		if lastTask || taskFailed {
+			status.Duration = metav1.Now().Sub(build.Status.StartedAt.Time).String()
+		}
+		if lastTask && !taskFailed {
+			status.Phase = v1alpha1.BuildPhaseSucceeded
+		}
+		err := action.updateBuildStatus(ctx, build, status)
+		if err != nil || taskFailed {
+			break
+		}
+	}
 }
 
 func (action *scheduleRoutineAction) updateBuildStatus(ctx context.Context, build *v1alpha1.Build, status v1alpha1.BuildStatus) error {


[camel-k] 10/13: chore(build): Use positive patching to update build status in routine strategy too

Posted by as...@apache.org.
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 b5a8e17d41a88673b47333bf4d9e95adae15736c
Author: Antonin Stefanutti <an...@stefanutti.fr>
AuthorDate: Tue Dec 17 12:48:11 2019 +0100

    chore(build): Use positive patching to update build status in routine strategy too
---
 pkg/controller/build/schedule_routine.go | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/pkg/controller/build/schedule_routine.go b/pkg/controller/build/schedule_routine.go
index f4abe59..61a9173 100644
--- a/pkg/controller/build/schedule_routine.go
+++ b/pkg/controller/build/schedule_routine.go
@@ -23,11 +23,13 @@ import (
 	"sync"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
 
 	"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/util/patch"
 )
 
 // NewScheduleRoutineAction creates a new schedule routine action
@@ -143,8 +145,13 @@ func (action *scheduleRoutineAction) updateBuildStatus(ctx context.Context, buil
 	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))
+	// Patch the build status with the result
+	p, err := patch.PositiveMergePatch(build, target)
+	if err != nil {
+		action.L.Errorf(err, "Cannot patch build status: %s", build.Name)
+		return err
+	}
+	err = action.client.Status().Patch(ctx, target, client.ConstantPatch(types.MergePatchType, p))
 	if err != nil {
 		action.L.Errorf(err, "Cannot update build status: %s", build.Name)
 		return err


[camel-k] 11/13: chore(build): Ensure build directory is cleaned-up

Posted by as...@apache.org.
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 dbe76a1af698770db31c9b4d7912a25d76c8d4eb
Author: Antonin Stefanutti <an...@stefanutti.fr>
AuthorDate: Tue Dec 17 18:35:42 2019 +0100

    chore(build): Ensure build directory is cleaned-up
---
 pkg/builder/builder_steps.go | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/pkg/builder/builder_steps.go b/pkg/builder/builder_steps.go
index 429d9b8..b5e9f1a 100644
--- a/pkg/builder/builder_steps.go
+++ b/pkg/builder/builder_steps.go
@@ -43,6 +43,7 @@ func init() {
 }
 
 type steps struct {
+	CleanBuildDir           Step
 	GenerateProjectSettings Step
 	InjectDependencies      Step
 	SanitizeDependencies    Step
@@ -52,6 +53,10 @@ type steps struct {
 
 // Steps --
 var Steps = steps{
+	CleanBuildDir: NewStep(
+		ProjectGenerationPhase-1,
+		cleanBuildDir,
+	),
 	GenerateProjectSettings: NewStep(
 		ProjectGenerationPhase+1,
 		generateProjectSettings,
@@ -76,6 +81,7 @@ var Steps = steps{
 
 // DefaultSteps --
 var DefaultSteps = []Step{
+	Steps.CleanBuildDir,
 	Steps.GenerateProjectSettings,
 	Steps.InjectDependencies,
 	Steps.SanitizeDependencies,
@@ -108,6 +114,14 @@ func registerStep(steps ...Step) {
 	}
 }
 
+func cleanBuildDir(ctx *Context) error {
+	if ctx.Build.BuildDir == "" {
+		return nil
+	}
+
+	return os.RemoveAll(ctx.Build.BuildDir)
+}
+
 func generateProjectSettings(ctx *Context) error {
 	val, err := kubernetes.ResolveValueSource(ctx.C, ctx.Client, ctx.Namespace, &ctx.Build.Maven.Settings)
 	if err != nil {


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

Posted by as...@apache.org.
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{})
+		}
+	}
+}