Repository: apache/camel-k
Pull request #2040 (2021-02-17 09:24:19)

feat(trait): toleration trait
<!-- Description -->
With this PR we introduce a new trait to include Tolerations configuration for an integration

<!--
Enter your extended release note in the below block. If the PR requires
additional action from users switching to the new release, include the string
"action required". If no release note is required, write "NONE". 

You can (optionally) mark this PR with labels "kind/bug" or "kind/feature" to make sure
the text is added to the right section of the release notes. 
-->

**Release Note**
```release-note
feat(trait): toleration trait
```



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe922e2c8..0edfab299 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,11 @@
 
 **Merged pull requests:**
 
+- chore\(api\): Add Scale methods to the Integration generated client API [\#2052](https://github.com/apache/camel-k/pull/2052) ([astefanutti](https://github.com/astefanutti))
+- For the moment lets remove absolute path [\#2049](https://github.com/apache/camel-k/pull/2049) ([oscerd](https://github.com/oscerd))
+- fix: Consider ObjectMeta field for deep derivative comparison before patching [\#2042](https://github.com/apache/camel-k/pull/2042) ([astefanutti](https://github.com/astefanutti))
+- fix\(pdb\): Add TypeMeta field to PodDisruptionBudget resource [\#2041](https://github.com/apache/camel-k/pull/2041) ([astefanutti](https://github.com/astefanutti))
+- feat: Use server-side apply to create and patch owned resources [\#2039](https://github.com/apache/camel-k/pull/2039) ([astefanutti](https://github.com/astefanutti))
 - chore\(ci\): Pin the Kubernetes version deployed by Kind [\#2037](https://github.com/apache/camel-k/pull/2037) ([astefanutti](https://github.com/astefanutti))
 - fix\(ci\): Use kubectl wait command to check Knative readiness [\#2036](https://github.com/apache/camel-k/pull/2036) ([astefanutti](https://github.com/astefanutti))
 - chore\(olm\): Update IntegrationPlatform creation step in CSV description [\#2034](https://github.com/apache/camel-k/pull/2034) ([astefanutti](https://github.com/astefanutti))
@@ -757,6 +762,7 @@
 **Merged pull requests:**
 
 - Fix \#1449: add support for modeline options [\#1455](https://github.com/apache/camel-k/pull/1455) ([nicolaferraro](https://github.com/nicolaferraro))
+- information to update the homebrew formula [\#1454](https://github.com/apache/camel-k/pull/1454) ([ipolyzos](https://github.com/ipolyzos))
 - Fix \#1450: fix servicemonitor roles on direct install [\#1453](https://github.com/apache/camel-k/pull/1453) ([nicolaferraro](https://github.com/nicolaferraro))
 - Helm fix [\#1448](https://github.com/apache/camel-k/pull/1448) ([nicolaferraro](https://github.com/nicolaferraro))
 
@@ -775,7 +781,6 @@
 
 **Merged pull requests:**
 
-- information to update the homebrew formula [\#1454](https://github.com/apache/camel-k/pull/1454) ([ipolyzos](https://github.com/ipolyzos))
 - Fix \#1446 and \#1395: completely update integration resource on redepl… [\#1447](https://github.com/apache/camel-k/pull/1447) ([nicolaferraro](https://github.com/nicolaferraro))
 - Fix \#1367: add more build events [\#1444](https://github.com/apache/camel-k/pull/1444) ([nicolaferraro](https://github.com/nicolaferraro))
 - Partially Fixes Camel 14995: generates master and 3/threescale traits docs. [\#1441](https://github.com/apache/camel-k/pull/1441) ([djencks](https://github.com/djencks))
@@ -1209,7 +1214,6 @@
 - Orphan generated XML DSL from OpenAPI JSON after deleting integration [\#839](https://github.com/apache/camel-k/issues/839)
 - All links of the documentation are missing spaces before and after displayed text [\#794](https://github.com/apache/camel-k/issues/794)
 - Update Camel K tagline in Kamel CLI [\#732](https://github.com/apache/camel-k/issues/732)
-- Review reset and delete with --all option for merging similar functions to one place  [\#598](https://github.com/apache/camel-k/issues/598)
 - Able to run maven build with debug and/or extended info [\#85](https://github.com/apache/camel-k/issues/85)
 - Remove references to host that did compilation in stacktraces [\#972](https://github.com/apache/camel-k/issues/972)
 - Integration fails with --dev mode [\#968](https://github.com/apache/camel-k/issues/968)
@@ -1513,6 +1517,7 @@
 - allow to configure default configurations on platform [\#608](https://github.com/apache/camel-k/issues/608)
 - Script set-version.sh does not honor the registry/repository settings [\#602](https://github.com/apache/camel-k/issues/602)
 - add options to configure affinity [\#601](https://github.com/apache/camel-k/issues/601)
+- Review reset and delete with --all option for merging similar functions to one place  [\#598](https://github.com/apache/camel-k/issues/598)
 - camel-k groovy, jvm, spring-boot gives error pushing image [\#588](https://github.com/apache/camel-k/issues/588)
 - on-premises: mount rwx volume [\#587](https://github.com/apache/camel-k/issues/587)
 - context with status error should not be included when looking up suitable context for an integration [\#584](https://github.com/apache/camel-k/issues/584)
@@ -1615,6 +1620,8 @@
 - feature\(kaniko\): support for incremental build [\#530](https://github.com/apache/camel-k/pull/530) ([lburgazzoli](https://github.com/lburgazzoli))
 - ordered delete [\#529](https://github.com/apache/camel-k/pull/529) ([lburgazzoli](https://github.com/lburgazzoli))
 - chore\(kamel\): fix reset command [\#526](https://github.com/apache/camel-k/pull/526) ([lburgazzoli](https://github.com/lburgazzoli))
+- chore\(release\): add automatic version update to release [\#524](https://github.com/apache/camel-k/pull/524) ([nicolaferraro](https://github.com/nicolaferraro))
+- Adding Probes to Knative services [\#514](https://github.com/apache/camel-k/pull/514) ([lburgazzoli](https://github.com/lburgazzoli))
 
 ## [0.3.0](https://github.com/apache/camel-k/tree/0.3.0) (2019-03-04)
 
@@ -1642,14 +1649,12 @@
 
 **Merged pull requests:**
 
-- chore\(release\): add automatic version update to release [\#524](https://github.com/apache/camel-k/pull/524) ([nicolaferraro](https://github.com/nicolaferraro))
 - fix: Allow file names as delete cmd argument [\#522](https://github.com/apache/camel-k/pull/522) ([christophd](https://github.com/christophd))
 - chore\(image\): include camel runtime adapters [\#521](https://github.com/apache/camel-k/pull/521) ([lburgazzoli](https://github.com/lburgazzoli))
 - test\(builder\): more assertions on deps [\#520](https://github.com/apache/camel-k/pull/520) ([oscerd](https://github.com/oscerd))
 - chore\(build\): remove jitpack configuration [\#519](https://github.com/apache/camel-k/pull/519) ([lburgazzoli](https://github.com/lburgazzoli))
 - chore\(runtime\): Bump runtime to 0.3.0 [\#518](https://github.com/apache/camel-k/pull/518) ([oscerd](https://github.com/oscerd))
 - knative\(serving\): add support for additional annotations [\#517](https://github.com/apache/camel-k/pull/517) ([lburgazzoli](https://github.com/lburgazzoli))
-- Adding Probes to Knative services [\#514](https://github.com/apache/camel-k/pull/514) ([lburgazzoli](https://github.com/lburgazzoli))
 - chore\(make\): fix images-push target [\#510](https://github.com/apache/camel-k/pull/510) ([lburgazzoli](https://github.com/lburgazzoli))
 - minikube doc update [\#509](https://github.com/apache/camel-k/pull/509) ([kameshsampath](https://github.com/kameshsampath))
 - chore\(crd\): add missing label to CamelCatalog CRD [\#502](https://github.com/apache/camel-k/pull/502) ([lburgazzoli](https://github.com/lburgazzoli))
diff --git a/deploy/traits.yaml b/deploy/traits.yaml
index f2936cc2a..ebe104d6d 100755
--- a/deploy/traits.yaml
+++ b/deploy/traits.yaml
@@ -633,6 +633,20 @@ traits:
   - name: description-path
     type: string
     description: The path where the Open-API specification is published (default `/openapi.json`)
+- name: toleration
+  platform: false
+  profiles:
+  - Kubernetes
+  - Knative
+  - OpenShift
+  description: 'This trait sets Tolerations over Integration pods. Tolerations allow (but do not require) the pods to schedule onto nodes with matching taints. See https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/ for more details. The toleration should be expressed in a similar manner of taints *_Key[=Value]:Effect[:Seconds]_* where values in square brackets are optional. Examples: node-role.kubernetes.io/master:NoSchedule node.kubernetes.io/network-unavailable:NoExecute:3000 disktype=ssd:PreferNoSchedule It''s disabled by default.'
+  properties:
+  - name: enabled
+    type: bool
+    description: Can be used to enable or disable a trait. All traits share this common property.
+  - name: taints
+    type: '[]string'
+    description: The taint to tolerate in the form Key[=Value]:Effect[:Seconds]
 - name: tracing
   platform: false
   profiles:
diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc
index fffc8c970..49610117a 100644
--- a/docs/modules/ROOT/nav.adoc
+++ b/docs/modules/ROOT/nav.adoc
@@ -59,5 +59,6 @@
 ** xref:traits:route.adoc[Route]
 ** xref:traits:service-binding.adoc[Service Binding]
 ** xref:traits:service.adoc[Service]
+** xref:traits:toleration.adoc[Toleration]
 ** xref:traits:tracing.adoc[Tracing]
 // End of autogenerated code - DO NOT EDIT! (trait-nav)
diff --git a/docs/modules/ROOT/pages/troubleshooting/debugging.adoc b/docs/modules/ROOT/pages/troubleshooting/debugging.adoc
index 3e229da17..6d08b42a2 100644
--- a/docs/modules/ROOT/pages/troubleshooting/debugging.adoc
+++ b/docs/modules/ROOT/pages/troubleshooting/debugging.adoc
@@ -5,7 +5,7 @@ Attaching a Java debugger to an integration is a common way to start the investi
 
 Even if Camel K integrations run on a Kubernetes cluster, it's very easy to attach a Java debugger to the remote integration using the CLI.
 
-If you're a VS Code user, you can watch this link:https://www.youtube.com/watch?v=cFPuxd1Je3c[video] or follow this link:https://camel.apache.org/blog/2021/01/DebugInVSCodeWithCamelK1.3.0/[blogpost].
+If you're a VS Code user, you can watch this link:https://www.youtube.com/watch?v=cFPuxd1Je3c[video].
 
 Find below a more generic explanation using IntelliJ.
 
diff --git a/docs/modules/traits/pages/toleration.adoc b/docs/modules/traits/pages/toleration.adoc
new file mode 100755
index 000000000..906745be0
--- /dev/null
+++ b/docs/modules/traits/pages/toleration.adoc
@@ -0,0 +1,43 @@
+= Toleration Trait
+
+// Start of autogenerated code - DO NOT EDIT! (description)
+This trait sets Tolerations over Integration pods. Tolerations allow (but do not require) the pods to schedule onto nodes with matching taints.
+See https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/ for more details.
+
+The toleration should be expressed in a similar manner of taints *_Key[=Value]:Effect[:Seconds]_* where values in square brackets are optional. Examples:
+
+node-role.kubernetes.io/master:NoSchedule
+node.kubernetes.io/network-unavailable:NoExecute:3000
+disktype=ssd:PreferNoSchedule
+
+It's disabled by default.
+
+
+This trait is available in the following profiles: **Kubernetes, Knative, OpenShift**.
+
+// End of autogenerated code - DO NOT EDIT! (description)
+// Start of autogenerated code - DO NOT EDIT! (configuration)
+== Configuration
+
+Trait properties can be specified when running any integration with the CLI:
+[source,console]
+----
+$ kamel run --trait toleration.[key]=[value] --trait toleration.[key2]=[value2] integration.groovy
+----
+The following configuration options are available:
+
+[cols="2m,1m,5a"]
+|===
+|Property | Type | Description
+
+| toleration.enabled
+| bool
+| Can be used to enable or disable a trait. All traits share this common property.
+
+| toleration.taints
+| []string
+| The taint to tolerate in the form Key[=Value]:Effect[:Seconds]
+
+|===
+
+// End of autogenerated code - DO NOT EDIT! (configuration)
diff --git a/pkg/apis/camel/v1/integration_types.go b/pkg/apis/camel/v1/integration_types.go
index 52a585368..678d0e61b 100644
--- a/pkg/apis/camel/v1/integration_types.go
+++ b/pkg/apis/camel/v1/integration_types.go
@@ -65,6 +65,8 @@ type IntegrationStatus struct {
 }
 
 // +genclient
+// +genclient:method=GetScale,verb=get,subresource=scale,result=k8s.io/api/autoscaling/v1.Scale
+// +genclient:method=UpdateScale,verb=update,subresource=scale,input=k8s.io/api/autoscaling/v1.Scale,result=k8s.io/api/autoscaling/v1.Scale
 // +kubebuilder:object:root=true
 // +kubebuilder:resource:path=integrations,scope=Namespaced,shortName=it,categories=kamel;camel
 // +kubebuilder:subresource:status
diff --git a/pkg/client/camel/clientset/versioned/typed/camel/v1/fake/fake_integration.go b/pkg/client/camel/clientset/versioned/typed/camel/v1/fake/fake_integration.go
index d05954441..7a976f609 100644
--- a/pkg/client/camel/clientset/versioned/typed/camel/v1/fake/fake_integration.go
+++ b/pkg/client/camel/clientset/versioned/typed/camel/v1/fake/fake_integration.go
@@ -23,6 +23,7 @@ import (
 	"context"
 
 	camelv1 "github.com/apache/camel-k/pkg/apis/camel/v1"
+	autoscalingv1 "k8s.io/api/autoscaling/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	labels "k8s.io/apimachinery/pkg/labels"
 	schema "k8s.io/apimachinery/pkg/runtime/schema"
@@ -141,3 +142,25 @@ func (c *FakeIntegrations) Patch(ctx context.Context, name string, pt types.Patc
 	}
 	return obj.(*camelv1.Integration), err
 }
+
+// GetScale takes name of the integration, and returns the corresponding scale object, and an error if there is any.
+func (c *FakeIntegrations) GetScale(ctx context.Context, integrationName string, options v1.GetOptions) (result *autoscalingv1.Scale, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewGetSubresourceAction(integrationsResource, c.ns, "scale", integrationName), &autoscalingv1.Scale{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*autoscalingv1.Scale), err
+}
+
+// UpdateScale takes the representation of a scale and updates it. Returns the server's representation of the scale, and an error, if there is any.
+func (c *FakeIntegrations) UpdateScale(ctx context.Context, integrationName string, scale *autoscalingv1.Scale, opts v1.UpdateOptions) (result *autoscalingv1.Scale, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateSubresourceAction(integrationsResource, "scale", c.ns, scale), &autoscalingv1.Scale{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*autoscalingv1.Scale), err
+}
diff --git a/pkg/client/camel/clientset/versioned/typed/camel/v1/integration.go b/pkg/client/camel/clientset/versioned/typed/camel/v1/integration.go
index 01e6a9d1c..47b7d98a1 100644
--- a/pkg/client/camel/clientset/versioned/typed/camel/v1/integration.go
+++ b/pkg/client/camel/clientset/versioned/typed/camel/v1/integration.go
@@ -25,6 +25,7 @@ import (
 
 	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
 	scheme "github.com/apache/camel-k/pkg/client/camel/clientset/versioned/scheme"
+	autoscalingv1 "k8s.io/api/autoscaling/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	types "k8s.io/apimachinery/pkg/types"
 	watch "k8s.io/apimachinery/pkg/watch"
@@ -48,6 +49,9 @@ type IntegrationInterface interface {
 	List(ctx context.Context, opts metav1.ListOptions) (*v1.IntegrationList, error)
 	Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error)
 	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.Integration, err error)
+	GetScale(ctx context.Context, integrationName string, options metav1.GetOptions) (*autoscalingv1.Scale, error)
+	UpdateScale(ctx context.Context, integrationName string, scale *autoscalingv1.Scale, opts metav1.UpdateOptions) (*autoscalingv1.Scale, error)
+
 	IntegrationExpansion
 }
 
@@ -194,3 +198,32 @@ func (c *integrations) Patch(ctx context.Context, name string, pt types.PatchTyp
 		Into(result)
 	return
 }
+
+// GetScale takes name of the integration, and returns the corresponding autoscalingv1.Scale object, and an error if there is any.
+func (c *integrations) GetScale(ctx context.Context, integrationName string, options metav1.GetOptions) (result *autoscalingv1.Scale, err error) {
+	result = &autoscalingv1.Scale{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("integrations").
+		Name(integrationName).
+		SubResource("scale").
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// UpdateScale takes the top resource name and the representation of a scale and updates it. Returns the server's representation of the scale, and an error, if there is any.
+func (c *integrations) UpdateScale(ctx context.Context, integrationName string, scale *autoscalingv1.Scale, opts metav1.UpdateOptions) (result *autoscalingv1.Scale, err error) {
+	result = &autoscalingv1.Scale{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("integrations").
+		Name(integrationName).
+		SubResource("scale").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(scale).
+		Do(ctx).
+		Into(result)
+	return
+}
diff --git a/pkg/cmd/completion_bash.go b/pkg/cmd/completion_bash.go
index 929ab0891..6d554c1ed 100644
--- a/pkg/cmd/completion_bash.go
+++ b/pkg/cmd/completion_bash.go
@@ -89,6 +89,32 @@ __kamel_deletion_policy() {
     COMPREPLY=( $( compgen -W "${type_list}" -- "$cur") )
 }
 
+__kamel_kubectl_get_servicebinding() {
+    local template
+    local template_gvkn
+    local kubectl_out
+    local service_names
+    local services_list
+
+    template="{{ range .items  }}{{ .metadata.name }} {{ end }}"
+    template_gvkn="{{ range .items  }}{{ .kind  }}/{{ .apiVersion  }}/{{ .metadata.name }} {{ end }}" 
+    if kubectl_out=$(kubectl get -o template --template="${template}" crd -l service.binding/provisioned-service=true 2>/dev/null); then
+        kubectl_out="${kubectl_out// /,}"
+        service_names="${kubectl_out}servicebinding"
+        if kubectl_out=$(kubectl get -o template --template="${template_gvkn}" ${service_names} 2>/dev/null); then
+            for resource in  $kubectl_out
+            do
+               name=$(echo ${resource} | cut -d'/' -f 4)
+               version=$(echo ${resource} | cut -d'/' -f 3)
+               group=$(echo ${resource} | cut -d'/' -f 2)
+               kind=$(echo ${resource} | cut -d'/' -f 1)
+               services_list="${services_list} ${kind}.${version}.${group}/${name}"
+            done
+            COMPREPLY=( $( compgen -W "${services_list[*]}" -- "$cur" ) )
+        fi
+    fi
+}
+
 __kamel_kubectl_get_configmap() {
     local template
     local kubectl_out
@@ -252,6 +278,13 @@ func configureKnownBashCompletions(command *cobra.Command) {
 			cobra.BashCompCustom: {"__kamel_deletion_policy"},
 		},
 	)
+	configureBashAnnotationForFlag(
+		command,
+		"connect",
+		map[string][]string{
+			cobra.BashCompCustom: {"__kamel_kubectl_get_servicebinding"},
+		},
+	)
 }
 
 func configureBashAnnotationForFlag(command *cobra.Command, flagName string, annotations map[string][]string) {
diff --git a/pkg/cmd/operator.go b/pkg/cmd/operator.go
index 276a1d425..f3d758e06 100644
--- a/pkg/cmd/operator.go
+++ b/pkg/cmd/operator.go
@@ -23,9 +23,10 @@ import (
 	"github.com/apache/camel-k/pkg/cmd/operator"
 )
 
+const operatorCommand = "operator"
+
 func newCmdOperator() (*cobra.Command, *operatorCmdOptions) {
-	options := operatorCmdOptions{
-	}
+	options := operatorCmdOptions{}
 
 	cmd := cobra.Command{
 		Use:     "operator",
diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go
index 0f10dc679..2c6b8b09a 100644
--- a/pkg/cmd/root.go
+++ b/pkg/cmd/root.go
@@ -189,10 +189,10 @@ func (command *RootCmdOptions) preRun(cmd *cobra.Command, _ []string) error {
 		// Check that the Kamel CLI matches that of the operator.
 		// The check relies on the version reported in the IntegrationPlatform status,
 		// which requires the operator is running and the IntegrationPlatform resource
-		// reconciled. Hence the compatibility check is skipped for the install command.
+		// reconciled. Hence the compatibility check is skipped for the install and the operator command.
 		// Furthermore, there can be any incompatibilities, as the install command deploys
 		// the operator version it's compatible with.
-		if cmd.Use != installCommand {
+		if cmd.Use != installCommand && cmd.Use != operatorCommand {
 			checkAndShowCompatibilityWarning(command.Context, c, command.Namespace)
 		}
 	}
diff --git a/pkg/trait/deployer.go b/pkg/trait/deployer.go
index cc7853700..a9a11d7b7 100644
--- a/pkg/trait/deployer.go
+++ b/pkg/trait/deployer.go
@@ -18,13 +18,17 @@ limitations under the License.
 package trait
 
 import (
+	"net/http"
+
 	"github.com/pkg/errors"
+
+	k8serrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
 
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
-	"github.com/apache/camel-k/pkg/util/kubernetes"
 	"github.com/apache/camel-k/pkg/util/patch"
 )
 
@@ -40,6 +44,8 @@ type deployerTrait struct {
 
 var _ ControllerStrategySelector = &deployerTrait{}
 
+var hasServerSideApply = true
+
 func newDeployerTrait() Trait {
 	return &deployerTrait{
 		BaseTrait: NewBaseTrait("deployer", 900),
@@ -62,15 +68,6 @@ func (t *deployerTrait) Configure(e *Environment) (bool, error) {
 func (t *deployerTrait) Apply(e *Environment) error {
 	switch e.Integration.Status.Phase {
 
-	case v1.IntegrationPhaseNone, v1.IntegrationPhaseWaitingForPlatform, v1.IntegrationPhaseInitialization, v1.IntegrationPhaseDeploying:
-		// Register a post action that updates the resources generated by the traits
-		e.PostActions = append(e.PostActions, func(env *Environment) error {
-			if err := kubernetes.ReplaceResources(env.C, env.Client, env.Resources.Items()); err != nil {
-				return errors.Wrap(err, "error during replace resource")
-			}
-			return nil
-		})
-
 	case v1.IntegrationPhaseBuildingKit, v1.IntegrationPhaseResolvingKit:
 		if e.IntegrationKitInPhase(v1.IntegrationKitPhaseReady) {
 			e.PostProcessors = append(e.PostProcessors, func(environment *Environment) error {
@@ -80,38 +77,30 @@ func (t *deployerTrait) Apply(e *Environment) error {
 			})
 		}
 
-	case v1.IntegrationPhaseRunning, v1.IntegrationPhaseWaitingForBindings:
+	case v1.IntegrationPhaseNone, v1.IntegrationPhaseInitialization,
+		v1.IntegrationPhaseWaitingForPlatform, v1.IntegrationPhaseWaitingForBindings,
+		v1.IntegrationPhaseDeploying, v1.IntegrationPhaseRunning:
 		// Register a post action that patches the resources generated by the traits
 		e.PostActions = append(e.PostActions, func(env *Environment) error {
 			for _, resource := range env.Resources.Items() {
-				key, err := client.ObjectKeyFromObject(resource)
-				if err != nil {
-					return err
-				}
-
-				object := resource.DeepCopyObject()
-				err = env.Client.Get(env.C, key, object)
-				if err != nil {
-					return err
-				}
-
-				if !patch.SpecEqualDeepDerivative(object, resource) {
-					// If both objects have a "Spec" field and it contains all expected fields
-					// (plus optional others), then avoid patching
-
-					p, err := patch.PositiveMergePatch(object, resource)
-					if err != nil {
-						return err
-					} else if len(p) == 0 {
-						// Avoid triggering a patch request for nothing
+				// We assume that server-side apply is enabled by default.
+				// It is currently convoluted to check pro-actively whether server-side apply
+				// is enabled. This is possible to fetch the OpenAPI endpoint, which returns
+				// the entire server API document, then lookup the resource PATCH endpoint, and
+				// check its list of accepted MIME types.
+				// As a simpler solution, we fallback to client-side apply at the first
+				// 415 error, and assume server-side apply is not available globally.
+				if hasServerSideApply {
+					if err := t.serverSideApply(env, resource); err == nil {
 						continue
-					}
-
-					err = env.Client.Patch(env.C, resource, client.RawPatch(types.MergePatchType, p))
-					if err != nil {
-						return errors.Wrap(err, "error during patch resource")
+					} else if isIncompatibleServerError(err) {
+						t.L.Info("Fallback to client-side apply to patch resources")
+						hasServerSideApply = false
 					}
 				}
+				if err := t.clientSideApply(env, resource); err != nil {
+					return err
+				}
 			}
 			return nil
 		})
@@ -120,6 +109,58 @@ func (t *deployerTrait) Apply(e *Environment) error {
 	return nil
 }
 
+func (t *deployerTrait) serverSideApply(env *Environment, resource runtime.Object) error {
+	target, err := patch.PositiveApplyPatch(resource)
+	if err != nil {
+		return err
+	}
+	err = env.Client.Patch(env.C, target, client.Apply, client.ForceOwnership, client.FieldOwner("camel-k-operator"))
+	if err != nil {
+		return errors.Wrapf(err, "error during apply resource: %v", resource)
+	}
+	return nil
+}
+
+func (t *deployerTrait) clientSideApply(env *Environment, resource runtime.Object) error {
+	err := env.Client.Create(env.C, resource)
+	if err == nil {
+		return nil
+	} else if !k8serrors.IsAlreadyExists(err) {
+		return errors.Wrapf(err, "error during create resource: %v", resource)
+	}
+	key, err := client.ObjectKeyFromObject(resource)
+	if err != nil {
+		return err
+	}
+	object := resource.DeepCopyObject()
+	err = env.Client.Get(env.C, key, object)
+	if err != nil {
+		return err
+	}
+	p, err := patch.PositiveMergePatch(object, resource)
+	if err != nil {
+		return err
+	} else if len(p) == 0 {
+		// Avoid triggering a patch request for nothing
+		return nil
+	}
+	err = env.Client.Patch(env.C, resource, client.RawPatch(types.MergePatchType, p))
+	if err != nil {
+		return errors.Wrapf(err, "error during patch resource: %v", resource)
+	}
+	return nil
+}
+
+func isIncompatibleServerError(err error) bool {
+	// 415: Unsupported media type means we're talking to a server which doesn't
+	// support server-side apply.
+	if _, ok := err.(*k8serrors.StatusError); !ok {
+		// Non-StatusError means the error isn't because the server is incompatible.
+		return false
+	}
+	return err.(*k8serrors.StatusError).Status().Code == http.StatusUnsupportedMediaType
+}
+
 func (t *deployerTrait) SelectControllerStrategy(e *Environment) (*ControllerStrategy, error) {
 	if t.Enabled != nil && !*t.Enabled {
 		return nil, nil
diff --git a/pkg/trait/openapi.go b/pkg/trait/openapi.go
index 99d94bc0e..ba23835d5 100644
--- a/pkg/trait/openapi.go
+++ b/pkg/trait/openapi.go
@@ -26,18 +26,18 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/apache/camel-k/pkg/util"
-	"github.com/apache/camel-k/pkg/util/digest"
 	"github.com/pkg/errors"
-	k8serrors "k8s.io/apimachinery/pkg/api/errors"
-	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	corev1 "k8s.io/api/core/v1"
+	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
-	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 
+	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
+	"github.com/apache/camel-k/pkg/util"
 	"github.com/apache/camel-k/pkg/util/defaults"
+	"github.com/apache/camel-k/pkg/util/digest"
 	"github.com/apache/camel-k/pkg/util/gzip"
 	"github.com/apache/camel-k/pkg/util/kubernetes"
 	"github.com/apache/camel-k/pkg/util/maven"
@@ -171,6 +171,8 @@ func (t *openAPITrait) generateOpenAPIConfigMap(e *Environment, resource v1.Reso
 		// ConfigMap already exists and matches the source
 		// Re-adding it to update its revision
 		cm.ResourceVersion = ""
+		// Clear the managed fields to support server-side apply
+		cm.ManagedFields = nil
 		e.Resources.Add(&cm)
 		return nil
 	}
diff --git a/pkg/trait/pdb.go b/pkg/trait/pdb.go
index de1131fe0..6569c93b3 100644
--- a/pkg/trait/pdb.go
+++ b/pkg/trait/pdb.go
@@ -85,6 +85,10 @@ func (t *pdbTrait) Apply(e *Environment) error {
 
 func (t *pdbTrait) podDisruptionBudgetFor(integration *v1.Integration) *v1beta1.PodDisruptionBudget {
 	pdb := &v1beta1.PodDisruptionBudget{
+		TypeMeta: metav1.TypeMeta{
+			Kind: "PodDisruptionBudget",
+			APIVersion: v1beta1.SchemeGroupVersion.String(),
+		},
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      integration.Name,
 			Namespace: integration.Namespace,
diff --git a/pkg/trait/toleration.go b/pkg/trait/toleration.go
new file mode 100644
index 000000000..700b1ea98
--- /dev/null
+++ b/pkg/trait/toleration.go
@@ -0,0 +1,153 @@
+/*
+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 trait
+
+import (
+	"fmt"
+	"regexp"
+	"strconv"
+
+	appsv1 "k8s.io/api/apps/v1"
+	"k8s.io/api/batch/v1beta1"
+	corev1 "k8s.io/api/core/v1"
+	serving "knative.dev/serving/pkg/apis/serving/v1"
+
+	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
+	"github.com/apache/camel-k/pkg/util"
+)
+
+// This trait sets Tolerations over Integration pods. Tolerations allow (but do not require) the pods to schedule onto nodes with matching taints.
+// See https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/ for more details.
+//
+// The toleration should be expressed in a similar manner of taints *_Key[=Value]:Effect[:Seconds]_* where values in square brackets are optional. Examples:
+//
+// node-role.kubernetes.io/master:NoSchedule
+// node.kubernetes.io/network-unavailable:NoExecute:3000
+// disktype=ssd:PreferNoSchedule
+//
+// It's disabled by default.
+//
+// +camel-k:trait=toleration
+type tolerationTrait struct {
+	BaseTrait `property:",squash"`
+	// The taint to tolerate in the form Key[=Value]:Effect[:Seconds]
+	Taints []string `property:"taints" json:"taints,omitempty"`
+}
+
+var validTaintRegexp = regexp.MustCompile(`^([\w\/_\-\.]+)(=)?([\w_\-\.]+)?:(NoSchedule|NoExecute|PreferNoSchedule):?(\d*)?$`)
+
+func newTolerationTrait() Trait {
+	return &tolerationTrait{
+		BaseTrait: NewBaseTrait("toleration", 1200),
+	}
+}
+
+func (t *tolerationTrait) Configure(e *Environment) (bool, error) {
+	if util.IsNilOrFalse(t.Enabled) {
+		return false, nil
+	}
+
+	if len(t.Taints) == 0 {
+		return false, fmt.Errorf("no taint was provided")
+	}
+
+	return e.IntegrationInPhase(v1.IntegrationPhaseDeploying, v1.IntegrationPhaseRunning), nil
+}
+
+func (t *tolerationTrait) Apply(e *Environment) (err error) {
+	tolerations, err := t.getTolerations()
+	if err != nil {
+		return err
+	}
+	var specTolerations *[]corev1.Toleration
+	found := false
+
+	// Deployment
+	deployment := e.Resources.GetDeployment(func(d *appsv1.Deployment) bool {
+		return d.Name == e.Integration.Name
+	})
+	if deployment != nil {
+		specTolerations = &deployment.Spec.Template.Spec.Tolerations
+		found = true
+	}
+
+	// Knative service
+	if !found {
+		knativeService := e.Resources.GetKnativeService(func(s *serving.Service) bool {
+			return s.Name == e.Integration.Name
+		})
+		if knativeService != nil {
+			specTolerations = &knativeService.Spec.Template.Spec.Tolerations
+			found = true
+		}
+	}
+
+	// Cronjob
+	if !found {
+		cronJob := e.Resources.GetCronJob(func(c *v1beta1.CronJob) bool {
+			return c.Name == e.Integration.Name
+		})
+		if cronJob != nil {
+			specTolerations = &cronJob.Spec.JobTemplate.Spec.Template.Spec.Tolerations
+			found = true
+		}
+	}
+
+	// Add the toleration
+	if found {
+		if *specTolerations == nil {
+			*specTolerations = make([]corev1.Toleration, 0)
+		}
+		*specTolerations = append(*specTolerations, tolerations...)
+	}
+
+	return nil
+}
+
+func (t *tolerationTrait) getTolerations() ([]corev1.Toleration, error) {
+	tolerations := make([]corev1.Toleration, 0)
+	for _, t := range t.Taints {
+		if !validTaintRegexp.MatchString(t) {
+			return nil, fmt.Errorf("could not match taint %v", t)
+		}
+		toleration := corev1.Toleration{}
+		// Parse the regexp groups
+		groups := validTaintRegexp.FindStringSubmatch(t)
+		toleration.Key = groups[1]
+		if groups[2] != "" {
+			toleration.Operator = corev1.TolerationOpEqual
+		} else {
+			toleration.Operator = corev1.TolerationOpExists
+		}
+		if groups[3] != "" {
+			toleration.Value = groups[3]
+		}
+		toleration.Effect = corev1.TaintEffect(groups[4])
+
+		if groups[5] != "" {
+			tolerationSeconds, err := strconv.ParseInt(groups[5], 10, 64)
+			if err != nil {
+				return nil, err
+			}
+			toleration.TolerationSeconds = &tolerationSeconds
+		}
+		tolerations = append(tolerations, toleration)
+	}
+
+	return tolerations, nil
+}
diff --git a/pkg/trait/trait_register.go b/pkg/trait/trait_register.go
index 1ec4e3127..4c06b3790 100644
--- a/pkg/trait/trait_register.go
+++ b/pkg/trait/trait_register.go
@@ -35,6 +35,7 @@ func init() {
 	AddToTraits(newDeploymentTrait)
 	AddToTraits(newGarbageCollectorTrait)
 	AddToTraits(newAffinityTrait)
+	AddToTraits(newTolerationTrait)
 	AddToTraits(newKnativeServiceTrait)
 	AddToTraits(newServiceTrait)
 	AddToTraits(newContainerTrait)
diff --git a/pkg/util/kubernetes/replace.go b/pkg/util/kubernetes/replace.go
index 55c8dcf78..a9f045d2f 100644
--- a/pkg/util/kubernetes/replace.go
+++ b/pkg/util/kubernetes/replace.go
@@ -20,27 +20,21 @@ package kubernetes
 import (
 	"context"
 
-	"github.com/apache/camel-k/pkg/client"
-	routev1 "github.com/openshift/api/route/v1"
 	"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"
 	"k8s.io/apimachinery/pkg/runtime"
-	serving "knative.dev/serving/pkg/apis/serving/v1"
+
 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
-)
 
-// ReplaceResources allows to completely replace a list of resources on Kubernetes, taking care of immutable fields and resource versions
-func ReplaceResources(ctx context.Context, c client.Client, objects []runtime.Object) error {
-	for _, object := range objects {
-		err := ReplaceResource(ctx, c, object)
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
+	serving "knative.dev/serving/pkg/apis/serving/v1"
+
+	routev1 "github.com/openshift/api/route/v1"
+
+	"github.com/apache/camel-k/pkg/client"
+)
 
 // ReplaceResource allows to completely replace a resource on Kubernetes, taking care of immutable fields and resource versions
 func ReplaceResource(ctx context.Context, c client.Client, res runtime.Object) error {
diff --git a/pkg/util/patch/patch.go b/pkg/util/patch/patch.go
index 67c2813cb..09c426542 100644
--- a/pkg/util/patch/patch.go
+++ b/pkg/util/patch/patch.go
@@ -21,7 +21,8 @@ import (
 	"reflect"
 
 	jsonpatch "github.com/evanphx/json-patch"
-	"k8s.io/apimachinery/pkg/api/equality"
+
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/json"
@@ -63,6 +64,26 @@ func PositiveMergePatch(source runtime.Object, target runtime.Object) ([]byte, e
 	return json.Marshal(positivePatch)
 }
 
+func PositiveApplyPatch(source runtime.Object) (runtime.Object, error) {
+	sourceJSON, err := json.Marshal(source)
+	if err != nil {
+		return nil, err
+	}
+
+	var positivePatch map[string]interface{}
+	err = json.Unmarshal(sourceJSON, &positivePatch)
+	if err != nil {
+		return nil, err
+	}
+
+	// The following is a work-around to remove null fields from the apply patch,
+	// so that ownership is not taken for non-managed fields.
+	// See https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/2155-clientgo-apply
+	removeNilValues(reflect.ValueOf(positivePatch), reflect.Value{})
+
+	return &unstructured.Unstructured{Object: positivePatch}, nil
+}
+
 func removeNilValues(v reflect.Value, parent reflect.Value) {
 	for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
 		v = v.Elem()
@@ -90,22 +111,3 @@ func removeNilValues(v reflect.Value, parent reflect.Value) {
 		}
 	}
 }
-
-func SpecEqualDeepDerivative(object runtime.Object, expected runtime.Object) (res bool) {
-	defer func() {
-		if r := recover(); r != nil {
-			res = false
-		}
-	}()
-
-	if expected == nil {
-		return true
-	} else if object == nil {
-		return false
-	}
-
-	objectSpec := reflect.ValueOf(object).Elem().FieldByName("Spec").Interface()
-	expectedSpec := reflect.ValueOf(expected).Elem().FieldByName("Spec").Interface()
-
-	return equality.Semantic.DeepDerivative(expectedSpec, objectSpec)
-}


Test patch:
diff --git a/e2e/common/run_test.go b/e2e/common/run_test.go
index 78e60a23b..de43ee6b2 100644
--- a/e2e/common/run_test.go
+++ b/e2e/common/run_test.go
@@ -24,13 +24,15 @@ package common
 import (
 	"testing"
 
-	. "github.com/apache/camel-k/e2e/support"
-	camelv1 "github.com/apache/camel-k/pkg/apis/camel/v1"
 	. "github.com/onsi/gomega"
+
 	v1 "k8s.io/api/core/v1"
+
+	. "github.com/apache/camel-k/e2e/support"
+	camelv1 "github.com/apache/camel-k/pkg/apis/camel/v1"
 )
 
-func TestRunExamplesFromGitHUB(t *testing.T) {
+func TestRunExamplesFromGitHub(t *testing.T) {
 	WithNewTestNamespace(t, func(ns string) {
 		Expect(Kamel("install", "-n", ns).Execute()).Should(BeNil())
 
@@ -73,5 +75,8 @@ func TestRunExamplesFromGitHUB(t *testing.T) {
 			Eventually(IntegrationLogs(ns, name), TestTimeoutShort).Should(ContainSubstring("Tick!"))
 			Expect(Kamel("delete", "--all", "-n", ns).Execute()).Should(BeNil())
 		})
+
+		// Cleanup
+		Expect(Kamel("delete", "--all", "-n", ns).Execute()).Should(BeNil())
 	})
 }
diff --git a/e2e/common/scale_test.go b/e2e/common/scale_test.go
new file mode 100644
index 000000000..cc77db126
--- /dev/null
+++ b/e2e/common/scale_test.go
@@ -0,0 +1,111 @@
+// +build integration
+
+// To enable compilation of this file in Goland, go to "Settings -> Go -> Vendoring & Build Tags -> Custom Tags" and add "knative"
+
+/*
+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 common
+
+import (
+	"testing"
+
+	. "github.com/onsi/gomega"
+	"github.com/onsi/gomega/gstruct"
+	"github.com/stretchr/testify/assert"
+
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+
+	"k8s.io/client-go/dynamic"
+	"k8s.io/client-go/restmapper"
+	"k8s.io/client-go/scale"
+
+	. "github.com/apache/camel-k/e2e/support"
+	camelv1 "github.com/apache/camel-k/pkg/apis/camel/v1"
+	"github.com/apache/camel-k/pkg/client/camel/clientset/versioned"
+)
+
+func TestIntegrationScale(t *testing.T) {
+	WithNewTestNamespace(t, func(ns string) {
+		name := "java"
+		Expect(Kamel("install", "-n", ns).Execute()).Should(BeNil())
+		Expect(Kamel("run", "-n", ns, "files/Java.java", "--name", name).Execute()).Should(BeNil())
+		Eventually(IntegrationPodPhase(ns, name), TestTimeoutLong).Should(Equal(v1.PodRunning))
+		Eventually(IntegrationCondition(ns, name, camelv1.IntegrationConditionReady), TestTimeoutShort).Should(Equal(v1.ConditionTrue))
+		Eventually(IntegrationLogs(ns, name), TestTimeoutShort).Should(ContainSubstring("Magicstring!"))
+
+		t.Run("Scale integration with polymorphic client", func(t *testing.T) {
+			// Polymorphic scale client
+			groupResources, err := restmapper.GetAPIGroupResources(TestClient().Discovery())
+			assert.Nil(t, err)
+			mapper := restmapper.NewDiscoveryRESTMapper(groupResources)
+			resolver := scale.NewDiscoveryScaleKindResolver(TestClient().Discovery())
+			scaleClient, err := scale.NewForConfig(TestClient().GetConfig(), mapper, dynamic.LegacyAPIPathResolverFunc, resolver)
+			assert.Nil(t, err)
+
+			// Patch the integration scale subresource
+			patch := "{\"spec\":{\"replicas\":2}}"
+			_, err = scaleClient.Scales(ns).Patch(TestContext, camelv1.SchemeGroupVersion.WithResource("integrations"), name, types.MergePatchType, []byte(patch), metav1.PatchOptions{})
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			// Check the Integration scale subresource Spec field
+			Eventually(IntegrationSpecReplicas(ns, name), TestTimeoutShort).
+				Should(gstruct.PointTo(BeNumerically("==", 2)))
+			// Then check it cascades into the Deployment scale
+			Eventually(IntegrationPods(ns, name), TestTimeoutMedium).Should(HaveLen(2))
+			// Finally check it cascades into the Integration scale subresource Status field
+			Eventually(IntegrationStatusReplicas(ns, name), TestTimeoutShort).
+				Should(gstruct.PointTo(BeNumerically("==", 2)))
+		})
+
+		t.Run("Scale integration with Camel K client", func(t *testing.T) {
+			camel, err := versioned.NewForConfig(TestClient().GetConfig())
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			// Getter
+			integrationScale, err := camel.CamelV1().Integrations(ns).GetScale(TestContext, name, metav1.GetOptions{})
+			Expect(integrationScale).ShouldNot(BeNil())
+			Expect(integrationScale.Spec.Replicas).Should(BeNumerically("==", 2))
+			Expect(integrationScale.Status.Replicas).Should(BeNumerically("==", 2))
+
+			// Setter
+			integrationScale.Spec.Replicas = 1
+			integrationScale, err = camel.CamelV1().Integrations(ns).UpdateScale(TestContext, name, integrationScale, metav1.UpdateOptions{})
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			// Check the Integration scale subresource Spec field
+			Eventually(IntegrationSpecReplicas(ns, name), TestTimeoutShort).
+				Should(gstruct.PointTo(BeNumerically("==", 1)))
+			// Then check it cascades into the Deployment scale
+			Eventually(IntegrationPods(ns, name), TestTimeoutMedium).Should(HaveLen(1))
+			// Finally check it cascades into the Integration scale subresource Status field
+			Eventually(IntegrationStatusReplicas(ns, name), TestTimeoutShort).
+				Should(gstruct.PointTo(BeNumerically("==", 1)))
+		})
+
+		// Cleanup
+		Expect(Kamel("delete", "--all", "-n", ns).Execute()).Should(BeNil())
+	})
+}
diff --git a/e2e/common/upgrade_test.go b/e2e/common/upgrade_test.go
index c747fb95a..00d1c22e2 100644
--- a/e2e/common/upgrade_test.go
+++ b/e2e/common/upgrade_test.go
@@ -34,6 +34,7 @@ import (
 func TestPlatformUpgrade(t *testing.T) {
 	WithNewTestNamespace(t, func(ns string) {
 		Expect(Kamel("install", "-n", ns).Execute()).Should(BeNil())
+		Eventually(OperatorPod(ns)).ShouldNot(BeNil())
 		Eventually(PlatformVersion(ns)).Should(Equal(defaults.Version))
 
 		// Scale the operator down to zero
diff --git a/e2e/support/test_support.go b/e2e/support/test_support.go
index 21a85d868..0849e7bb3 100644
--- a/e2e/support/test_support.go
+++ b/e2e/support/test_support.go
@@ -34,11 +34,10 @@ import (
 	"testing"
 	"time"
 
-	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
-	"github.com/apache/camel-k/pkg/util/kubernetes"
 	"github.com/google/uuid"
 	"github.com/onsi/gomega"
 	"github.com/spf13/cobra"
+
 	appsv1 "k8s.io/api/apps/v1"
 	"k8s.io/api/batch/v1beta1"
 	corev1 "k8s.io/api/core/v1"
@@ -46,6 +45,7 @@ import (
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
+
 	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
 
 	eventing "knative.dev/eventing/pkg/apis/eventing/v1beta1"
@@ -57,10 +57,12 @@ import (
 
 	"github.com/apache/camel-k/e2e/support/util"
 	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
+	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
 	"github.com/apache/camel-k/pkg/client"
 	"github.com/apache/camel-k/pkg/cmd"
 	"github.com/apache/camel-k/pkg/install"
 	"github.com/apache/camel-k/pkg/util/defaults"
+	"github.com/apache/camel-k/pkg/util/kubernetes"
 	"github.com/apache/camel-k/pkg/util/log"
 	"github.com/apache/camel-k/pkg/util/openshift"
 
@@ -260,6 +262,16 @@ func IntegrationPodImage(ns string, name string) func() string {
 
 func IntegrationPod(ns string, name string) func() *corev1.Pod {
 	return func() *corev1.Pod {
+		pods := IntegrationPods(ns, name)()
+		if len(pods) == 0 {
+			return nil
+		}
+		return &pods[0]
+	}
+}
+
+func IntegrationPods(ns string, name string) func() []corev1.Pod {
+	return func() []corev1.Pod {
 		lst := corev1.PodList{
 			TypeMeta: metav1.TypeMeta{
 				Kind:       "Pod",
@@ -274,10 +286,27 @@ func IntegrationPod(ns string, name string) func() *corev1.Pod {
 		if err != nil {
 			panic(err)
 		}
-		if len(lst.Items) == 0 {
+		return lst.Items
+	}
+}
+
+func IntegrationSpecReplicas(ns string, name string) func() *int32 {
+	return func() *int32 {
+		it := Integration(ns, name)()
+		if it == nil {
 			return nil
 		}
-		return &lst.Items[0]
+		return it.Spec.Replicas
+	}
+}
+
+func IntegrationStatusReplicas(ns string, name string) func() *int32 {
+	return func() *int32 {
+		it := Integration(ns, name)()
+		if it == nil {
+			return nil
+		}
+		return it.Status.Replicas
 	}
 }
 
@@ -858,7 +887,7 @@ func CreateKamelPod(ns string, name string, command ...string) error {
 	pod := corev1.Pod{
 		TypeMeta: metav1.TypeMeta{
 			Kind:       "Pod",
-			APIVersion: v1.SchemeGroupVersion.String(),
+			APIVersion: corev1.SchemeGroupVersion.String(),
 		},
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace: ns,
diff --git a/pkg/trait/toleration_test.go b/pkg/trait/toleration_test.go
new file mode 100644
index 000000000..b5b474135
--- /dev/null
+++ b/pkg/trait/toleration_test.go
@@ -0,0 +1,202 @@
+/*
+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 trait
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	serving "knative.dev/serving/pkg/apis/serving/v1"
+
+	appsv1 "k8s.io/api/apps/v1"
+	"k8s.io/api/batch/v1beta1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
+	"github.com/apache/camel-k/pkg/util"
+	"github.com/apache/camel-k/pkg/util/kubernetes"
+)
+
+func TestConfigureTolerationTraitMissingTaint(t *testing.T) {
+	environment, _ := createNominalDeploymentTolerationTest()
+	tolerationTrait := createNominalTolerationTrait()
+
+	success, err := tolerationTrait.Configure(environment)
+
+	assert.Equal(t, false, success)
+	assert.NotNil(t, err)
+}
+
+func TestApplyTolerationTraitMalformedTaint(t *testing.T) {
+	environment, _ := createNominalDeploymentTolerationTest()
+	tolerationTrait := createNominalTolerationTrait()
+	tolerationTrait.Taints = append(tolerationTrait.Taints, "my-toleration-failure")
+
+	err := tolerationTrait.Apply(environment)
+
+	assert.NotNil(t, err)
+}
+
+func TestApplyPodTolerationLabelsDefault(t *testing.T) {
+	tolerationTrait := createNominalTolerationTrait()
+	tolerationTrait.Taints = append(tolerationTrait.Taints, "my-toleration=my-value:NoExecute")
+
+	environment, deployment := createNominalDeploymentTolerationTest()
+	testApplyPodTolerationLabelsDefault(t, tolerationTrait, environment, &deployment.Spec.Template.Spec.Tolerations)
+
+	environment, knativeService := createNominalKnativeServiceTolerationTest()
+	testApplyPodTolerationLabelsDefault(t, tolerationTrait, environment, &knativeService.Spec.Template.Spec.Tolerations)
+
+	environment, cronJob := createNominalCronJobTolerationTest()
+	testApplyPodTolerationLabelsDefault(t, tolerationTrait, environment, &cronJob.Spec.JobTemplate.Spec.Template.Spec.Tolerations)
+}
+
+func testApplyPodTolerationLabelsDefault(t *testing.T, trait *tolerationTrait, environment *Environment, tolerations *[]corev1.Toleration) {
+	err := trait.Apply(environment)
+
+	assert.Nil(t, err)
+	assert.Equal(t, 1, len(*tolerations))
+	toleration := (*tolerations)[0]
+	assert.Equal(t, "my-toleration", toleration.Key)
+	assert.Equal(t, corev1.TolerationOpEqual, toleration.Operator)
+	assert.Equal(t, "my-value", toleration.Value)
+	assert.Equal(t, corev1.TaintEffectNoExecute, toleration.Effect)
+}
+
+func TestApplyPodTolerationLabelsTolerationSeconds(t *testing.T) {
+	tolerationTrait := createNominalTolerationTrait()
+	tolerationTrait.Taints = append(tolerationTrait.Taints, "my-toleration:NoExecute:300")
+
+	environment, deployment := createNominalDeploymentTolerationTest()
+	testApplyPodTolerationLabelsTolerationSeconds(t, tolerationTrait, environment, &deployment.Spec.Template.Spec.Tolerations)
+
+	environment, knativeService := createNominalKnativeServiceTolerationTest()
+	testApplyPodTolerationLabelsTolerationSeconds(t, tolerationTrait, environment, &knativeService.Spec.Template.Spec.Tolerations)
+
+	environment, cronJob := createNominalCronJobTolerationTest()
+	testApplyPodTolerationLabelsTolerationSeconds(t, tolerationTrait, environment, &cronJob.Spec.JobTemplate.Spec.Template.Spec.Tolerations)
+}
+
+func testApplyPodTolerationLabelsTolerationSeconds(t *testing.T, trait *tolerationTrait, environment *Environment, tolerations *[]corev1.Toleration) {
+	err := trait.Apply(environment)
+
+	assert.Nil(t, err)
+	assert.Equal(t, 1, len(*tolerations))
+	toleration := (*tolerations)[0]
+	assert.Equal(t, "my-toleration", toleration.Key)
+	assert.Equal(t, corev1.TolerationOpExists, toleration.Operator)
+	assert.Equal(t, corev1.TaintEffectNoExecute, toleration.Effect)
+	assert.Equal(t, int64(300), *toleration.TolerationSeconds)
+}
+
+func TestTolerationValidTaints(t *testing.T) {
+	environment, _ := createNominalDeploymentTolerationTest()
+	tolerationTrait := createNominalTolerationTrait()
+	tolerationTrait.Taints = append(tolerationTrait.Taints, "my-toleration:NoExecute")
+	tolerationTrait.Taints = append(tolerationTrait.Taints, "my-toleration:NoSchedule")
+	tolerationTrait.Taints = append(tolerationTrait.Taints, "my-toleration:PreferNoSchedule")
+	tolerationTrait.Taints = append(tolerationTrait.Taints, "my-toleration:PreferNoSchedule:100")
+	tolerationTrait.Taints = append(tolerationTrait.Taints, "my-toleration=my-val:NoExecute")
+	tolerationTrait.Taints = append(tolerationTrait.Taints, "my-toleration=my-val:NoExecute:120")
+	tolerationTrait.Taints = append(tolerationTrait.Taints, "org.apache.camel/my-toleration:NoExecute")
+	tolerationTrait.Taints = append(tolerationTrait.Taints, "org.apache.camel/my-toleration=val:NoExecute")
+
+	err := tolerationTrait.Apply(environment)
+
+	assert.Nil(t, err)
+}
+
+func createNominalDeploymentTolerationTest() (*Environment, *appsv1.Deployment) {
+	deployment := &appsv1.Deployment{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "integration-name",
+		},
+		Spec: appsv1.DeploymentSpec{
+			Template: corev1.PodTemplateSpec{},
+		},
+	}
+
+	environment := &Environment{
+		Integration: &v1.Integration{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "integration-name",
+			},
+			Status: v1.IntegrationStatus{
+				Phase: v1.IntegrationPhaseDeploying,
+			},
+		},
+		Resources: kubernetes.NewCollection(deployment),
+	}
+
+	return environment, deployment
+}
+
+func createNominalKnativeServiceTolerationTest() (*Environment, *serving.Service) {
+	knativeService := &serving.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "integration-name",
+		},
+		Spec: serving.ServiceSpec{},
+	}
+
+	environment := &Environment{
+		Integration: &v1.Integration{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "integration-name",
+			},
+			Status: v1.IntegrationStatus{
+				Phase: v1.IntegrationPhaseDeploying,
+			},
+		},
+		Resources: kubernetes.NewCollection(knativeService),
+	}
+
+	return environment, knativeService
+}
+
+func createNominalCronJobTolerationTest() (*Environment, *v1beta1.CronJob) {
+	cronJob := &v1beta1.CronJob{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "integration-name",
+		},
+		Spec: v1beta1.CronJobSpec{},
+	}
+
+	environment := &Environment{
+		Integration: &v1.Integration{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "integration-name",
+			},
+			Status: v1.IntegrationStatus{
+				Phase: v1.IntegrationPhaseDeploying,
+			},
+		},
+		Resources: kubernetes.NewCollection(cronJob),
+	}
+
+	return environment, cronJob
+}
+
+func createNominalTolerationTrait() *tolerationTrait {
+	tolerationTrait := newTolerationTrait().(*tolerationTrait)
+	tolerationTrait.Enabled = util.BoolP(true)
+	tolerationTrait.Taints = make([]string, 0)
+
+	return tolerationTrait
+}