Repository: apache/camel-k
Pull request #784 (2019-06-28 15:00:16)

Refactor controller logic
Common actions such as deep-cloning the resource and update
them are now performed by the controller wich results in 
clean handlers

lburgazzoli: @astefanutti ready for review

Patch:
diff --git a/pkg/apis/camel/v1alpha1/integrationkit_types.go b/pkg/apis/camel/v1alpha1/integrationkit_types.go
index 0dc8616db..2d199ae5c 100644
--- a/pkg/apis/camel/v1alpha1/integrationkit_types.go
+++ b/pkg/apis/camel/v1alpha1/integrationkit_types.go
@@ -82,6 +82,10 @@ const (
 	// IntegrationKitTypeExternal --
 	IntegrationKitTypeExternal = "external"
 
+	// IntegrationKitPhaseInitial --
+	IntegrationKitPhaseInitial IntegrationKitPhase = ""
+	// IntegrationKitPhaseWaitingForPlatform --
+	IntegrationKitPhaseWaitingForPlatform IntegrationKitPhase = "Waiting For Platform"
 	// IntegrationKitPhaseBuildSubmitted --
 	IntegrationKitPhaseBuildSubmitted IntegrationKitPhase = "Build Submitted"
 	// IntegrationKitPhaseBuildRunning --
diff --git a/pkg/controller/build/action.go b/pkg/controller/build/action.go
index b504d4d9d..448f80527 100644
--- a/pkg/controller/build/action.go
+++ b/pkg/controller/build/action.go
@@ -28,6 +28,7 @@ import (
 // Action --
 type Action interface {
 	client.Injectable
+	log.Injectable
 
 	// a user friendly name for the action
 	Name() string
@@ -36,10 +37,7 @@ type Action interface {
 	CanHandle(build *v1alpha1.Build) bool
 
 	// executes the handling function
-	Handle(ctx context.Context, build *v1alpha1.Build) error
-
-	// Inject integration logger
-	InjectLogger(log.Logger)
+	Handle(ctx context.Context, build *v1alpha1.Build) (*v1alpha1.Build, error)
 }
 
 type baseAction struct {
diff --git a/pkg/controller/build/build_controller.go b/pkg/controller/build/build_controller.go
index 1febc5a75..fd8516046 100644
--- a/pkg/controller/build/build_controller.go
+++ b/pkg/controller/build/build_controller.go
@@ -148,9 +148,9 @@ func (r *ReconcileBuild) Reconcile(request reconcile.Request) (reconcile.Result,
 	ctx := context.TODO()
 
 	// Fetch the Integration instance
-	instance := &v1alpha1.Build{}
-	err := r.client.Get(ctx, request.NamespacedName, instance)
-	if err != nil {
+	var instance v1alpha1.Build
+
+	if err := r.client.Get(ctx, request.NamespacedName, &instance); err != nil {
 		if errors.IsNotFound(err) {
 			// Request object not found, could have been deleted after reconcile request.
 			// Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
@@ -161,7 +161,7 @@ func (r *ReconcileBuild) Reconcile(request reconcile.Request) (reconcile.Result,
 		return reconcile.Result{}, err
 	}
 
-	buildActionPool := []Action{
+	actions := []Action{
 		NewInitializeAction(),
 		NewScheduleRoutineAction(r.reader, r.builder, &r.routines),
 		NewSchedulePodAction(r.reader),
@@ -170,33 +170,57 @@ func (r *ReconcileBuild) Reconcile(request reconcile.Request) (reconcile.Result,
 		NewErrorRecoveryAction(),
 	}
 
-	blog := rlog.ForBuild(instance)
-	for _, a := range buildActionPool {
+	var err error
+
+	target := instance.DeepCopy()
+	targetPhase := target.Status.Phase
+	targetLog := rlog.ForBuild(target)
+
+	for _, a := range actions {
 		a.InjectClient(r.client)
-		a.InjectLogger(blog)
-		if a.CanHandle(instance) {
-			blog.Infof("Invoking action %s", a.Name())
-			if err := a.Handle(ctx, instance); err != nil {
-				if k8serrors.IsConflict(err) {
-					blog.Error(err, "conflict")
-					return reconcile.Result{
-						Requeue: true,
-					}, nil
-				}
+		a.InjectLogger(targetLog)
 
+		if a.CanHandle(target) {
+			targetLog.Infof("Invoking action %s", a.Name())
+
+			phaseFrom := target.Status.Phase
+
+			target, err = a.Handle(ctx, target)
+			if err != nil {
 				return reconcile.Result{}, err
 			}
-		}
-	}
 
-	// Refresh the build and check the state
-	if err = r.client.Get(ctx, request.NamespacedName, instance); err != nil {
-		return reconcile.Result{}, err
+			if target != nil {
+				if err := r.client.Status().Update(ctx, target); err != nil {
+					if k8serrors.IsConflict(err) {
+						targetLog.Error(err, "conflict")
+						return reconcile.Result{
+							Requeue: true,
+						}, nil
+					}
+
+					return reconcile.Result{}, err
+				}
+
+				targetPhase = target.Status.Phase
+
+				if targetPhase != phaseFrom {
+					targetLog.Info(
+						"state transition",
+						"phase-from", phaseFrom,
+						"phase-to", target.Status.Phase,
+					)
+				}
+			}
+
+			// handle one action at time so the resource
+			// is always at its latest state
+			break
+		}
 	}
 
 	// Requeue scheduling build so that it re-enters the build working queue
-	if instance.Status.Phase == v1alpha1.BuildPhaseScheduling ||
-		instance.Status.Phase == v1alpha1.BuildPhaseFailed {
+	if targetPhase == v1alpha1.BuildPhaseScheduling || targetPhase == v1alpha1.BuildPhaseFailed {
 		return reconcile.Result{
 			RequeueAfter: 5 * time.Second,
 		}, nil
diff --git a/pkg/controller/build/initialize.go b/pkg/controller/build/initialize.go
index bd9b8d0d0..a6fba9cc1 100644
--- a/pkg/controller/build/initialize.go
+++ b/pkg/controller/build/initialize.go
@@ -43,10 +43,8 @@ func (action *initializeAction) CanHandle(build *v1alpha1.Build) bool {
 }
 
 // Handle handles the builds
-func (action *initializeAction) Handle(ctx context.Context, build *v1alpha1.Build) error {
-	target := build.DeepCopy()
-	target.Status.Phase = v1alpha1.BuildPhaseScheduling
-	action.L.Info("Build state transition", "phase", target.Status.Phase)
+func (action *initializeAction) Handle(ctx context.Context, build *v1alpha1.Build) (*v1alpha1.Build, error) {
+	build.Status.Phase = v1alpha1.BuildPhaseScheduling
 
-	return action.client.Status().Update(ctx, target)
+	return build, nil
 }
diff --git a/pkg/controller/build/monitor_pod.go b/pkg/controller/build/monitor_pod.go
index 485ce387e..7979e3403 100644
--- a/pkg/controller/build/monitor_pod.go
+++ b/pkg/controller/build/monitor_pod.go
@@ -49,34 +49,34 @@ func (action *monitorPodAction) CanHandle(build *v1alpha1.Build) bool {
 }
 
 // Handle handles the builds
-func (action *monitorPodAction) Handle(ctx context.Context, build *v1alpha1.Build) error {
-	target := build.DeepCopy()
-
+func (action *monitorPodAction) Handle(ctx context.Context, build *v1alpha1.Build) (*v1alpha1.Build, error) {
 	// Get the build pod
 	pod := &corev1.Pod{}
 	err := action.client.Get(ctx, types.NamespacedName{Namespace: build.Namespace, Name: buildPodName(build.Spec.Meta)}, pod)
 	if err != nil {
 		if k8serrors.IsNotFound(err) {
 			// Let's reschedule the build
-			target.Status.Phase = v1alpha1.BuildPhaseScheduling
+			build.Status.Phase = v1alpha1.BuildPhaseScheduling
 		} else {
-			return err
+			return nil, err
 		}
 	}
 
+	var buildPhase v1alpha1.BuildPhase
+
 	switch pod.Status.Phase {
 	case corev1.PodSucceeded:
-		target.Status.Phase = v1alpha1.BuildPhaseSucceeded
+		buildPhase = v1alpha1.BuildPhaseSucceeded
 	case corev1.PodFailed:
-		target.Status.Phase = v1alpha1.BuildPhaseFailed
+		buildPhase = v1alpha1.BuildPhaseFailed
 	}
 
-	if target.Status.Phase == build.Status.Phase {
+	if build.Status.Phase == buildPhase {
 		// Status is already up-to-date
-		return nil
+		return nil, nil
 	}
 
-	action.L.Info("Build state transition", "phase", target.Status.Phase)
+	build.Status.Phase = buildPhase
 
-	return action.client.Status().Update(ctx, target)
+	return build, nil
 }
diff --git a/pkg/controller/build/monitor_routine.go b/pkg/controller/build/monitor_routine.go
index 8fc8dd802..170a57561 100644
--- a/pkg/controller/build/monitor_routine.go
+++ b/pkg/controller/build/monitor_routine.go
@@ -49,19 +49,15 @@ func (action *monitorRoutineAction) CanHandle(build *v1alpha1.Build) bool {
 }
 
 // Handle handles the builds
-func (action *monitorRoutineAction) Handle(ctx context.Context, build *v1alpha1.Build) error {
-	target := build.DeepCopy()
-
+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.
-		target.Status.Phase = v1alpha1.BuildPhaseScheduling
-
-		action.L.Info("Build state transition", "phase", target.Status.Phase)
+		build.Status.Phase = v1alpha1.BuildPhaseScheduling
 
-		return action.client.Status().Update(ctx, target)
+		return build, nil
 	}
 
-	return nil
+	return nil, nil
 }
diff --git a/pkg/controller/build/recovery.go b/pkg/controller/build/recovery.go
index 9c6d18ae5..4492c87b8 100644
--- a/pkg/controller/build/recovery.go
+++ b/pkg/controller/build/recovery.go
@@ -55,11 +55,11 @@ func (action *errorRecoveryAction) CanHandle(build *v1alpha1.Build) bool {
 	return build.Status.Phase == v1alpha1.BuildPhaseFailed
 }
 
-func (action *errorRecoveryAction) Handle(ctx context.Context, build *v1alpha1.Build) error {
+func (action *errorRecoveryAction) Handle(ctx context.Context, build *v1alpha1.Build) (*v1alpha1.Build, error) {
 	// The integration platform must be initialized before handling the error recovery
 	if _, err := platform.GetCurrentPlatform(ctx, action.client, build.Namespace); err != nil {
 		action.L.Info("Waiting for an integration platform to be initialized")
-		return nil
+		return nil, nil
 	}
 
 	if build.Status.Failure == nil {
@@ -75,17 +75,12 @@ func (action *errorRecoveryAction) Handle(ctx context.Context, build *v1alpha1.B
 
 	err := action.client.Status().Update(ctx, build)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
-	target := build.DeepCopy()
-
 	if build.Status.Failure.Recovery.Attempt >= build.Status.Failure.Recovery.AttemptMax {
-		target.Status.Phase = v1alpha1.BuildPhaseError
-
-		action.L.Info("Max recovery attempt reached, transition to error phase")
-
-		return action.client.Status().Update(ctx, target)
+		build.Status.Phase = v1alpha1.BuildPhaseError
+		return build, nil
 	}
 
 	lastAttempt := build.Status.Failure.Recovery.AttemptTime.Time
@@ -97,19 +92,18 @@ func (action *errorRecoveryAction) Handle(ctx context.Context, build *v1alpha1.B
 	elapsedMin := action.backOff.ForAttempt(float64(build.Status.Failure.Recovery.Attempt)).Seconds()
 
 	if elapsed < elapsedMin {
-		return nil
+		return nil, nil
 	}
 
-	target.Status = v1alpha1.BuildStatus{}
-	target.Status.Phase = ""
-	target.Status.Failure = build.Status.Failure
-	target.Status.Failure.Recovery.Attempt = build.Status.Failure.Recovery.Attempt + 1
-	target.Status.Failure.Recovery.AttemptTime = metav1.Now()
+	build.Status = v1alpha1.BuildStatus{}
+	build.Status.Phase = v1alpha1.BuildPhaseInitial
+	build.Status.Failure.Recovery.Attempt++
+	build.Status.Failure.Recovery.AttemptTime = metav1.Now()
 
 	action.L.Infof("Recovery attempt (%d/%d)",
-		target.Status.Failure.Recovery.Attempt,
-		target.Status.Failure.Recovery.AttemptMax,
+		build.Status.Failure.Recovery.Attempt,
+		build.Status.Failure.Recovery.AttemptMax,
 	)
 
-	return action.client.Status().Update(ctx, target)
+	return build, nil
 }
diff --git a/pkg/controller/build/schedule_pod.go b/pkg/controller/build/schedule_pod.go
index 801d84cce..a5f38da45 100644
--- a/pkg/controller/build/schedule_pod.go
+++ b/pkg/controller/build/schedule_pod.go
@@ -61,7 +61,7 @@ func (action *schedulePodAction) CanHandle(build *v1alpha1.Build) bool {
 }
 
 // Handle handles the builds
-func (action *schedulePodAction) Handle(ctx context.Context, build *v1alpha1.Build) error {
+func (action *schedulePodAction) Handle(ctx context.Context, build *v1alpha1.Build) (*v1alpha1.Build, error) {
 	// Enter critical section
 	action.lock.Lock()
 	defer action.lock.Unlock()
@@ -72,7 +72,7 @@ func (action *schedulePodAction) Handle(ctx context.Context, build *v1alpha1.Bui
 	// atomically by write operations
 	err := action.reader.List(ctx, options, builds)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	// Emulate a serialized working queue to only allow one build to run at a given time.
@@ -87,13 +87,13 @@ func (action *schedulePodAction) Handle(ctx context.Context, build *v1alpha1.Bui
 
 	if hasScheduledBuild {
 		// Let's requeue the build in case one is already running
-		return nil
+		return nil, nil
 	}
 
 	// Try to get operator image name before starting the build
 	operatorImage, err := platform.GetCurrentOperatorImage(ctx, action.client)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	// Otherwise, let's create the build pod
@@ -103,29 +103,27 @@ func (action *schedulePodAction) Handle(ctx context.Context, build *v1alpha1.Bui
 
 	// Set the Build instance as the owner and controller
 	if err := controllerutil.SetControllerReference(build, pod, action.client.GetScheme()); err != nil {
-		return err
+		return nil, err
 	}
 
 	// Ensure service account is present
 	if err := action.ensureServiceAccount(ctx, pod); err != nil {
-		return errors.Wrap(err, "cannot ensure service account is present")
+		return nil, errors.Wrap(err, "cannot ensure service account is present")
 	}
 
 	err = action.client.Delete(ctx, pod)
 	if err != nil && !k8serrors.IsNotFound(err) {
-		return errors.Wrap(err, "cannot delete build pod")
+		return nil, errors.Wrap(err, "cannot delete build pod")
 	}
 
 	err = action.client.Create(ctx, pod)
 	if err != nil {
-		return errors.Wrap(err, "cannot create build pod")
+		return nil, errors.Wrap(err, "cannot create build pod")
 	}
 
-	target := build.DeepCopy()
-	target.Status.Phase = v1alpha1.BuildPhasePending
-	action.L.Info("Build state transition", "phase", target.Status.Phase)
+	build.Status.Phase = v1alpha1.BuildPhasePending
 
-	return action.client.Status().Update(ctx, target)
+	return build, nil
 }
 
 func (action *schedulePodAction) ensureServiceAccount(ctx context.Context, buildPod *corev1.Pod) error {
diff --git a/pkg/controller/build/schedule_routine.go b/pkg/controller/build/schedule_routine.go
index e171601fd..589b69d0e 100644
--- a/pkg/controller/build/schedule_routine.go
+++ b/pkg/controller/build/schedule_routine.go
@@ -56,7 +56,7 @@ func (action *scheduleRoutineAction) CanHandle(build *v1alpha1.Build) bool {
 }
 
 // Handle handles the builds
-func (action *scheduleRoutineAction) Handle(ctx context.Context, build *v1alpha1.Build) error {
+func (action *scheduleRoutineAction) Handle(ctx context.Context, build *v1alpha1.Build) (*v1alpha1.Build, error) {
 	// Enter critical section
 	action.lock.Lock()
 	defer action.lock.Unlock()
@@ -67,7 +67,7 @@ func (action *scheduleRoutineAction) Handle(ctx context.Context, build *v1alpha1
 	// atomically by write operations
 	err := action.reader.List(ctx, options, builds)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	// Emulate a serialized working queue to only allow one build to run at a given time.
@@ -82,7 +82,7 @@ func (action *scheduleRoutineAction) Handle(ctx context.Context, build *v1alpha1
 
 	if hasScheduledBuild {
 		// Let's requeue the build in case one is already running
-		return nil
+		return nil, nil
 	}
 
 	// Transition the build to running state
@@ -91,14 +91,14 @@ func (action *scheduleRoutineAction) Handle(ctx context.Context, build *v1alpha1
 	action.L.Info("Build state transition", "phase", target.Status.Phase)
 	err = action.client.Status().Update(ctx, target)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	// and run it asynchronously to avoid blocking the reconcile loop
 	action.routines.Store(build.Name, true)
 	go action.build(ctx, build)
 
-	return nil
+	return nil, nil
 }
 
 func (action *scheduleRoutineAction) build(ctx context.Context, build *v1alpha1.Build) {
diff --git a/pkg/controller/integration/action.go b/pkg/controller/integration/action.go
index 1fb2a02bb..b205756bf 100644
--- a/pkg/controller/integration/action.go
+++ b/pkg/controller/integration/action.go
@@ -28,6 +28,7 @@ import (
 // Action --
 type Action interface {
 	client.Injectable
+	log.Injectable
 
 	// a user friendly name for the action
 	Name() string
@@ -36,10 +37,7 @@ type Action interface {
 	CanHandle(integration *v1alpha1.Integration) bool
 
 	// executes the handling function
-	Handle(ctx context.Context, integration *v1alpha1.Integration) error
-
-	// Inject integration logger
-	InjectLogger(log.Logger)
+	Handle(ctx context.Context, integration *v1alpha1.Integration) (*v1alpha1.Integration, error)
 }
 
 type baseAction struct {
diff --git a/pkg/controller/integration/build_kit.go b/pkg/controller/integration/build_kit.go
index 0a207f620..eda6aa1ac 100644
--- a/pkg/controller/integration/build_kit.go
+++ b/pkg/controller/integration/build_kit.go
@@ -24,7 +24,6 @@ import (
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
 	"github.com/apache/camel-k/pkg/trait"
 	"github.com/apache/camel-k/pkg/util"
-	"github.com/apache/camel-k/pkg/util/digest"
 	"github.com/rs/xid"
 )
 
@@ -46,11 +45,11 @@ func (action *buildKitAction) CanHandle(integration *v1alpha1.Integration) bool
 		integration.Status.Phase == v1alpha1.IntegrationPhaseResolvingKit
 }
 
-func (action *buildKitAction) Handle(ctx context.Context, integration *v1alpha1.Integration) error {
+func (action *buildKitAction) Handle(ctx context.Context, integration *v1alpha1.Integration) (*v1alpha1.Integration, error) {
 	kit, err := LookupKitForIntegration(ctx, action.client, integration)
 	if err != nil {
 		//TODO: we may need to add a wait strategy, i.e give up after some time
-		return err
+		return nil, err
 	}
 
 	if kit != nil {
@@ -64,57 +63,37 @@ func (action *buildKitAction) Handle(ctx context.Context, integration *v1alpha1.
 				// We need to re-generate a kit or search for a new one that
 				// satisfies integrations needs so let's remove the association
 				// with a kit
-				target := integration.DeepCopy()
-				target.Status.Kit = ""
-				return action.client.Status().Update(ctx, target)
+				integration.Status.Kit = ""
+				return integration, nil
 			}
 		}
 
 		if kit.Status.Phase == v1alpha1.IntegrationKitPhaseError {
-			target := integration.DeepCopy()
-			target.Status.Image = kit.ImageForIntegration()
-			target.Status.Kit = kit.Name
-			target.Status.Phase = v1alpha1.IntegrationPhaseError
-
-			target.Status.Digest, err = digest.ComputeForIntegration(target)
-			if err != nil {
-				return err
-			}
-
-			action.L.Info("Integration state transition", "phase", target.Status.Phase)
+			integration.Status.Image = kit.ImageForIntegration()
+			integration.Status.Kit = kit.Name
+			integration.Status.Phase = v1alpha1.IntegrationPhaseError
 
-			return action.client.Status().Update(ctx, target)
+			return integration, nil
 		}
 
 		if kit.Status.Phase == v1alpha1.IntegrationKitPhaseReady {
-			target := integration.DeepCopy()
-			target.Status.Image = kit.ImageForIntegration()
-			target.Status.Kit = kit.Name
+			integration.Status.Image = kit.ImageForIntegration()
+			integration.Status.Kit = kit.Name
 
-			dgst, err := digest.ComputeForIntegration(target)
-			if err != nil {
-				return err
+			if _, err := trait.Apply(ctx, action.client, integration, kit); err != nil {
+				return nil, err
 			}
 
-			target.Status.Digest = dgst
-
-			if _, err := trait.Apply(ctx, action.client, target, kit); err != nil {
-				return err
-			}
-
-			action.L.Info("Integration state transition", "phase", target.Status.Phase)
-
-			return action.client.Status().Update(ctx, target)
+			return integration, nil
 		}
 
 		if integration.Status.Kit == "" {
-			// We need to set the kit
-			target := integration.DeepCopy()
-			target.Status.Kit = kit.Name
-			return action.client.Status().Update(ctx, target)
+			integration.Status.Kit = kit.Name
+
+			return integration, nil
 		}
 
-		return nil
+		return nil, nil
 	}
 
 	platformCtxName := fmt.Sprintf("kit-%s", xid.New())
@@ -137,13 +116,12 @@ func (action *buildKitAction) Handle(ctx context.Context, integration *v1alpha1.
 	}
 
 	if err := action.client.Create(ctx, &platformCtx); err != nil {
-		return err
+		return nil, err
 	}
 
 	// Set the kit name so the next handle loop, will fall through the
 	// same path as integration with a user defined kit
-	target := integration.DeepCopy()
-	target.Status.Kit = platformCtxName
+	integration.Status.Kit = platformCtxName
 
-	return action.client.Status().Update(ctx, target)
+	return integration, nil
 }
diff --git a/pkg/controller/integration/delete.go b/pkg/controller/integration/delete.go
index fc8bf92f5..c1d4ec7fd 100644
--- a/pkg/controller/integration/delete.go
+++ b/pkg/controller/integration/delete.go
@@ -52,43 +52,41 @@ func (action *deleteAction) CanHandle(integration *v1alpha1.Integration) bool {
 	return integration.Status.Phase == v1alpha1.IntegrationPhaseDeleting
 }
 
-func (action *deleteAction) Handle(ctx context.Context, integration *v1alpha1.Integration) error {
+func (action *deleteAction) Handle(ctx context.Context, integration *v1alpha1.Integration) (*v1alpha1.Integration, error) {
 	l := log.Log.ForIntegration(integration)
 
 	ok, err := finalizer.Exists(integration, finalizer.CamelIntegrationFinalizer)
 	if err != nil {
-		return err
+		return nil, err
 	}
 	if !ok {
-		return nil
+		return nil, nil
 	}
 
-	target := integration.DeepCopy()
-
 	// Select all resources created by this integration
 	selectors := []string{
 		fmt.Sprintf("camel.apache.org/integration=%s", integration.Name),
 	}
 
 	l.Info("Collecting resources to delete")
-	resources, err := kubernetes.LookUpResources(ctx, action.client, target.Namespace, selectors)
+	resources, err := kubernetes.LookUpResources(ctx, action.client, integration.Namespace, selectors)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	// If the ForegroundDeletion deletion is not set remove the finalizer and
 	// delete child resources from a dedicated goroutine
-	foreground, err := finalizer.Exists(target, finalizer.ForegroundDeletion)
+	foreground, err := finalizer.Exists(integration, finalizer.ForegroundDeletion)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	if !foreground {
 		//
 		// Async
 		//
-		if err := action.removeFinalizer(ctx, target); err != nil {
-			return err
+		if err := action.removeFinalizer(ctx, integration); err != nil {
+			return nil, err
 		}
 
 		go func() {
@@ -101,14 +99,14 @@ func (action *deleteAction) Handle(ctx context.Context, integration *v1alpha1.In
 		// Sync
 		//
 		if err := action.deleteChildResources(ctx, &l, resources); err != nil {
-			return err
+			return nil, err
 		}
-		if err = action.removeFinalizer(ctx, target); err != nil {
-			return err
+		if err = action.removeFinalizer(ctx, integration); err != nil {
+			return nil, err
 		}
 	}
 
-	return nil
+	return nil, nil
 }
 
 func (action *deleteAction) removeFinalizer(ctx context.Context, integration *v1alpha1.Integration) error {
diff --git a/pkg/controller/integration/deploy.go b/pkg/controller/integration/deploy.go
index cefdc7677..36c43ec46 100644
--- a/pkg/controller/integration/deploy.go
+++ b/pkg/controller/integration/deploy.go
@@ -43,24 +43,22 @@ func (action *deployAction) CanHandle(integration *v1alpha1.Integration) bool {
 	return integration.Status.Phase == v1alpha1.IntegrationPhaseDeploying
 }
 
-func (action *deployAction) Handle(ctx context.Context, integration *v1alpha1.Integration) error {
+func (action *deployAction) Handle(ctx context.Context, integration *v1alpha1.Integration) (*v1alpha1.Integration, error) {
 	if integration.Status.Kit == "" {
-		return errors.Errorf("no kit set on integration %s", integration.Name)
+		return nil, errors.Errorf("no kit set on integration %s", integration.Name)
 	}
 
 	kit, err := kubernetes.GetIntegrationKit(ctx, action.client, integration.Status.Kit, integration.Namespace)
 	if err != nil {
-		return errors.Wrapf(err, "unable to find integration kit %s, %s", integration.Status.Kit, err)
+		return nil, errors.Wrapf(err, "unable to find integration kit %s, %s", integration.Status.Kit, err)
 	}
 
 	if _, err := trait.Apply(ctx, action.client, integration, kit); err != nil {
-		return err
+		return nil, err
 	}
 
 	target := integration.DeepCopy()
 	target.Status.Phase = v1alpha1.IntegrationPhaseRunning
 
-	action.L.Info("Integration state transition", "phase", target.Status.Phase)
-
-	return action.client.Status().Update(ctx, target)
+	return integration, nil
 }
diff --git a/pkg/controller/integration/initialize.go b/pkg/controller/integration/initialize.go
index b84aa0ff5..8fed1e8ca 100644
--- a/pkg/controller/integration/initialize.go
+++ b/pkg/controller/integration/initialize.go
@@ -23,7 +23,6 @@ import (
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
 	"github.com/apache/camel-k/pkg/platform"
 	"github.com/apache/camel-k/pkg/trait"
-	"github.com/apache/camel-k/pkg/util/digest"
 )
 
 // NewInitializeAction creates a new initialize action
@@ -46,10 +45,10 @@ func (action *initializeAction) CanHandle(integration *v1alpha1.Integration) boo
 }
 
 // Handle handles the integrations
-func (action *initializeAction) Handle(ctx context.Context, integration *v1alpha1.Integration) error {
+func (action *initializeAction) Handle(ctx context.Context, integration *v1alpha1.Integration) (*v1alpha1.Integration, error) {
 	pl, err := platform.GetCurrentPlatform(ctx, action.client, integration.Namespace)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	// The integration platform needs to be ready before starting to create integrations
@@ -57,20 +56,11 @@ func (action *initializeAction) Handle(ctx context.Context, integration *v1alpha
 		action.L.Info("Waiting for the integration platform to be initialized")
 
 		if integration.Status.Phase != v1alpha1.IntegrationPhaseWaitingForPlatform {
-			target := integration.DeepCopy()
-			target.Status.Phase = v1alpha1.IntegrationPhaseWaitingForPlatform
-
-			action.L.Info("Integration state transition", "phase", target.Status.Phase)
-
-			return action.client.Status().Update(ctx, target)
+			integration.Status.Phase = v1alpha1.IntegrationPhaseWaitingForPlatform
+			return integration, nil
 		}
 
-		return nil
-	}
-
-	dgst, err := digest.ComputeForIntegration(integration)
-	if err != nil {
-		return err
+		return nil, nil
 	}
 
 	//
@@ -78,29 +68,22 @@ func (action *initializeAction) Handle(ctx context.Context, integration *v1alpha
 	// WaitingForPlatform phase
 	//
 	if integration.Status.Phase == v1alpha1.IntegrationPhaseWaitingForPlatform {
-		target := integration.DeepCopy()
-		target.Status.Phase = v1alpha1.IntegrationPhaseInitial
-		target.Status.Digest = dgst
+		integration.Status.Phase = v1alpha1.IntegrationPhaseInitial
 
-		return action.client.Status().Update(ctx, target)
+		return integration, nil
 	}
 
 	// better not changing the spec section of the target because it may be used for comparison by a
 	// higher level controller (e.g. Knative source controller)
 
-	target := integration.DeepCopy()
-
 	// execute custom initialization
-	if _, err := trait.Apply(ctx, action.client, target, nil); err != nil {
-		return err
+	if _, err := trait.Apply(ctx, action.client, integration, nil); err != nil {
+		return nil, err
 	}
 
-	target.Status.Phase = v1alpha1.IntegrationPhaseBuildingKit
-	target.Status.Digest = dgst
-	target.Status.Kit = integration.Spec.Kit
-	target.Status.Image = ""
-
-	action.L.Info("Integration state transition", "phase", target.Status.Phase)
+	integration.Status.Phase = v1alpha1.IntegrationPhaseBuildingKit
+	integration.Status.Kit = integration.Spec.Kit
+	integration.Status.Image = ""
 
-	return action.client.Status().Update(ctx, target)
+	return integration, nil
 }
diff --git a/pkg/controller/integration/integration_controller.go b/pkg/controller/integration/integration_controller.go
index 8fc4b5a26..e6042525b 100644
--- a/pkg/controller/integration/integration_controller.go
+++ b/pkg/controller/integration/integration_controller.go
@@ -19,6 +19,8 @@ package integration
 import (
 	"context"
 
+	"github.com/apache/camel-k/pkg/util/digest"
+
 	"k8s.io/apimachinery/pkg/api/errors"
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -209,9 +211,9 @@ func (r *ReconcileIntegration) Reconcile(request reconcile.Request) (reconcile.R
 	ctx := context.TODO()
 
 	// Fetch the Integration instance
-	instance := &v1alpha1.Integration{}
-	err := r.client.Get(ctx, request.NamespacedName, instance)
-	if err != nil {
+	var instance v1alpha1.Integration
+
+	if err := r.client.Get(ctx, request.NamespacedName, &instance); err != nil {
 		if errors.IsNotFound(err) {
 			// Request object not found, could have been deleted after reconcile request.
 			// Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
@@ -222,7 +224,7 @@ func (r *ReconcileIntegration) Reconcile(request reconcile.Request) (reconcile.R
 		return reconcile.Result{}, err
 	}
 
-	integrationActionPool := []Action{
+	actions := []Action{
 		NewInitializeAction(),
 		NewBuildKitAction(),
 		NewDeployAction(),
@@ -235,22 +237,59 @@ func (r *ReconcileIntegration) Reconcile(request reconcile.Request) (reconcile.R
 		instance.Status.Phase = v1alpha1.IntegrationPhaseDeleting
 	}
 
-	ilog := rlog.ForIntegration(instance)
-	for _, a := range integrationActionPool {
+	var targetPhase v1alpha1.IntegrationPhase
+	var err error
+
+	target := instance.DeepCopy()
+	targetLog := rlog.ForIntegration(target)
+
+	for _, a := range actions {
 		a.InjectClient(r.client)
-		a.InjectLogger(ilog)
-		if a.CanHandle(instance) {
-			ilog.Infof("Invoking action %s", a.Name())
-			if err := a.Handle(ctx, instance); err != nil {
-				if k8serrors.IsConflict(err) {
-					ilog.Error(err, "conflict")
-					return reconcile.Result{
-						Requeue: true,
-					}, nil
-				}
+		a.InjectLogger(targetLog)
+
+		if a.CanHandle(target) {
+			targetLog.Infof("Invoking action %s", a.Name())
+
+			phaseFrom := target.Status.Phase
 
+			target, err = a.Handle(ctx, target)
+			if err != nil {
 				return reconcile.Result{}, err
 			}
+
+			if target != nil {
+				dgst, err := digest.ComputeForIntegration(target)
+				if err != nil {
+					return reconcile.Result{}, err
+				}
+
+				target.Status.Digest = dgst
+
+				if err := r.client.Status().Update(ctx, target); err != nil {
+					if k8serrors.IsConflict(err) {
+						targetLog.Error(err, "conflict")
+						return reconcile.Result{
+							Requeue: true,
+						}, nil
+					}
+
+					return reconcile.Result{}, err
+				}
+
+				targetPhase = target.Status.Phase
+
+				if targetPhase != phaseFrom {
+					targetLog.Info(
+						"state transition",
+						"phase-from", phaseFrom,
+						"phase-to", target.Status.Phase,
+					)
+				}
+			}
+
+			// handle one action at time so the resource
+			// is always at its latest state
+			break
 		}
 	}
 
diff --git a/pkg/controller/integration/monitor.go b/pkg/controller/integration/monitor.go
index 6a630dae2..b5c99cb23 100644
--- a/pkg/controller/integration/monitor.go
+++ b/pkg/controller/integration/monitor.go
@@ -42,25 +42,21 @@ func (action *monitorAction) CanHandle(integration *v1alpha1.Integration) bool {
 		integration.Status.Phase == v1alpha1.IntegrationPhaseError
 }
 
-func (action *monitorAction) Handle(ctx context.Context, integration *v1alpha1.Integration) error {
-
+func (action *monitorAction) Handle(ctx context.Context, integration *v1alpha1.Integration) (*v1alpha1.Integration, error) {
 	hash, err := digest.ComputeForIntegration(integration)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	if hash != integration.Status.Digest {
 		action.L.Info("Integration needs a rebuild")
 
-		target := integration.DeepCopy()
-		target.Status.Digest = hash
-		target.Status.Phase = ""
-
-		action.L.Info("Integration state transition", "phase", target.Status.Phase)
+		integration.Status.Digest = hash
+		integration.Status.Phase = ""
 
-		return action.client.Status().Update(ctx, target)
+		return integration, nil
 	}
 
 	// TODO check also if deployment matches (e.g. replicas)
-	return nil
+	return nil, nil
 }
diff --git a/pkg/controller/integrationkit/action.go b/pkg/controller/integrationkit/action.go
index 69691bfb7..5dd5ecf7a 100644
--- a/pkg/controller/integrationkit/action.go
+++ b/pkg/controller/integrationkit/action.go
@@ -28,6 +28,7 @@ import (
 // Action --
 type Action interface {
 	client.Injectable
+	log.Injectable
 
 	// a user friendly name for the action
 	Name() string
@@ -36,10 +37,7 @@ type Action interface {
 	CanHandle(kit *v1alpha1.IntegrationKit) bool
 
 	// executes the handling function
-	Handle(ctx context.Context, kit *v1alpha1.IntegrationKit) error
-
-	// Inject integration logger
-	InjectLogger(log.Logger)
+	Handle(ctx context.Context, kit *v1alpha1.IntegrationKit) (*v1alpha1.IntegrationKit, error)
 }
 
 type baseAction struct {
diff --git a/pkg/controller/integrationkit/build.go b/pkg/controller/integrationkit/build.go
index 97b7cafa4..4fb448a8e 100644
--- a/pkg/controller/integrationkit/build.go
+++ b/pkg/controller/integrationkit/build.go
@@ -52,20 +52,20 @@ func (action *buildAction) CanHandle(kit *v1alpha1.IntegrationKit) bool {
 		kit.Status.Phase == v1alpha1.IntegrationKitPhaseBuildRunning
 }
 
-func (action *buildAction) Handle(ctx context.Context, kit *v1alpha1.IntegrationKit) error {
+func (action *buildAction) Handle(ctx context.Context, kit *v1alpha1.IntegrationKit) (*v1alpha1.IntegrationKit, error) {
 	if kit.Status.Phase == v1alpha1.IntegrationKitPhaseBuildSubmitted {
 		return action.handleBuildSubmitted(ctx, kit)
 	} else if kit.Status.Phase == v1alpha1.IntegrationKitPhaseBuildRunning {
 		return action.handleBuildRunning(ctx, kit)
 	}
 
-	return nil
+	return nil, nil
 }
 
-func (action *buildAction) handleBuildSubmitted(ctx context.Context, kit *v1alpha1.IntegrationKit) error {
+func (action *buildAction) handleBuildSubmitted(ctx context.Context, kit *v1alpha1.IntegrationKit) (*v1alpha1.IntegrationKit, error) {
 	build, err := kubernetes.GetBuild(ctx, action.client, kit.Name, kit.Namespace)
 	if err != nil && !k8serrors.IsNotFound(err) {
-		return err
+		return nil, err
 	}
 
 	if err != nil && k8serrors.IsNotFound(err) ||
@@ -75,11 +75,11 @@ func (action *buildAction) handleBuildSubmitted(ctx context.Context, kit *v1alph
 
 		env, err := trait.Apply(ctx, action.client, nil, kit)
 		if err != nil {
-			return err
+			return nil, err
 		}
 
 		if env.CamelCatalog == nil {
-			return errors.New("undefined camel catalog")
+			return nil, errors.New("undefined camel catalog")
 		}
 
 		build = &v1alpha1.Build{
@@ -104,97 +104,81 @@ func (action *buildAction) handleBuildSubmitted(ctx context.Context, kit *v1alph
 
 		// Set the integration kit instance as the owner and controller
 		if err := controllerutil.SetControllerReference(kit, build, action.client.GetScheme()); err != nil {
-			return err
+			return nil, err
 		}
 
 		err = action.client.Delete(ctx, build)
 		if err != nil && !k8serrors.IsNotFound(err) {
-			return errors.Wrap(err, "cannot delete build")
+			return nil, errors.Wrap(err, "cannot delete build")
 		}
 
 		err = action.client.Create(ctx, build)
 		if err != nil {
-			return errors.Wrap(err, "cannot create build")
+			return nil, errors.Wrap(err, "cannot create build")
 		}
 	}
 
 	if build.Status.Phase == v1alpha1.BuildPhaseRunning {
-		target := kit.DeepCopy()
-		target.Status.Phase = v1alpha1.IntegrationKitPhaseBuildRunning
-
-		action.L.Info("IntegrationKit state transition", "phase", target.Status.Phase)
-
-		return action.client.Status().Update(ctx, target)
+		kit.Status.Phase = v1alpha1.IntegrationKitPhaseBuildRunning
+		return kit, nil
 	}
 
-	return nil
+	return nil, nil
 }
 
-func (action *buildAction) handleBuildRunning(ctx context.Context, kit *v1alpha1.IntegrationKit) error {
+func (action *buildAction) handleBuildRunning(ctx context.Context, kit *v1alpha1.IntegrationKit) (*v1alpha1.IntegrationKit, error) {
 	build, err := kubernetes.GetBuild(ctx, action.client, kit.Name, kit.Namespace)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	switch build.Status.Phase {
-
 	case v1alpha1.BuildPhaseRunning:
 		action.L.Info("Build running")
-
 	case v1alpha1.BuildPhaseSucceeded:
-		target := kit.DeepCopy()
-
 		// we should ensure that the integration kit is still in the right phase,
 		// if not there is a chance that the kit has been modified by the user
-		if target.Status.Phase != v1alpha1.IntegrationKitPhaseBuildRunning {
-			return fmt.Errorf("found kit %s not in the expected phase (expectd=%s, found=%s)",
+		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,
 				string(v1alpha1.IntegrationKitPhaseBuildRunning),
-				string(target.Status.Phase),
+				string(kit.Status.Phase),
 			)
 		}
 
-		target.Status.BaseImage = build.Status.BaseImage
-		target.Status.Image = build.Status.Image
-		target.Status.PublicImage = build.Status.PublicImage
-		target.Status.Phase = v1alpha1.IntegrationKitPhaseReady
-		target.Status.Artifacts = make([]v1alpha1.Artifact, 0, len(build.Status.Artifacts))
+		kit.Status.BaseImage = build.Status.BaseImage
+		kit.Status.Image = build.Status.Image
+		kit.Status.PublicImage = build.Status.PublicImage
+		kit.Status.Phase = v1alpha1.IntegrationKitPhaseReady
+		kit.Status.Artifacts = make([]v1alpha1.Artifact, 0, len(build.Status.Artifacts))
 
 		for _, a := range build.Status.Artifacts {
 			// do not include artifact location
-			target.Status.Artifacts = append(target.Status.Artifacts, v1alpha1.Artifact{
+			kit.Status.Artifacts = append(kit.Status.Artifacts, v1alpha1.Artifact{
 				ID:       a.ID,
 				Location: "",
 				Target:   a.Target,
 			})
 		}
 
-		action.L.Info("IntegrationKit state transition", "phase", target.Status.Phase)
-		if err := action.client.Status().Update(ctx, target); err != nil {
-			return err
-		}
-
+		return kit, err
 	case v1alpha1.BuildPhaseError, v1alpha1.BuildPhaseInterrupted:
-		target := kit.DeepCopy()
-
 		// we should ensure that the integration kit is still in the right phase,
 		// if not there is a chance that the kit has been modified by the user
-		if target.Status.Phase != v1alpha1.IntegrationKitPhaseBuildRunning {
-			return fmt.Errorf("found kit %s not the an expected phase (expectd=%s, found=%s)",
+		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,
 				string(v1alpha1.IntegrationKitPhaseBuildRunning),
-				string(target.Status.Phase),
+				string(kit.Status.Phase),
 			)
 		}
 
 		// Let's copy the build failure to the integration kit status
-		target.Status.Failure = build.Status.Failure
-		target.Status.Phase = v1alpha1.IntegrationKitPhaseError
-
-		action.L.Error(fmt.Errorf(build.Status.Error), "IntegrationKit state transition", "phase", target.Status.Phase)
+		kit.Status.Failure = build.Status.Failure
+		kit.Status.Phase = v1alpha1.IntegrationKitPhaseError
 
-		return action.client.Status().Update(ctx, target)
+		return kit, nil
 	}
 
-	return nil
+	return nil, nil
 }
diff --git a/pkg/controller/integrationkit/initialize.go b/pkg/controller/integrationkit/initialize.go
index d90076bd2..405797f13 100644
--- a/pkg/controller/integrationkit/initialize.go
+++ b/pkg/controller/integrationkit/initialize.go
@@ -23,7 +23,6 @@ import (
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
 	"github.com/apache/camel-k/pkg/platform"
 	"github.com/apache/camel-k/pkg/trait"
-	"github.com/apache/camel-k/pkg/util/digest"
 )
 
 // NewInitializeAction creates a new initialization handling action for the kit
@@ -40,47 +39,33 @@ func (action *initializeAction) Name() string {
 }
 
 func (action *initializeAction) CanHandle(kit *v1alpha1.IntegrationKit) bool {
-	return kit.Status.Phase == ""
+	return kit.Status.Phase == v1alpha1.IntegrationKitPhaseInitial || kit.Status.Phase == v1alpha1.IntegrationKitPhaseWaitingForPlatform
 }
 
-func (action *initializeAction) Handle(ctx context.Context, kit *v1alpha1.IntegrationKit) error {
+func (action *initializeAction) Handle(ctx context.Context, kit *v1alpha1.IntegrationKit) (*v1alpha1.IntegrationKit, error) {
 	// The integration platform needs to be initialized before starting to create kits
-	if _, err := platform.GetCurrentPlatform(ctx, action.client, kit.Namespace); err != nil {
+	_, err := platform.GetCurrentPlatform(ctx, action.client, kit.Namespace)
+	if err != nil {
 		action.L.Info("Waiting for the integration platform to be initialized")
-		return nil
+		return nil, nil
 	}
 
-	target := kit.DeepCopy()
-
-	_, err := trait.Apply(ctx, action.client, nil, target)
+	_, err = trait.Apply(ctx, action.client, nil, kit)
 	if err != nil {
-		return err
-	}
-
-	// Updating the whole integration kit as it may have changed
-	action.L.Info("Updating IntegrationKit")
-	if err := action.client.Update(ctx, target); err != nil {
-		return err
+		return nil, err
 	}
 
-	if target.Spec.Image == "" {
+	if kit.Spec.Image == "" {
 		// by default the kit should be built
-		target.Status.Phase = v1alpha1.IntegrationKitPhaseBuildSubmitted
+		kit.Status.Phase = v1alpha1.IntegrationKitPhaseBuildSubmitted
 	} else {
 		// but in case it has been created from an image, mark the
 		// kit as ready
-		target.Status.Phase = v1alpha1.IntegrationKitPhaseReady
+		kit.Status.Phase = v1alpha1.IntegrationKitPhaseReady
 
 		// and set the image to be used
-		target.Status.Image = target.Spec.Image
-	}
-
-	dgst, err := digest.ComputeForIntegrationKit(target)
-	if err != nil {
-		return err
+		kit.Status.Image = kit.Spec.Image
 	}
-	target.Status.Digest = dgst
 
-	action.L.Info("IntegrationKit state transition", "phase", target.Status.Phase)
-	return action.client.Status().Update(ctx, target)
+	return kit, nil
 }
diff --git a/pkg/controller/integrationkit/integrationkit_controller.go b/pkg/controller/integrationkit/integrationkit_controller.go
index 57d580a70..bca22e2f7 100644
--- a/pkg/controller/integrationkit/integrationkit_controller.go
+++ b/pkg/controller/integrationkit/integrationkit_controller.go
@@ -19,6 +19,8 @@ package integrationkit
 import (
 	"context"
 
+	"github.com/apache/camel-k/pkg/util/digest"
+
 	"k8s.io/apimachinery/pkg/api/errors"
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -128,8 +130,7 @@ func (r *ReconcileIntegrationKit) Reconcile(request reconcile.Request) (reconcil
 	var instance v1alpha1.IntegrationKit
 
 	// Fetch the IntegrationKit instance
-	err := r.client.Get(ctx, request.NamespacedName, &instance)
-	if err != nil {
+	if err := r.client.Get(ctx, request.NamespacedName, &instance); err != nil {
 		if errors.IsNotFound(err) {
 			// Request object not found, could have been deleted after reconcile request.
 			// Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
@@ -140,28 +141,66 @@ func (r *ReconcileIntegrationKit) Reconcile(request reconcile.Request) (reconcil
 		return reconcile.Result{}, err
 	}
 
-	actionPool := []Action{
+	actions := []Action{
 		NewInitializeAction(),
 		NewBuildAction(),
 		NewMonitorAction(),
 	}
 
-	ilog := rlog.ForIntegrationKit(&instance)
-	for _, a := range actionPool {
+	var targetPhase v1alpha1.IntegrationKitPhase
+	var err error
+
+	target := instance.DeepCopy()
+	targetLog := rlog.ForIntegrationKit(target)
+
+	for _, a := range actions {
 		a.InjectClient(r.client)
-		a.InjectLogger(ilog)
-		if a.CanHandle(&instance) {
-			ilog.Infof("Invoking action %s", a.Name())
-			if err := a.Handle(ctx, &instance); err != nil {
-				if k8serrors.IsConflict(err) {
-					ilog.Error(err, "conflict")
-					return reconcile.Result{
-						Requeue: true,
-					}, nil
-				}
+		a.InjectLogger(targetLog)
 
+		if a.CanHandle(target) {
+			targetLog.Infof("Invoking action %s", a.Name())
+
+			phaseFrom := target.Status.Phase
+
+			target, err = a.Handle(ctx, target)
+			if err != nil {
 				return reconcile.Result{}, err
 			}
+
+			if target != nil {
+				dgst, err := digest.ComputeForIntegrationKit(target)
+				if err != nil {
+					return reconcile.Result{}, err
+				}
+
+				target.Status.Digest = dgst
+
+				err = r.client.Status().Update(ctx, target)
+				if err != nil {
+					if k8serrors.IsConflict(err) {
+						targetLog.Error(err, "conflict")
+						return reconcile.Result{
+							Requeue: true,
+						}, nil
+					}
+
+					return reconcile.Result{}, err
+				}
+
+				targetPhase = target.Status.Phase
+
+				if targetPhase != phaseFrom {
+					targetLog.Info(
+						"state transition",
+						"phase-from", phaseFrom,
+						"phase-to", target.Status.Phase,
+					)
+				}
+			}
+
+			// handle one action at time so the resource
+			// is always at its latest state
+			break
 		}
 	}
 
diff --git a/pkg/controller/integrationkit/monitor.go b/pkg/controller/integrationkit/monitor.go
index 79671e392..a3910b1ef 100644
--- a/pkg/controller/integrationkit/monitor.go
+++ b/pkg/controller/integrationkit/monitor.go
@@ -41,22 +41,19 @@ func (action *monitorAction) CanHandle(kit *v1alpha1.IntegrationKit) bool {
 	return kit.Status.Phase == v1alpha1.IntegrationKitPhaseReady || kit.Status.Phase == v1alpha1.IntegrationKitPhaseError
 }
 
-func (action *monitorAction) Handle(ctx context.Context, kit *v1alpha1.IntegrationKit) error {
+func (action *monitorAction) Handle(ctx context.Context, kit *v1alpha1.IntegrationKit) (*v1alpha1.IntegrationKit, error) {
 	hash, err := digest.ComputeForIntegrationKit(kit)
 	if err != nil {
-		return err
+		return nil, err
 	}
 	if hash != kit.Status.Digest {
 		action.L.Info("IntegrationKit needs a rebuild")
 
-		target := kit.DeepCopy()
-		target.Status.Digest = hash
-		target.Status.Phase = v1alpha1.IntegrationKitPhaseBuildSubmitted
+		kit.Status.Digest = hash
+		kit.Status.Phase = v1alpha1.IntegrationKitPhaseBuildSubmitted
 
-		action.L.Info("IntegrationKit state transition", "phase", target.Status.Phase)
-
-		return action.client.Status().Update(ctx, target)
+		return kit, nil
 	}
 
-	return nil
+	return nil, nil
 }
diff --git a/pkg/controller/integrationplatform/action.go b/pkg/controller/integrationplatform/action.go
index 11a4c597b..efa33a417 100644
--- a/pkg/controller/integrationplatform/action.go
+++ b/pkg/controller/integrationplatform/action.go
@@ -28,6 +28,7 @@ import (
 // Action --
 type Action interface {
 	client.Injectable
+	log.Injectable
 
 	// a user friendly name for the action
 	Name() string
@@ -36,10 +37,7 @@ type Action interface {
 	CanHandle(platform *v1alpha1.IntegrationPlatform) bool
 
 	// executes the handling function
-	Handle(ctx context.Context, platform *v1alpha1.IntegrationPlatform) error
-
-	// Inject integration logger
-	InjectLogger(log.Logger)
+	Handle(ctx context.Context, platform *v1alpha1.IntegrationPlatform) (*v1alpha1.IntegrationPlatform, error)
 }
 
 type baseAction struct {
diff --git a/pkg/controller/integrationplatform/create.go b/pkg/controller/integrationplatform/create.go
index 184cd8083..2e2bfa523 100644
--- a/pkg/controller/integrationplatform/create.go
+++ b/pkg/controller/integrationplatform/create.go
@@ -45,13 +45,13 @@ func (action *createAction) CanHandle(platform *v1alpha1.IntegrationPlatform) bo
 	return platform.Status.Phase == v1alpha1.IntegrationPlatformPhaseCreating
 }
 
-func (action *createAction) Handle(ctx context.Context, platform *v1alpha1.IntegrationPlatform) error {
+func (action *createAction) Handle(ctx context.Context, platform *v1alpha1.IntegrationPlatform) (*v1alpha1.IntegrationPlatform, error) {
 	for k := range deploy.Resources {
 		if strings.HasPrefix(k, "camel-catalog-") {
 			action.L.Infof("Installing camel catalog: %s", k)
 			err := install.Resources(ctx, action.client, platform.Namespace, install.IdentityResourceCustomizer, k)
 			if err != nil {
-				return err
+				return nil, err
 			}
 		}
 	}
@@ -75,14 +75,12 @@ func (action *createAction) Handle(ctx context.Context, platform *v1alpha1.Integ
 			action.L.Info("Installing custom platform resources")
 			err := install.Resources(ctx, action.client, platform.Namespace, install.IdentityResourceCustomizer, res...)
 			if err != nil {
-				return err
+				return nil, err
 			}
 		}
 	}
 
-	target := platform.DeepCopy()
-	target.Status.Phase = v1alpha1.IntegrationPlatformPhaseStarting
-	action.L.Info("IntegrationPlatform state transition", "phase", target.Status.Phase)
+	platform.Status.Phase = v1alpha1.IntegrationPlatformPhaseStarting
 
-	return action.client.Status().Update(ctx, target)
+	return platform, nil
 }
diff --git a/pkg/controller/integrationplatform/initialize.go b/pkg/controller/integrationplatform/initialize.go
index 7f52c5ef7..2c499496a 100644
--- a/pkg/controller/integrationplatform/initialize.go
+++ b/pkg/controller/integrationplatform/initialize.go
@@ -28,9 +28,10 @@ import (
 
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
 	"github.com/apache/camel-k/pkg/client"
-	"github.com/apache/camel-k/pkg/platform"
 	"github.com/apache/camel-k/pkg/util/defaults"
 	"github.com/apache/camel-k/pkg/util/openshift"
+
+	platformutil "github.com/apache/camel-k/pkg/platform"
 )
 
 // NewInitializeAction returns a action that initializes the platform configuration when not provided by the user
@@ -50,119 +51,115 @@ func (action *initializeAction) CanHandle(platform *v1alpha1.IntegrationPlatform
 	return platform.Status.Phase == "" || platform.Status.Phase == v1alpha1.IntegrationPlatformPhaseDuplicate
 }
 
-func (action *initializeAction) Handle(ctx context.Context, ip *v1alpha1.IntegrationPlatform) error {
-	target := ip.DeepCopy()
+func (action *initializeAction) Handle(ctx context.Context, platform *v1alpha1.IntegrationPlatform) (*v1alpha1.IntegrationPlatform, error) {
 
-	duplicate, err := action.isDuplicate(ctx, ip)
+	duplicate, err := action.isDuplicate(ctx, platform)
 	if err != nil {
-		return err
+		return nil, err
 	}
 	if duplicate {
 		// another platform already present in the namespace
-		if ip.Status.Phase != v1alpha1.IntegrationPlatformPhaseDuplicate {
-			target := ip.DeepCopy()
-			target.Status.Phase = v1alpha1.IntegrationPlatformPhaseDuplicate
-
-			action.L.Info("IntegrationPlatform state transition", "phase", target.Status.Phase)
+		if platform.Status.Phase != v1alpha1.IntegrationPlatformPhaseDuplicate {
+			platform := platform.DeepCopy()
+			platform.Status.Phase = v1alpha1.IntegrationPlatformPhaseDuplicate
 
-			return action.client.Status().Update(ctx, target)
+			return platform, nil
 		}
-		return nil
+
+		return nil, nil
 	}
 
 	// update missing fields in the resource
-	if target.Spec.Cluster == "" {
+	if platform.Spec.Cluster == "" {
 		// determine the kind of cluster the platform is installed into
 		isOpenShift, err := openshift.IsOpenShift(action.client)
 		switch {
 		case err != nil:
-			return err
+			return nil, err
 		case isOpenShift:
-			target.Spec.Cluster = v1alpha1.IntegrationPlatformClusterOpenShift
+			platform.Spec.Cluster = v1alpha1.IntegrationPlatformClusterOpenShift
 		default:
-			target.Spec.Cluster = v1alpha1.IntegrationPlatformClusterKubernetes
+			platform.Spec.Cluster = v1alpha1.IntegrationPlatformClusterKubernetes
 		}
 	}
 
-	if target.Spec.Build.PublishStrategy == "" {
-		if target.Spec.Cluster == v1alpha1.IntegrationPlatformClusterOpenShift {
-			target.Spec.Build.PublishStrategy = v1alpha1.IntegrationPlatformBuildPublishStrategyS2I
+	if platform.Spec.Build.PublishStrategy == "" {
+		if platform.Spec.Cluster == v1alpha1.IntegrationPlatformClusterOpenShift {
+			platform.Spec.Build.PublishStrategy = v1alpha1.IntegrationPlatformBuildPublishStrategyS2I
 		} else {
-			target.Spec.Build.PublishStrategy = v1alpha1.IntegrationPlatformBuildPublishStrategyKaniko
+			platform.Spec.Build.PublishStrategy = v1alpha1.IntegrationPlatformBuildPublishStrategyKaniko
 		}
 	}
 
-	if target.Spec.Build.BuildStrategy == "" {
+	if platform.Spec.Build.BuildStrategy == "" {
 		// If the operator is global, a global build strategy should be used
-		if platform.IsCurrentOperatorGlobal() {
+		if platformutil.IsCurrentOperatorGlobal() {
 			// The only global strategy we have for now
-			target.Spec.Build.BuildStrategy = v1alpha1.IntegrationPlatformBuildStrategyPod
+			platform.Spec.Build.BuildStrategy = v1alpha1.IntegrationPlatformBuildStrategyPod
 		} else {
-			if target.Spec.Build.PublishStrategy == v1alpha1.IntegrationPlatformBuildPublishStrategyKaniko {
+			if platform.Spec.Build.PublishStrategy == v1alpha1.IntegrationPlatformBuildPublishStrategyKaniko {
 				// The build output has to be shared with Kaniko via a persistent volume
-				target.Spec.Build.BuildStrategy = v1alpha1.IntegrationPlatformBuildStrategyPod
+				platform.Spec.Build.BuildStrategy = v1alpha1.IntegrationPlatformBuildStrategyPod
 			} else {
-				target.Spec.Build.BuildStrategy = v1alpha1.IntegrationPlatformBuildStrategyRoutine
+				platform.Spec.Build.BuildStrategy = v1alpha1.IntegrationPlatformBuildStrategyRoutine
 			}
 		}
 	}
 
-	if target.Spec.Build.PublishStrategy == v1alpha1.IntegrationPlatformBuildPublishStrategyKaniko && target.Spec.Build.Registry.Address == "" {
+	if platform.Spec.Build.PublishStrategy == v1alpha1.IntegrationPlatformBuildPublishStrategyKaniko && platform.Spec.Build.Registry.Address == "" {
 		action.L.Info("No registry specified for publishing images")
 	}
 
-	action.setDefaults(target)
+	action.setDefaults(platform)
 
-	if target.Spec.Build.Maven.Timeout.Duration != 0 {
-		action.L.Infof("Maven Timeout set to %s", target.Spec.Build.Maven.Timeout.Duration)
+	if platform.Spec.Build.Maven.Timeout.Duration != 0 {
+		action.L.Infof("Maven Timeout set to %s", platform.Spec.Build.Maven.Timeout.Duration)
 	}
 
-	err = action.client.Update(ctx, target)
+	err = action.client.Update(ctx, platform)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
-	if target.Spec.Build.PublishStrategy == v1alpha1.IntegrationPlatformBuildPublishStrategyKaniko {
+	if platform.Spec.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, target)
+		err := createPersistentVolumeClaim(ctx, action.client, platform)
 		if err != nil {
-			return err
+			return nil, err
 		}
 
 		// Check if the operator is running in the same namespace before starting the cache warmer
-		if target.Namespace == platform.GetOperatorNamespace() {
+		if platform.Namespace == platformutil.GetOperatorNamespace() {
 			// 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, target)
+			err = createKanikoCacheWarmerPod(ctx, action.client, platform)
 			if err != nil {
-				return err
+				return nil, err
 			}
 
-			target.Status.Phase = v1alpha1.IntegrationPlatformPhaseWarming
+			platform.Status.Phase = v1alpha1.IntegrationPlatformPhaseWarming
 		} else {
 			// Skip the warmer pod creation
-			target.Status.Phase = v1alpha1.IntegrationPlatformPhaseCreating
+			platform.Status.Phase = v1alpha1.IntegrationPlatformPhaseCreating
 		}
 
 	} else {
-		target.Status.Phase = v1alpha1.IntegrationPlatformPhaseCreating
+		platform.Status.Phase = v1alpha1.IntegrationPlatformPhaseCreating
 	}
 
-	// next phase
-	action.L.Info("IntegrationPlatform state transition", "phase", target.Status.Phase)
-	return action.client.Status().Update(ctx, target)
+	return platform, nil
 }
 
 func (action *initializeAction) isDuplicate(ctx context.Context, thisPlatform *v1alpha1.IntegrationPlatform) (bool, error) {
-	platforms, err := platform.ListPlatforms(ctx, action.client, thisPlatform.Namespace)
+	platforms, err := platformutil.ListPlatforms(ctx, action.client, thisPlatform.Namespace)
 	if err != nil {
 		return false, err
 	}
 	for _, p := range platforms.Items {
 		p := p // pin
-		if p.Name != thisPlatform.Name && platform.IsActive(&p) {
+		if p.Name != thisPlatform.Name && platformutil.IsActive(&p) {
 			return true, nil
 		}
 	}
@@ -170,59 +167,59 @@ func (action *initializeAction) isDuplicate(ctx context.Context, thisPlatform *v
 	return false, nil
 }
 
-func (action *initializeAction) setDefaults(target *v1alpha1.IntegrationPlatform) {
-	if target.Spec.Profile == "" {
-		target.Spec.Profile = platform.GetProfile(target)
+func (action *initializeAction) setDefaults(platform *v1alpha1.IntegrationPlatform) {
+	if platform.Spec.Profile == "" {
+		platform.Spec.Profile = platformutil.GetProfile(platform)
 	}
-	if target.Spec.Build.CamelVersion == "" {
-		target.Spec.Build.CamelVersion = defaults.CamelVersionConstraint
+	if platform.Spec.Build.CamelVersion == "" {
+		platform.Spec.Build.CamelVersion = defaults.CamelVersionConstraint
 	}
-	if target.Spec.Build.RuntimeVersion == "" {
-		target.Spec.Build.RuntimeVersion = defaults.RuntimeVersion
+	if platform.Spec.Build.RuntimeVersion == "" {
+		platform.Spec.Build.RuntimeVersion = defaults.RuntimeVersion
 	}
-	if target.Spec.Build.BaseImage == "" {
-		target.Spec.Build.BaseImage = defaults.BaseImage
+	if platform.Spec.Build.BaseImage == "" {
+		platform.Spec.Build.BaseImage = defaults.BaseImage
 	}
-	if target.Spec.Build.LocalRepository == "" {
-		target.Spec.Build.LocalRepository = defaults.LocalRepository
+	if platform.Spec.Build.LocalRepository == "" {
+		platform.Spec.Build.LocalRepository = defaults.LocalRepository
 	}
-	if target.Spec.Build.PersistentVolumeClaim == "" {
-		target.Spec.Build.PersistentVolumeClaim = target.Name
+	if platform.Spec.Build.PersistentVolumeClaim == "" {
+		platform.Spec.Build.PersistentVolumeClaim = platform.Name
 	}
 
-	if target.Spec.Build.Timeout.Duration != 0 {
-		d := target.Spec.Build.Timeout.Duration.Truncate(time.Second)
+	if platform.Spec.Build.Timeout.Duration != 0 {
+		d := platform.Spec.Build.Timeout.Duration.Truncate(time.Second)
 
-		if target.Spec.Build.Timeout.Duration != d {
-			action.L.Infof("Build timeout minimum unit is sec (configured: %s, truncated: %s)", target.Spec.Build.Timeout.Duration, d)
+		if platform.Spec.Build.Timeout.Duration != d {
+			action.L.Infof("Build timeout minimum unit is sec (configured: %s, truncated: %s)", platform.Spec.Build.Timeout.Duration, d)
 		}
 
-		target.Spec.Build.Timeout.Duration = d
+		platform.Spec.Build.Timeout.Duration = d
 	}
-	if target.Spec.Build.Timeout.Duration == 0 {
-		target.Spec.Build.Timeout.Duration = 5 * time.Minute
+	if platform.Spec.Build.Timeout.Duration == 0 {
+		platform.Spec.Build.Timeout.Duration = 5 * time.Minute
 	}
 
-	if target.Spec.Build.Maven.Timeout.Duration != 0 {
-		d := target.Spec.Build.Maven.Timeout.Duration.Truncate(time.Second)
+	if platform.Spec.Build.Maven.Timeout.Duration != 0 {
+		d := platform.Spec.Build.Maven.Timeout.Duration.Truncate(time.Second)
 
-		if target.Spec.Build.Maven.Timeout.Duration != d {
-			action.L.Infof("Maven timeout minimum unit is sec (configured: %s, truncated: %s)", target.Spec.Build.Maven.Timeout.Duration, d)
+		if platform.Spec.Build.Maven.Timeout.Duration != d {
+			action.L.Infof("Maven timeout minimum unit is sec (configured: %s, truncated: %s)", platform.Spec.Build.Maven.Timeout.Duration, d)
 		}
 
-		target.Spec.Build.Maven.Timeout.Duration = d
+		platform.Spec.Build.Maven.Timeout.Duration = d
 	}
-	if target.Spec.Build.Maven.Timeout.Duration == 0 {
-		n := target.Spec.Build.Timeout.Duration.Seconds() * 0.75
-		target.Spec.Build.Maven.Timeout.Duration = (time.Duration(n) * time.Second).Truncate(time.Second)
+	if platform.Spec.Build.Maven.Timeout.Duration == 0 {
+		n := platform.Spec.Build.Timeout.Duration.Seconds() * 0.75
+		platform.Spec.Build.Maven.Timeout.Duration = (time.Duration(n) * time.Second).Truncate(time.Second)
 	}
 
-	action.L.Infof("CamelVersion set to %s", target.Spec.Build.CamelVersion)
-	action.L.Infof("RuntimeVersion set to %s", target.Spec.Build.RuntimeVersion)
-	action.L.Infof("BaseImage set to %s", target.Spec.Build.BaseImage)
-	action.L.Infof("LocalRepository set to %s", target.Spec.Build.LocalRepository)
-	action.L.Infof("Timeout set to %s", target.Spec.Build.Timeout)
-	action.L.Infof("Maven Timeout set to %s", target.Spec.Build.Maven.Timeout.Duration)
+	action.L.Infof("CamelVersion set to %s", platform.Spec.Build.CamelVersion)
+	action.L.Infof("RuntimeVersion set to %s", platform.Spec.Build.RuntimeVersion)
+	action.L.Infof("BaseImage set to %s", platform.Spec.Build.BaseImage)
+	action.L.Infof("LocalRepository set to %s", platform.Spec.Build.LocalRepository)
+	action.L.Infof("Timeout set to %s", platform.Spec.Build.Timeout)
+	action.L.Infof("Maven Timeout set to %s", platform.Spec.Build.Maven.Timeout.Duration)
 }
 
 func createPersistentVolumeClaim(ctx context.Context, client client.Client, platform *v1alpha1.IntegrationPlatform) error {
diff --git a/pkg/controller/integrationplatform/integrationplatform_controller.go b/pkg/controller/integrationplatform/integrationplatform_controller.go
index 61425ab04..cc3e1dd1a 100644
--- a/pkg/controller/integrationplatform/integrationplatform_controller.go
+++ b/pkg/controller/integrationplatform/integrationplatform_controller.go
@@ -22,6 +22,7 @@ import (
 	"time"
 
 	camelv1alpha1 "github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+
 	"github.com/apache/camel-k/pkg/client"
 
 	"k8s.io/apimachinery/pkg/api/errors"
@@ -97,8 +98,8 @@ type ReconcileIntegrationPlatform struct {
 	scheme *runtime.Scheme
 }
 
-// Reconcile reads that state of the cluster for a IntegrationPlatform object and makes changes based on the state read
-// and what is in the IntegrationPlatform.Spec
+// Reconcile reads that state of the cluster for a IntegrationPlatform object and makes changes based
+// on the state read and what is in the IntegrationPlatform.Spec
 // Note:
 // The Controller will requeue the Request to be processed again if the returned error is non-nil or
 // Result.Requeue is true, otherwise upon completion it will remove the work from the queue.
@@ -109,12 +110,14 @@ func (r *ReconcileIntegrationPlatform) Reconcile(request reconcile.Request) (rec
 	ctx := context.TODO()
 
 	// Fetch the IntegrationPlatform instance
-	instance := &camelv1alpha1.IntegrationPlatform{}
-	err := r.client.Get(ctx, request.NamespacedName, instance)
-	if err != nil {
+	var instance camelv1alpha1.IntegrationPlatform
+
+	if err := r.client.Get(ctx, request.NamespacedName, &instance); err != nil {
 		if errors.IsNotFound(err) {
 			// Request object not found, could have been deleted after reconcile request.
-			// Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
+			// Owned objects are automatically garbage collected. For additional cleanup
+			// logic use finalizers.
+
 			// Return and don't requeue
 			return reconcile.Result{}, nil
 		}
@@ -122,40 +125,66 @@ func (r *ReconcileIntegrationPlatform) Reconcile(request reconcile.Request) (rec
 		return reconcile.Result{}, err
 	}
 
-	integrationPlatformActionPool := []Action{
+	actions := []Action{
 		NewInitializeAction(),
 		NewWarmAction(),
 		NewCreateAction(),
 		NewStartAction(),
 	}
 
-	ilog := rlog.ForIntegrationPlatform(instance)
-	for _, a := range integrationPlatformActionPool {
+	var targetPhase camelv1alpha1.IntegrationPlatformPhase
+	var err error
+
+	target := instance.DeepCopy()
+	targetLog := rlog.ForIntegrationPlatform(target)
+
+	for _, a := range actions {
 		a.InjectClient(r.client)
-		a.InjectLogger(ilog)
-		if a.CanHandle(instance) {
-			ilog.Infof("Invoking action %s", a.Name())
-			if err := a.Handle(ctx, instance); err != nil {
-				if k8serrors.IsConflict(err) {
-					ilog.Error(err, "conflict")
-					return reconcile.Result{
-						Requeue: true,
-					}, nil
-				}
+		a.InjectLogger(targetLog)
+
+		if a.CanHandle(target) {
+			targetLog.Infof("Invoking action %s", a.Name())
+
+			phaseFrom := target.Status.Phase
 
+			target, err = a.Handle(ctx, target)
+			if err != nil {
 				return reconcile.Result{}, err
 			}
-		}
-	}
 
-	// Fetch the IntegrationPlatform again and check the state
-	if err = r.client.Get(ctx, request.NamespacedName, instance); err != nil {
-		return reconcile.Result{}, err
+			if target != nil {
+				if err := r.client.Status().Update(ctx, target); err != nil {
+					if k8serrors.IsConflict(err) {
+						targetLog.Error(err, "conflict")
+						return reconcile.Result{
+							Requeue: true,
+						}, nil
+					}
+
+					return reconcile.Result{}, err
+				}
+
+				targetPhase = target.Status.Phase
+
+				if targetPhase != phaseFrom {
+					targetLog.Info(
+						"state transition",
+						"phase-from", phaseFrom,
+						"phase-to", target.Status.Phase,
+					)
+				}
+			}
+
+			// handle one action at time so the resource
+			// is always at its latest state
+			break
+		}
 	}
 
-	if instance.Status.Phase == camelv1alpha1.IntegrationPlatformPhaseReady {
+	if targetPhase == camelv1alpha1.IntegrationPlatformPhaseReady {
 		return reconcile.Result{}, nil
 	}
+
 	// Requeue
 	return reconcile.Result{
 		RequeueAfter: 5 * time.Second,
diff --git a/pkg/controller/integrationplatform/start.go b/pkg/controller/integrationplatform/start.go
index d1ca50f89..6d461262d 100644
--- a/pkg/controller/integrationplatform/start.go
+++ b/pkg/controller/integrationplatform/start.go
@@ -42,21 +42,19 @@ func (action *startAction) CanHandle(platform *v1alpha1.IntegrationPlatform) boo
 	return platform.Status.Phase == v1alpha1.IntegrationPlatformPhaseStarting || platform.Status.Phase == v1alpha1.IntegrationPlatformPhaseError
 }
 
-func (action *startAction) Handle(ctx context.Context, platform *v1alpha1.IntegrationPlatform) error {
+func (action *startAction) Handle(ctx context.Context, platform *v1alpha1.IntegrationPlatform) (*v1alpha1.IntegrationPlatform, error) {
 	aggregatePhase, err := action.aggregatePlatformPhaseFromContexts(ctx, platform.Namespace)
 	if err != nil {
-		return err
+		return nil, err
 	}
-	if platform.Status.Phase != aggregatePhase {
-		target := platform.DeepCopy()
-		target.Status.Phase = aggregatePhase
-
-		action.L.Info("IntegrationPlatform state transition", "phase", target.Status.Phase)
 
-		return action.client.Status().Update(ctx, target)
+	if platform.Status.Phase != aggregatePhase {
+		platform.Status.Phase = aggregatePhase
+		return platform, nil
 	}
+
 	// wait
-	return nil
+	return nil, nil
 }
 
 func (action *startAction) aggregatePlatformPhaseFromContexts(ctx context.Context, namespace string) (v1alpha1.IntegrationPlatformPhase, error) {
diff --git a/pkg/controller/integrationplatform/warm.go b/pkg/controller/integrationplatform/warm.go
index 853521f30..dceadb21e 100644
--- a/pkg/controller/integrationplatform/warm.go
+++ b/pkg/controller/integrationplatform/warm.go
@@ -45,7 +45,7 @@ func (action *warmAction) CanHandle(platform *v1alpha1.IntegrationPlatform) bool
 	return platform.Status.Phase == v1alpha1.IntegrationPlatformPhaseWarming
 }
 
-func (action *warmAction) Handle(ctx context.Context, platform *v1alpha1.IntegrationPlatform) error {
+func (action *warmAction) Handle(ctx context.Context, platform *v1alpha1.IntegrationPlatform) (*v1alpha1.IntegrationPlatform, error) {
 	// Check Kaniko warmer pod status
 	pod := corev1.Pod{
 		TypeMeta: metav1.TypeMeta{
@@ -60,24 +60,22 @@ func (action *warmAction) Handle(ctx context.Context, platform *v1alpha1.Integra
 
 	err := action.client.Get(ctx, types.NamespacedName{Namespace: pod.Namespace, Name: pod.Name}, &pod)
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	switch pod.Status.Phase {
 
 	case corev1.PodSucceeded:
 		action.L.Info("Kaniko cache successfully warmed up")
-		target := platform.DeepCopy()
-		target.Status.Phase = v1alpha1.IntegrationPlatformPhaseCreating
-		action.L.Info("IntegrationPlatform state transition", "phase", target.Status.Phase)
-		return action.client.Status().Update(ctx, target)
+		platform.Status.Phase = v1alpha1.IntegrationPlatformPhaseCreating
+		return platform, nil
 
 	case corev1.PodFailed:
-		return errors.New("failed to warm up Kaniko cache")
+		return nil, errors.New("failed to warm up Kaniko cache")
 
 	default:
 		action.L.Info("Waiting for Kaniko cache to warm up...")
 		// Requeue
-		return nil
+		return nil, nil
 	}
 }
diff --git a/pkg/util/log/log.go b/pkg/util/log/log.go
index ec095998f..d54900e56 100644
--- a/pkg/util/log/log.go
+++ b/pkg/util/log/log.go
@@ -35,6 +35,11 @@ func init() {
 	}
 }
 
+// Injectable identifies objects that can receive a Logger
+type Injectable interface {
+	InjectLogger(Logger)
+}
+
 // Logger --
 type Logger struct {
 	delegate logr.Logger


Test patch:
diff --git a/pkg/controller/integrationplatform/create_test.go b/pkg/controller/integrationplatform/create_test.go
new file mode 100644
index 000000000..ea9e1674d
--- /dev/null
+++ b/pkg/controller/integrationplatform/create_test.go
@@ -0,0 +1,74 @@
+/*
+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 integrationplatform
+
+import (
+	"context"
+	"strings"
+	"testing"
+
+	"github.com/apache/camel-k/deploy"
+
+	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+	"github.com/apache/camel-k/pkg/util/log"
+	"github.com/apache/camel-k/pkg/util/test"
+	"github.com/rs/xid"
+	"github.com/stretchr/testify/assert"
+	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+func TestCreate(t *testing.T) {
+	ip := v1alpha1.IntegrationPlatform{}
+	ip.Namespace = "ns"
+	ip.Name = xid.New().String()
+	ip.Spec.Cluster = v1alpha1.IntegrationPlatformClusterOpenShift
+
+	c, err := test.NewFakeClient(&ip)
+	assert.Nil(t, err)
+
+	h := NewCreateAction()
+	h.InjectLogger(log.Log)
+	h.InjectClient(c)
+
+	answer, err := h.Handle(context.TODO(), &ip)
+	assert.Nil(t, err)
+	assert.NotNil(t, answer)
+
+	list := v1alpha1.NewCamelCatalogList()
+	err = c.List(context.TODO(), &k8sclient.ListOptions{Namespace: ip.Namespace}, &list)
+
+	assert.Nil(t, err)
+	assert.NotEmpty(t, list.Items)
+
+	for k := range deploy.Resources {
+		if strings.HasPrefix(k, "camel-catalog-") {
+			found := false
+
+			for _, c := range list.Items {
+				n := strings.TrimSuffix(k, ".yaml")
+				n = strings.ToLower(n)
+
+				if c.Name == n {
+					found = true
+				}
+			}
+
+			assert.True(t, found)
+		}
+	}
+}
diff --git a/pkg/controller/integrationplatform/initialize_test.go b/pkg/controller/integrationplatform/initialize_test.go
index 2a1e4b049..bd181a78e 100644
--- a/pkg/controller/integrationplatform/initialize_test.go
+++ b/pkg/controller/integrationplatform/initialize_test.go
@@ -31,7 +31,6 @@ import (
 	"github.com/stretchr/testify/assert"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
 )
 
 func TestTimeouts_Default(t *testing.T) {
@@ -47,18 +46,9 @@ func TestTimeouts_Default(t *testing.T) {
 	h.InjectLogger(log.Log)
 	h.InjectClient(c)
 
-	err = h.Handle(context.TODO(), &ip)
-	assert.Nil(t, err)
-
-	key := k8sclient.ObjectKey{
-		Name:      ip.Name,
-		Namespace: ip.Namespace,
-	}
-
-	answer := v1alpha1.NewIntegrationPlatform(ip.Name, ip.Namespace)
-
-	err = c.Get(context.TODO(), key, &answer)
+	answer, err := h.Handle(context.TODO(), &ip)
 	assert.Nil(t, err)
+	assert.NotNil(t, answer)
 
 	n := answer.Spec.Build.Timeout.Duration.Seconds() * 0.75
 	d := (time.Duration(n) * time.Second).Truncate(time.Second)
@@ -87,18 +77,9 @@ func TestTimeouts_MavenComputedFromBuild(t *testing.T) {
 	h.InjectLogger(log.Log)
 	h.InjectClient(c)
 
-	err = h.Handle(context.TODO(), &ip)
-	assert.Nil(t, err)
-
-	key := k8sclient.ObjectKey{
-		Name:      ip.Name,
-		Namespace: ip.Namespace,
-	}
-
-	answer := v1alpha1.NewIntegrationPlatform(ip.Name, ip.Namespace)
-
-	err = c.Get(context.TODO(), key, &answer)
+	answer, err := h.Handle(context.TODO(), &ip)
 	assert.Nil(t, err)
+	assert.NotNil(t, answer)
 
 	n := answer.Spec.Build.Timeout.Duration.Seconds() * 0.75
 	d := (time.Duration(n) * time.Second).Truncate(time.Second)
@@ -134,18 +115,9 @@ func TestTimeouts_Truncated(t *testing.T) {
 	h.InjectLogger(log.Log)
 	h.InjectClient(c)
 
-	err = h.Handle(context.TODO(), &ip)
-	assert.Nil(t, err)
-
-	key := k8sclient.ObjectKey{
-		Name:      ip.Name,
-		Namespace: ip.Namespace,
-	}
-
-	answer := v1alpha1.NewIntegrationPlatform(ip.Name, ip.Namespace)
-
-	err = c.Get(context.TODO(), key, &answer)
+	answer, err := h.Handle(context.TODO(), &ip)
 	assert.Nil(t, err)
+	assert.NotNil(t, answer)
 
 	assert.Equal(t, 2*time.Minute, answer.Spec.Build.Maven.Timeout.Duration)
 	assert.Equal(t, 5*time.Minute, answer.Spec.Build.Timeout.Duration)
diff --git a/pkg/controller/integrationplatform/warm_test.go b/pkg/controller/integrationplatform/warm_test.go
new file mode 100644
index 000000000..160cbc63c
--- /dev/null
+++ b/pkg/controller/integrationplatform/warm_test.go
@@ -0,0 +1,128 @@
+/*
+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 integrationplatform
+
+import (
+	"context"
+	"testing"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+	"github.com/apache/camel-k/pkg/util/log"
+	"github.com/apache/camel-k/pkg/util/test"
+	"github.com/rs/xid"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestWarm_Succeeded(t *testing.T) {
+	ip := v1alpha1.IntegrationPlatform{}
+	ip.Namespace = "ns"
+	ip.Name = xid.New().String()
+	ip.Spec.Cluster = v1alpha1.IntegrationPlatformClusterOpenShift
+
+	pod := corev1.Pod{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: corev1.SchemeGroupVersion.String(),
+			Kind:       "Pod",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: ip.Namespace,
+			Name:      ip.Name + "-cache",
+		},
+		Status: corev1.PodStatus{
+			Phase: corev1.PodSucceeded,
+		},
+	}
+
+	c, err := test.NewFakeClient(&ip, &pod)
+	assert.Nil(t, err)
+
+	h := NewWarmAction()
+	h.InjectLogger(log.Log)
+	h.InjectClient(c)
+
+	answer, err := h.Handle(context.TODO(), &ip)
+	assert.Nil(t, err)
+	assert.NotNil(t, answer)
+}
+
+func TestWarm_Failing(t *testing.T) {
+	ip := v1alpha1.IntegrationPlatform{}
+	ip.Namespace = "ns"
+	ip.Name = xid.New().String()
+	ip.Spec.Cluster = v1alpha1.IntegrationPlatformClusterOpenShift
+
+	pod := corev1.Pod{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: corev1.SchemeGroupVersion.String(),
+			Kind:       "Pod",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: ip.Namespace,
+			Name:      ip.Name + "-cache",
+		},
+		Status: corev1.PodStatus{
+			Phase: corev1.PodFailed,
+		},
+	}
+
+	c, err := test.NewFakeClient(&ip, &pod)
+	assert.Nil(t, err)
+
+	h := NewWarmAction()
+	h.InjectLogger(log.Log)
+	h.InjectClient(c)
+
+	answer, err := h.Handle(context.TODO(), &ip)
+	assert.NotNil(t, err)
+	assert.Nil(t, answer)
+}
+
+func TestWarm_WarmingUp(t *testing.T) {
+	ip := v1alpha1.IntegrationPlatform{}
+	ip.Namespace = "ns"
+	ip.Name = xid.New().String()
+	ip.Spec.Cluster = v1alpha1.IntegrationPlatformClusterOpenShift
+
+	pod := corev1.Pod{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: corev1.SchemeGroupVersion.String(),
+			Kind:       "Pod",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: ip.Namespace,
+			Name:      ip.Name + "-cache",
+		},
+		Status: corev1.PodStatus{
+			Phase: corev1.PodRunning,
+		},
+	}
+
+	c, err := test.NewFakeClient(&ip, &pod)
+	assert.Nil(t, err)
+
+	h := NewWarmAction()
+	h.InjectLogger(log.Log)
+	h.InjectClient(c)
+
+	answer, err := h.Handle(context.TODO(), &ip)
+	assert.Nil(t, err)
+	assert.Nil(t, answer)
+}
diff --git a/pkg/util/test/client.go b/pkg/util/test/client.go
index 10c9e937c..a7588442a 100644
--- a/pkg/util/test/client.go
+++ b/pkg/util/test/client.go
@@ -56,5 +56,5 @@ type FakeClient struct {
 
 // GetScheme ---
 func (c *FakeClient) GetScheme() *runtime.Scheme {
-	return nil
+	return clientscheme.Scheme
 }