You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@camel.apache.org by GitBox <gi...@apache.org> on 2018/10/08 14:19:37 UTC

[GitHub] lburgazzoli closed pull request #160: Add initial support for traits

lburgazzoli closed pull request #160: Add initial support for traits
URL: https://github.com/apache/camel-k/pull/160
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/README.adoc b/README.adoc
index ff41188..dd7d0d7 100644
--- a/README.adoc
+++ b/README.adoc
@@ -148,7 +148,7 @@ Camel K supports multiple languages for writing integrations:
 | Kotlin			| Kotlin Script `.kts` files are supported (experimental).
 |=======================
 
-More information about supported languages is provided in the link:docs/languages.adoc[lanuguages guide]
+More information about supported languages is provided in the link:docs/languages.adoc[lanuguages guide].
 
 Integrations written in different languages are provided in the link:/runtime/examples[examples] directory.
 
@@ -171,6 +171,11 @@ To run it, you need just to execute:
 kamel run runtime/examples/dns.js
 ```
 
+=== Traits
+
+The details of how the integration is mapped into Kubernetes resources can be *customized using traits*.
+More information is provided in the link:docs/traits.adoc[traits section].
+
 === Monitoring the Status
 
 Camel K integrations follow a lifecycle composed of several steps before getting into the `Running` state.
diff --git a/deploy/operator-role-openshift.yaml b/deploy/operator-role-openshift.yaml
index 1cd659b..788a7cc 100644
--- a/deploy/operator-role-openshift.yaml
+++ b/deploy/operator-role-openshift.yaml
@@ -105,3 +105,17 @@ rules:
   - builds/clone
   verbs:
   - create
+- apiGroups:
+  - ""
+  - "route.openshift.io"
+  resources:
+  - routes
+  verbs:
+  - create
+  - delete
+  - deletecollection
+  - get
+  - list
+  - patch
+  - update
+  - watch
diff --git a/deploy/resources.go b/deploy/resources.go
index b30cd40..c3ec245 100644
--- a/deploy/resources.go
+++ b/deploy/resources.go
@@ -2484,6 +2484,20 @@ rules:
   - builds/clone
   verbs:
   - create
+- apiGroups:
+  - ""
+  - "route.openshift.io"
+  resources:
+  - routes
+  verbs:
+  - create
+  - delete
+  - deletecollection
+  - get
+  - list
+  - patch
+  - update
+  - watch
 
 `
 	Resources["operator-service-account.yaml"] =
diff --git a/docs/traits.adoc b/docs/traits.adoc
new file mode 100644
index 0000000..c5ada79
--- /dev/null
+++ b/docs/traits.adoc
@@ -0,0 +1,88 @@
+[[traits]]
+= Traits
+
+Traits are high level named features of Camel K that can be enabled/disabled or configured to customize the
+behavior of the final integration.
+
+Camel K provide sensible defaults for all such traits, taking into account the details of the target platform where
+the integration is going to run into. However, it's possible for a **expert user** to configure them in
+order to obtain a different behavior.
+
+== Configuration
+
+Each trait has a unique ID that can be used to configure it using the command line tool.
+
+E.g. in order to disable the creation of a Service for a integration, a user can execute:
+
+```
+kamel run --trait service.enabled=false file.groovy
+```
+
+The flag `--trait` can be also abbreviated with `-t`.
+
+The `enabled` property is available on all traits and can be used to enable/disable them. All traits have their own
+internal logic to determine if they need to be enabled when the user does not activate them explicitly.
+
+NOTE: Some traits are applicable only to specific platforms (see "profiles" in the table).
+
+A trait may have additional properties that can be configured by the end user.
+
+E.g. the following command configures the container `port` that should be exposed by the service:
+
+```
+kamel run --trait service.enabled=true --trait service.port=8081 file.groovy
+```
+
+Or the equivalent command (assuming that the service trait is enabled by auto-detection):
+
+```
+kamel run -t service.port=8081 file.groovy
+```
+
+NOTE: Enabling a trait does not force the trait to be activated, especially if the trait specific preconditions do not hold.
+E.g. enabling the `route` trait while the `service` trait is disabled does not produce automatically a route, since a service is needed
+for the `route` trait to work.
+
+== Common Traits
+
+The following is a list of common traits that can be configured by the end users:
+
+[options="header",cols="1m,2,3a"]
+|=======================
+| Trait      | Profiles 				| Description
+
+| service
+| Kubernetes, OpenShift
+| Exposes the integration with a Service resource so that it can be accessed by other applications (or integrations) in the same namespace.
+  +
+  +
+  It's enabled by default if the integration depends on a Camel component that can expose a HTTP endpoint.
+
+[cols="m,"]
+!===
+
+! service.port
+! To configure a different port exposed by the container (default `8080`).
+
+!===
+
+| route
+| OpenShift
+| Exposes the service associated with the integration to the outside world with a OpenShift Route.
+  +
+  +
+  It's enabled by default whenever a Service is added to the integration (through the `service` trait).
+
+|=======================
+
+
+== Platform Traits (Advanced)
+
+There are also platform traits that **normally should not be configured** by the end user. So change them **only if you know what you're doing**.
+
+[options="header",cols="m,,"]
+|=======================
+| Trait      | Profiles 				| Description
+| base		 | Kubernetes, OpenShift	| Creates the basic Kubernetes resource needed for running the integration.
+| owner      | Kubernetes, OpenShift	| Makes sure that every resource created by the traits belongs to the integration custom resource (so they are deleted when the integration is deleted).
+|=======================
diff --git a/pkg/apis/camel/v1alpha1/types.go b/pkg/apis/camel/v1alpha1/types.go
index 114942f..61141af 100644
--- a/pkg/apis/camel/v1alpha1/types.go
+++ b/pkg/apis/camel/v1alpha1/types.go
@@ -48,12 +48,13 @@ type Integration struct {
 
 // IntegrationSpec --
 type IntegrationSpec struct {
-	Replicas                  *int32              `json:"replicas,omitempty"`
-	Source                    SourceSpec          `json:"source,omitempty"`
-	Context                   string              `json:"context,omitempty"`
-	Dependencies              []string            `json:"dependencies,omitempty"`
-	DependenciesAutoDiscovery *bool               `json:"dependenciesAutoDiscovery,omitempty"`
-	Configuration             []ConfigurationSpec `json:"configuration,omitempty"`
+	Replicas                  *int32                          `json:"replicas,omitempty"`
+	Source                    SourceSpec                      `json:"source,omitempty"`
+	Context                   string                          `json:"context,omitempty"`
+	Dependencies              []string                        `json:"dependencies,omitempty"`
+	Traits                    map[string]IntegrationTraitSpec `json:"traits,omitempty"`
+	DependenciesAutoDiscovery *bool                           `json:"dependenciesAutoDiscovery,omitempty"`
+	Configuration             []ConfigurationSpec             `json:"configuration,omitempty"`
 }
 
 // SourceSpec --
@@ -81,6 +82,12 @@ const (
 	LanguageKotlin Language = "kts"
 )
 
+// A IntegrationTraitSpec contains the configuration of a trait
+type IntegrationTraitSpec struct {
+	Enabled       *bool             `json:"enabled,omitempty"`
+	Configuration map[string]string `json:"configuration,omitempty"`
+}
+
 // IntegrationStatus --
 type IntegrationStatus struct {
 	Phase  IntegrationPhase `json:"phase,omitempty"`
diff --git a/pkg/client/cmd/run.go b/pkg/client/cmd/run.go
index 10e75ed..30f6fa8 100644
--- a/pkg/client/cmd/run.go
+++ b/pkg/client/cmd/run.go
@@ -22,6 +22,7 @@ import (
 	"io/ioutil"
 	"net/http"
 	"os"
+	"regexp"
 	"strconv"
 	"strings"
 
@@ -43,6 +44,10 @@ import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
+var (
+	traitConfigRegexp = regexp.MustCompile("^([a-z-]+)((?:\\.[a-z-]+)+)=(.*)$")
+)
+
 func newCmdRun(rootCmdOptions *RootCmdOptions) *cobra.Command {
 	options := runCmdOptions{
 		RootCmdOptions: rootCmdOptions,
@@ -69,6 +74,7 @@ func newCmdRun(rootCmdOptions *RootCmdOptions) *cobra.Command {
 	cmd.Flags().BoolVar(&options.Sync, "sync", false, "Synchronize the local source file with the cluster, republishing at each change")
 	cmd.Flags().BoolVar(&options.Dev, "dev", false, "Enable Dev mode (equivalent to \"-w --logs --sync\")")
 	cmd.Flags().BoolVar(&options.DependenciesAutoDiscovery, "auto-discovery", true, "Automatically discover Camel modules by analyzing user code")
+	cmd.Flags().StringSliceVarP(&options.Traits, "trait", "t", nil, "Configure a trait. E.g. \"-t service.enabled=false\"")
 
 	// completion support
 	configureKnownCompletions(&cmd)
@@ -91,6 +97,7 @@ type runCmdOptions struct {
 	Sync                      bool
 	Dev                       bool
 	DependenciesAutoDiscovery bool
+	Traits                    []string
 }
 
 func (*runCmdOptions) validateArgs(cmd *cobra.Command, args []string) error {
@@ -318,6 +325,12 @@ func (o *runCmdOptions) updateIntegrationCode(filename string) (*v1alpha1.Integr
 		})
 	}
 
+	for _, traitConf := range o.Traits {
+		if err := o.configureTrait(&integration, traitConf); err != nil {
+			return nil, err
+		}
+	}
+
 	existed := false
 	err = sdk.Create(&integration)
 	if err != nil && k8serrors.IsAlreadyExists(err) {
@@ -361,3 +374,35 @@ func (*runCmdOptions) loadCode(fileName string) (string, error) {
 	bodyString := string(bodyBytes)
 	return string(bodyString), err
 }
+
+func (*runCmdOptions) configureTrait(integration *v1alpha1.Integration, config string) error {
+	if integration.Spec.Traits == nil {
+		integration.Spec.Traits = make(map[string]v1alpha1.IntegrationTraitSpec)
+	}
+
+	parts := traitConfigRegexp.FindStringSubmatch(config)
+	if len(parts) < 4 {
+		return errors.New("unrecognized config format (expected \"<trait>.<prop>=<val>\"): " + config)
+	}
+	traitID := parts[1]
+	prop := parts[2][1:]
+	val := parts[3]
+	var spec v1alpha1.IntegrationTraitSpec
+	var ok bool
+	if spec, ok = integration.Spec.Traits[traitID]; !ok {
+		spec = v1alpha1.IntegrationTraitSpec{
+			Configuration: make(map[string]string),
+		}
+	}
+	if prop == "enabled" {
+		boolVal, err := strconv.ParseBool(val)
+		if err != nil {
+			return errors.Wrap(err, "cannot parse bool value "+val)
+		}
+		spec.Enabled = &boolVal
+	} else {
+		spec.Configuration[prop] = val
+	}
+	integration.Spec.Traits[traitID] = spec
+	return nil
+}
diff --git a/pkg/stub/action/integration/deploy.go b/pkg/stub/action/integration/deploy.go
index 8067b3c..2c814bc 100644
--- a/pkg/stub/action/integration/deploy.go
+++ b/pkg/stub/action/integration/deploy.go
@@ -18,17 +18,11 @@ limitations under the License.
 package integration
 
 import (
-	"fmt"
-	"github.com/sirupsen/logrus"
-	"strings"
-
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+	"github.com/apache/camel-k/pkg/trait"
+	"github.com/apache/camel-k/pkg/util/kubernetes"
 	"github.com/operator-framework/operator-sdk/pkg/sdk"
-	"github.com/pkg/errors"
-	appsv1 "k8s.io/api/apps/v1"
-	corev1 "k8s.io/api/core/v1"
-	k8serrors "k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"github.com/sirupsen/logrus"
 )
 
 // NewDeployAction create an action that handles integration deploy
@@ -48,15 +42,12 @@ func (action *deployAction) CanHandle(integration *v1alpha1.Integration) bool {
 }
 
 func (action *deployAction) Handle(integration *v1alpha1.Integration) error {
-	ctx, err := LookupContextForIntegration(integration)
-	if err != nil {
-		return err
-	}
-	err = createOrUpdateConfigMap(ctx, integration)
+	resources, err := trait.ComputeDeployment(integration)
 	if err != nil {
 		return err
 	}
-	err = createOrUpdateDeployment(ctx, integration)
+	// TODO we should look for objects that are no longer present in the collection and remove them
+	err = kubernetes.ReplaceResources(resources)
 	if err != nil {
 		return err
 	}
@@ -67,256 +58,3 @@ func (action *deployAction) Handle(integration *v1alpha1.Integration) error {
 
 	return sdk.Update(target)
 }
-
-// **********************************
-//
-// ConfigMap
-//
-// **********************************
-
-func getConfigMapFor(ctx *v1alpha1.IntegrationContext, integration *v1alpha1.Integration) (*corev1.ConfigMap, error) {
-	controller := true
-	blockOwnerDeletion := true
-
-	// combine properties of integration with context, integration
-	// properties have the priority
-	properties := CombineConfigurationAsMap("property", ctx, integration)
-
-	cm := corev1.ConfigMap{
-		TypeMeta: metav1.TypeMeta{
-			Kind:       "ConfigMap",
-			APIVersion: "v1",
-		},
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      integration.Name,
-			Namespace: integration.Namespace,
-			Labels:    integration.Labels,
-			Annotations: map[string]string{
-				"camel.apache.org/source.language": string(integration.Spec.Source.Language),
-				"camel.apache.org/source.name":     integration.Spec.Source.Name,
-			},
-			OwnerReferences: []metav1.OwnerReference{
-				{
-					APIVersion:         integration.APIVersion,
-					Kind:               integration.Kind,
-					Name:               integration.Name,
-					UID:                integration.UID,
-					Controller:         &controller,
-					BlockOwnerDeletion: &blockOwnerDeletion,
-				},
-			},
-		},
-		Data: map[string]string{
-			"integration": integration.Spec.Source.Content,
-			"properties":  PropertiesString(properties),
-		},
-	}
-
-	return &cm, nil
-}
-
-func createOrUpdateConfigMap(ctx *v1alpha1.IntegrationContext, integration *v1alpha1.Integration) error {
-	cm, err := getConfigMapFor(ctx, integration)
-	if err != nil {
-		return err
-	}
-
-	err = sdk.Create(cm)
-	if err != nil && k8serrors.IsAlreadyExists(err) {
-		err = sdk.Update(cm)
-	}
-	if err != nil {
-		return errors.Wrap(err, "could not create or replace configmap for integration "+integration.Name)
-	}
-
-	return err
-}
-
-// **********************************
-//
-// Deployment
-//
-// **********************************
-
-func getDeploymentFor(ctx *v1alpha1.IntegrationContext, integration *v1alpha1.Integration) (*appsv1.Deployment, error) {
-	controller := true
-	blockOwnerDeletion := true
-	sourceName := strings.TrimPrefix(integration.Spec.Source.Name, "/")
-
-	// combine environment of integration with context, integration
-	// environment has the priority
-	environment := CombineConfigurationAsMap("env", ctx, integration)
-
-	// set env vars needed by the runtime
-	environment["JAVA_MAIN_CLASS"] = "org.apache.camel.k.jvm.Application"
-
-	// camel-k runtime
-	environment["CAMEL_K_ROUTES_URI"] = "file:/etc/camel/conf/" + sourceName
-	environment["CAMEL_K_ROUTES_LANGUAGE"] = string(integration.Spec.Source.Language)
-	environment["CAMEL_K_CONF"] = "/etc/camel/conf/application.properties"
-	environment["CAMEL_K_CONF_D"] = "/etc/camel/conf.d"
-
-	// add a dummy env var to trigger deployment if everything but the code
-	// has been changed
-	environment["CAMEL_K_DIGEST"] = integration.Status.Digest
-
-	// optimizations
-	environment["AB_JOLOKIA_OFF"] = "true"
-
-	labels := map[string]string{
-		"camel.apache.org/integration": integration.Name,
-	}
-	deployment := appsv1.Deployment{
-		TypeMeta: metav1.TypeMeta{
-			Kind:       "Deployment",
-			APIVersion: appsv1.SchemeGroupVersion.String(),
-		},
-		ObjectMeta: metav1.ObjectMeta{
-			Name:        integration.Name,
-			Namespace:   integration.Namespace,
-			Labels:      integration.Labels,
-			Annotations: integration.Annotations,
-			OwnerReferences: []metav1.OwnerReference{
-				{
-					APIVersion:         integration.APIVersion,
-					Kind:               integration.Kind,
-					Name:               integration.Name,
-					Controller:         &controller,
-					BlockOwnerDeletion: &blockOwnerDeletion,
-					UID:                integration.UID,
-				},
-			},
-		},
-		Spec: appsv1.DeploymentSpec{
-			Replicas: integration.Spec.Replicas,
-			Selector: &metav1.LabelSelector{
-				MatchLabels: labels,
-			},
-			Template: corev1.PodTemplateSpec{
-				ObjectMeta: metav1.ObjectMeta{
-					Labels: labels,
-				},
-				Spec: corev1.PodSpec{
-					Containers: []corev1.Container{
-						{
-							Name:  integration.Name,
-							Image: integration.Status.Image,
-							Env:   EnvironmentAsEnvVarSlice(environment),
-						},
-					},
-				},
-			},
-		},
-	}
-
-	//
-	// Volumes :: Setup
-	//
-
-	vols := make([]corev1.Volume, 0)
-	mnts := make([]corev1.VolumeMount, 0)
-	cnt := 0
-
-	//
-	// Volumes :: Defaults
-	//
-
-	vols = append(vols, corev1.Volume{
-		Name: "integration",
-		VolumeSource: corev1.VolumeSource{
-			ConfigMap: &corev1.ConfigMapVolumeSource{
-				LocalObjectReference: corev1.LocalObjectReference{
-					Name: integration.Name,
-				},
-				Items: []corev1.KeyToPath{
-					{
-						Key:  "integration",
-						Path: sourceName,
-					}, {
-						Key:  "properties",
-						Path: "application.properties",
-					},
-				},
-			},
-		},
-	})
-
-	mnts = append(mnts, corev1.VolumeMount{
-		Name:      "integration",
-		MountPath: "/etc/camel/conf",
-	})
-
-	//
-	// Volumes :: Additional ConfigMaps
-	//
-
-	cmList := CombineConfigurationAsSlice("configmap", ctx, integration)
-	for _, cmName := range cmList {
-		cnt++
-
-		vols = append(vols, corev1.Volume{
-			Name: "integration-cm-" + cmName,
-			VolumeSource: corev1.VolumeSource{
-				ConfigMap: &corev1.ConfigMapVolumeSource{
-					LocalObjectReference: corev1.LocalObjectReference{
-						Name: cmName,
-					},
-				},
-			},
-		})
-
-		mnts = append(mnts, corev1.VolumeMount{
-			Name:      "integration-cm-" + cmName,
-			MountPath: fmt.Sprintf("/etc/camel/conf.d/%03d_%s", cnt, cmName),
-		})
-	}
-
-	//
-	// Volumes :: Additional Secrets
-	//
-
-	secretList := CombineConfigurationAsSlice("secret", ctx, integration)
-	for _, secretName := range secretList {
-		cnt++
-
-		vols = append(vols, corev1.Volume{
-			Name: "integration-secret-" + secretName,
-			VolumeSource: corev1.VolumeSource{
-				Secret: &corev1.SecretVolumeSource{
-					SecretName: secretName,
-				},
-			},
-		})
-
-		mnts = append(mnts, corev1.VolumeMount{
-			Name:      "integration-secret-" + secretName,
-			MountPath: fmt.Sprintf("/etc/camel/conf.d/%03d_%s", cnt, secretName),
-		})
-	}
-
-	//
-	// Volumes
-	//
-
-	deployment.Spec.Template.Spec.Volumes = vols
-	deployment.Spec.Template.Spec.Containers[0].VolumeMounts = mnts
-
-	return &deployment, nil
-}
-
-func createOrUpdateDeployment(ctx *v1alpha1.IntegrationContext, integration *v1alpha1.Integration) error {
-	deployment, err := getDeploymentFor(ctx, integration)
-	if err != nil {
-		return err
-	}
-
-	err = sdk.Create(deployment)
-	if err != nil && k8serrors.IsAlreadyExists(err) {
-		err = sdk.Update(deployment)
-	}
-	if err != nil {
-		return errors.Wrap(err, "could not create or replace deployment for integration "+integration.Name)
-	}
-
-	return nil
-}
diff --git a/pkg/stub/action/integration/util.go b/pkg/stub/action/integration/util.go
index ab9cf79..27bfb29 100644
--- a/pkg/stub/action/integration/util.go
+++ b/pkg/stub/action/integration/util.go
@@ -18,15 +18,10 @@ limitations under the License.
 package integration
 
 import (
-	"fmt"
-	"strings"
-
 	"github.com/apache/camel-k/pkg/util"
 
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
 	"github.com/operator-framework/operator-sdk/pkg/sdk"
-	"k8s.io/api/core/v1"
-
 	"github.com/pkg/errors"
 )
 
@@ -65,81 +60,3 @@ func LookupContextForIntegration(integration *v1alpha1.Integration) (*v1alpha1.I
 
 	return nil, nil
 }
-
-// PropertiesString --
-func PropertiesString(m map[string]string) string {
-	properties := ""
-	for k, v := range m {
-		properties += fmt.Sprintf("%s=%s\n", k, v)
-	}
-
-	return properties
-}
-
-// EnvironmentAsEnvVarSlice --
-func EnvironmentAsEnvVarSlice(m map[string]string) []v1.EnvVar {
-	env := make([]v1.EnvVar, 0, len(m))
-
-	for k, v := range m {
-		env = append(env, v1.EnvVar{Name: k, Value: v})
-	}
-
-	return env
-}
-
-// CombineConfigurationAsMap --
-func CombineConfigurationAsMap(configurationType string, context *v1alpha1.IntegrationContext, integration *v1alpha1.Integration) map[string]string {
-	result := make(map[string]string)
-	if context != nil {
-		// Add context properties first so integrations can
-		// override it
-		for _, c := range context.Spec.Configuration {
-			if c.Type == configurationType {
-				pair := strings.Split(c.Value, "=")
-				if len(pair) == 2 {
-					result[pair[0]] = pair[1]
-				}
-			}
-		}
-	}
-
-	if integration != nil {
-		for _, c := range integration.Spec.Configuration {
-			if c.Type == configurationType {
-				pair := strings.Split(c.Value, "=")
-				if len(pair) == 2 {
-					result[pair[0]] = pair[1]
-				}
-			}
-		}
-	}
-
-	return result
-}
-
-// CombineConfigurationAsSlice --
-func CombineConfigurationAsSlice(configurationType string, context *v1alpha1.IntegrationContext, integration *v1alpha1.Integration) []string {
-	result := make(map[string]bool, 0)
-	if context != nil {
-		// Add context properties first so integrations can
-		// override it
-		for _, c := range context.Spec.Configuration {
-			if c.Type == configurationType {
-				result[c.Value] = true
-			}
-		}
-	}
-
-	for _, c := range integration.Spec.Configuration {
-		if c.Type == configurationType {
-			result[c.Value] = true
-		}
-	}
-
-	keys := make([]string, 0, len(result))
-	for k := range result {
-		keys = append(keys, k)
-	}
-
-	return keys
-}
diff --git a/pkg/trait/base.go b/pkg/trait/base.go
new file mode 100644
index 0000000..3432b93
--- /dev/null
+++ b/pkg/trait/base.go
@@ -0,0 +1,239 @@
+/*
+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"
+	"github.com/apache/camel-k/pkg/util/kubernetes"
+	"strings"
+
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+type baseTrait struct {
+}
+
+func (*baseTrait) id() id {
+	return id("base")
+}
+
+func (d *baseTrait) customize(environment *environment, resources *kubernetes.Collection) (bool, error) {
+	resources.Add(d.getConfigMapFor(environment))
+	resources.Add(d.getDeploymentFor(environment))
+	return true, nil
+}
+
+// **********************************
+//
+// ConfigMap
+//
+// **********************************
+
+func (*baseTrait) getConfigMapFor(e *environment) *corev1.ConfigMap {
+	// combine properties of integration with context, integration
+	// properties have the priority
+	properties := CombineConfigurationAsMap("property", e.Context, e.Integration)
+
+	cm := corev1.ConfigMap{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "ConfigMap",
+			APIVersion: "v1",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      e.Integration.Name,
+			Namespace: e.Integration.Namespace,
+			Labels: map[string]string{
+				"camel.apache.org/integration": e.Integration.Name,
+			},
+			Annotations: map[string]string{
+				"camel.apache.org/source.language": string(e.Integration.Spec.Source.Language),
+				"camel.apache.org/source.name":     e.Integration.Spec.Source.Name,
+			},
+		},
+		Data: map[string]string{
+			"integration": e.Integration.Spec.Source.Content,
+			"properties":  PropertiesString(properties),
+		},
+	}
+
+	return &cm
+}
+
+// **********************************
+//
+// Deployment
+//
+// **********************************
+
+func (*baseTrait) getDeploymentFor(e *environment) *appsv1.Deployment {
+	sourceName := strings.TrimPrefix(e.Integration.Spec.Source.Name, "/")
+
+	// combine environment of integration with context, integration
+	// environment has the priority
+	environment := CombineConfigurationAsMap("env", e.Context, e.Integration)
+
+	// set env vars needed by the runtime
+	environment["JAVA_MAIN_CLASS"] = "org.apache.camel.k.jvm.Application"
+
+	// camel-k runtime
+	environment["CAMEL_K_ROUTES_URI"] = "file:/etc/camel/conf/" + sourceName
+	environment["CAMEL_K_ROUTES_LANGUAGE"] = string(e.Integration.Spec.Source.Language)
+	environment["CAMEL_K_CONF"] = "/etc/camel/conf/application.properties"
+	environment["CAMEL_K_CONF_D"] = "/etc/camel/conf.d"
+
+	// add a dummy env var to trigger deployment if everything but the code
+	// has been changed
+	environment["CAMEL_K_DIGEST"] = e.Integration.Status.Digest
+
+	// optimizations
+	environment["AB_JOLOKIA_OFF"] = "true"
+
+	labels := map[string]string{
+		"camel.apache.org/integration": e.Integration.Name,
+	}
+	deployment := appsv1.Deployment{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "Deployment",
+			APIVersion: appsv1.SchemeGroupVersion.String(),
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      e.Integration.Name,
+			Namespace: e.Integration.Namespace,
+			Labels: map[string]string{
+				"camel.apache.org/integration": e.Integration.Name,
+			},
+			Annotations: e.Integration.Annotations,
+		},
+		Spec: appsv1.DeploymentSpec{
+			Replicas: e.Integration.Spec.Replicas,
+			Selector: &metav1.LabelSelector{
+				MatchLabels: labels,
+			},
+			Template: corev1.PodTemplateSpec{
+				ObjectMeta: metav1.ObjectMeta{
+					Labels: labels,
+				},
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{
+						{
+							Name:  e.Integration.Name,
+							Image: e.Integration.Status.Image,
+							Env:   EnvironmentAsEnvVarSlice(environment),
+						},
+					},
+				},
+			},
+		},
+	}
+
+	//
+	// Volumes :: Setup
+	//
+
+	vols := make([]corev1.Volume, 0)
+	mnts := make([]corev1.VolumeMount, 0)
+	cnt := 0
+
+	//
+	// Volumes :: Defaults
+	//
+
+	vols = append(vols, corev1.Volume{
+		Name: "integration",
+		VolumeSource: corev1.VolumeSource{
+			ConfigMap: &corev1.ConfigMapVolumeSource{
+				LocalObjectReference: corev1.LocalObjectReference{
+					Name: e.Integration.Name,
+				},
+				Items: []corev1.KeyToPath{
+					{
+						Key:  "integration",
+						Path: sourceName,
+					}, {
+						Key:  "properties",
+						Path: "application.properties",
+					},
+				},
+			},
+		},
+	})
+
+	mnts = append(mnts, corev1.VolumeMount{
+		Name:      "integration",
+		MountPath: "/etc/camel/conf",
+	})
+
+	//
+	// Volumes :: Additional ConfigMaps
+	//
+
+	cmList := CombineConfigurationAsSlice("configmap", e.Context, e.Integration)
+	for _, cmName := range cmList {
+		cnt++
+
+		vols = append(vols, corev1.Volume{
+			Name: "integration-cm-" + cmName,
+			VolumeSource: corev1.VolumeSource{
+				ConfigMap: &corev1.ConfigMapVolumeSource{
+					LocalObjectReference: corev1.LocalObjectReference{
+						Name: cmName,
+					},
+				},
+			},
+		})
+
+		mnts = append(mnts, corev1.VolumeMount{
+			Name:      "integration-cm-" + cmName,
+			MountPath: fmt.Sprintf("/etc/camel/conf.d/%03d_%s", cnt, cmName),
+		})
+	}
+
+	//
+	// Volumes :: Additional Secrets
+	//
+
+	secretList := CombineConfigurationAsSlice("secret", e.Context, e.Integration)
+	for _, secretName := range secretList {
+		cnt++
+
+		vols = append(vols, corev1.Volume{
+			Name: "integration-secret-" + secretName,
+			VolumeSource: corev1.VolumeSource{
+				Secret: &corev1.SecretVolumeSource{
+					SecretName: secretName,
+				},
+			},
+		})
+
+		mnts = append(mnts, corev1.VolumeMount{
+			Name:      "integration-secret-" + secretName,
+			MountPath: fmt.Sprintf("/etc/camel/conf.d/%03d_%s", cnt, secretName),
+		})
+	}
+
+	//
+	// Volumes
+	//
+
+	deployment.Spec.Template.Spec.Volumes = vols
+	deployment.Spec.Template.Spec.Containers[0].VolumeMounts = mnts
+
+	return &deployment
+}
diff --git a/pkg/trait/catalog.go b/pkg/trait/catalog.go
new file mode 100644
index 0000000..abc6415
--- /dev/null
+++ b/pkg/trait/catalog.go
@@ -0,0 +1,82 @@
+/*
+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 (
+	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+	"github.com/apache/camel-k/pkg/util/kubernetes"
+)
+
+var (
+	tBase    = &baseTrait{}
+	tService = &serviceTrait{}
+	tRoute   = &routeTrait{}
+	tOwner   = &ownerTrait{}
+)
+
+// customizersFor returns a Catalog for the given integration details
+func customizersFor(environment *environment) customizer {
+	switch environment.Platform.Spec.Cluster {
+	case v1alpha1.IntegrationPlatformClusterOpenShift:
+		return compose(
+			tBase,
+			tService,
+			tRoute,
+			tOwner,
+		)
+	case v1alpha1.IntegrationPlatformClusterKubernetes:
+		return compose(
+			tBase,
+			tService,
+			tOwner,
+		)
+		// case Knative: ...
+	}
+	return nil
+}
+
+func compose(traits ...customizer) customizer {
+	return &chainedCustomizer{
+		customizers: traits,
+	}
+}
+
+// -------------------------------------------
+
+type chainedCustomizer struct {
+	customizers []customizer
+}
+
+func (c *chainedCustomizer) id() id {
+	return id("")
+}
+
+func (c *chainedCustomizer) customize(environment *environment, resources *kubernetes.Collection) (bool, error) {
+	atLeastOne := false
+	for _, custom := range c.customizers {
+		if environment.isExplicitlyEnabled(custom.id()) || environment.isAutoDetectionMode(custom.id()) {
+			if done, err := custom.customize(environment, resources); err != nil {
+				return false, err
+			} else if done && custom.id() != "" {
+				environment.ExecutedCustomizers = append(environment.ExecutedCustomizers, custom.id())
+				atLeastOne = atLeastOne || done
+			}
+		}
+	}
+	return atLeastOne, nil
+}
diff --git a/pkg/trait/doc.go b/pkg/trait/doc.go
new file mode 100644
index 0000000..7216c27
--- /dev/null
+++ b/pkg/trait/doc.go
@@ -0,0 +1,19 @@
+/*
+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 contains implementations of all available traits (features)
+package trait
diff --git a/pkg/trait/owner.go b/pkg/trait/owner.go
new file mode 100644
index 0000000..8603bd5
--- /dev/null
+++ b/pkg/trait/owner.go
@@ -0,0 +1,48 @@
+/*
+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 "github.com/apache/camel-k/pkg/util/kubernetes"
+import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+// ownerTrait ensures that all created resources belong to the integration being created
+type ownerTrait struct {
+}
+
+func (*ownerTrait) id() id {
+	return id("owner")
+}
+
+func (*ownerTrait) customize(e *environment, resources *kubernetes.Collection) (bool, error) {
+	controller := true
+	blockOwnerDeletion := true
+	resources.VisitMetaObject(func(res metav1.Object) {
+		references := []metav1.OwnerReference{
+			{
+				APIVersion:         e.Integration.APIVersion,
+				Kind:               e.Integration.Kind,
+				Name:               e.Integration.Name,
+				UID:                e.Integration.UID,
+				Controller:         &controller,
+				BlockOwnerDeletion: &blockOwnerDeletion,
+			},
+		}
+		res.SetOwnerReferences(references)
+	})
+	return true, nil
+}
diff --git a/pkg/trait/route.go b/pkg/trait/route.go
new file mode 100644
index 0000000..acf6984
--- /dev/null
+++ b/pkg/trait/route.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 trait
+
+import (
+	"github.com/apache/camel-k/pkg/util/kubernetes"
+	routev1 "github.com/openshift/api/route/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/intstr"
+)
+
+type routeTrait struct {
+}
+
+func (*routeTrait) id() id {
+	return id("route")
+}
+
+func (e *routeTrait) customize(environment *environment, resources *kubernetes.Collection) (bool, error) {
+	var service *corev1.Service
+	resources.VisitService(func(s *corev1.Service) {
+		if s.ObjectMeta.Labels != nil {
+			if intName, ok := s.ObjectMeta.Labels["camel.apache.org/integration"]; ok && intName == environment.Integration.Name {
+				service = s
+			}
+		}
+	})
+
+	if service != nil {
+		resources.Add(e.getRouteFor(environment, service))
+		return true, nil
+	}
+
+	return false, nil
+}
+
+func (*routeTrait) getRouteFor(e *environment, service *corev1.Service) *routev1.Route {
+	route := routev1.Route{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "Route",
+			APIVersion: routev1.SchemeGroupVersion.String(),
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      service.Name,
+			Namespace: service.Namespace,
+		},
+		Spec: routev1.RouteSpec{
+			Port: &routev1.RoutePort{
+				TargetPort: intstr.FromString("http"),
+			},
+			To: routev1.RouteTargetReference{
+				Kind: "Service",
+				Name: service.Name,
+			},
+		},
+	}
+	return &route
+}
diff --git a/pkg/trait/service.go b/pkg/trait/service.go
new file mode 100644
index 0000000..9907466
--- /dev/null
+++ b/pkg/trait/service.go
@@ -0,0 +1,103 @@
+/*
+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 (
+	"github.com/apache/camel-k/pkg/util/kubernetes"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/intstr"
+)
+
+var webComponents = map[string]bool{
+	"camel:servlet":     true,
+	"camel:undertow":    true,
+	"camel:jetty":       true,
+	"camel:netty-http":  true,
+	"camel:netty4-http": true,
+	// TODO find a better way to discover need for exposure
+	// maybe using the resolved classpath of the context instead of the requested dependencies
+}
+
+type serviceTrait struct {
+}
+
+const (
+	serviceTraitPortKey = "port"
+)
+
+func (*serviceTrait) id() id {
+	return id("service")
+}
+
+func (s *serviceTrait) customize(environment *environment, resources *kubernetes.Collection) (bool, error) {
+	if environment.isAutoDetectionMode(s.id()) && !s.requiresService(environment) {
+		return false, nil
+	}
+	svc, err := s.getServiceFor(environment)
+	if err != nil {
+		return false, err
+	}
+	resources.Add(svc)
+	return true, nil
+}
+
+func (s *serviceTrait) getServiceFor(e *environment) (*corev1.Service, error) {
+	port, err := e.getIntConfigOr(s.id(), serviceTraitPortKey, 8080)
+	if err != nil {
+		return nil, err
+	}
+
+	svc := corev1.Service{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "Service",
+			APIVersion: "v1",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      e.Integration.Name,
+			Namespace: e.Integration.Namespace,
+			Labels: map[string]string{
+				"camel.apache.org/integration": e.Integration.Name,
+			},
+		},
+		Spec: corev1.ServiceSpec{
+			Ports: []corev1.ServicePort{
+				{
+					Name:       "http",
+					Port:       80,
+					Protocol:   corev1.ProtocolTCP,
+					TargetPort: intstr.FromInt(port),
+				},
+			},
+			Selector: map[string]string{
+				"camel.apache.org/integration": e.Integration.Name,
+			},
+		},
+	}
+
+	return &svc, nil
+}
+
+func (*serviceTrait) requiresService(environment *environment) bool {
+	for _, dep := range environment.Integration.Spec.Dependencies {
+		if decision, present := webComponents[dep]; present {
+			return decision
+		}
+	}
+	return false
+}
diff --git a/pkg/trait/trait.go b/pkg/trait/trait.go
new file mode 100644
index 0000000..db3c56f
--- /dev/null
+++ b/pkg/trait/trait.go
@@ -0,0 +1,60 @@
+/*
+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 (
+	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+	"github.com/apache/camel-k/pkg/platform"
+	"github.com/apache/camel-k/pkg/util/kubernetes"
+	"github.com/pkg/errors"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+// ComputeDeployment generates all required resources for deploying the given integration
+func ComputeDeployment(integration *v1alpha1.Integration) ([]runtime.Object, error) {
+	environment, err := newEnvironment(integration)
+	if err != nil {
+		return nil, err
+	}
+	resources := kubernetes.NewCollection()
+	customizers := customizersFor(environment)
+	// invoke the trait framework to determine the needed resources
+	if _, err = customizers.customize(environment, resources); err != nil {
+		return nil, errors.Wrap(err, "error during trait customization")
+	}
+	return resources.Items(), nil
+}
+
+// newEnvironment creates a environment from the given data
+func newEnvironment(integration *v1alpha1.Integration) (*environment, error) {
+	pl, err := platform.GetCurrentPlatform(integration.Namespace)
+	if err != nil {
+		return nil, err
+	}
+	ctx, err := GetIntegrationContext(integration)
+	if err != nil {
+		return nil, err
+	}
+
+	return &environment{
+		Platform:            pl,
+		Context:             ctx,
+		Integration:         integration,
+		ExecutedCustomizers: make([]id, 0),
+	}, nil
+}
diff --git a/pkg/trait/trait_test.go b/pkg/trait/trait_test.go
new file mode 100644
index 0000000..8427068
--- /dev/null
+++ b/pkg/trait/trait_test.go
@@ -0,0 +1,161 @@
+/*
+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 (
+	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+	"github.com/apache/camel-k/pkg/util/kubernetes"
+	routev1 "github.com/openshift/api/route/v1"
+	"github.com/stretchr/testify/assert"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"testing"
+)
+
+func TestOpenshiftTraits(t *testing.T) {
+	env := createTestEnv(v1alpha1.IntegrationPlatformClusterOpenShift, "camel:core")
+	res := processTestEnv(t, env)
+	assert.Contains(t, env.ExecutedCustomizers, id("base"))
+	assert.NotContains(t, env.ExecutedCustomizers, id("service"))
+	assert.NotContains(t, env.ExecutedCustomizers, id("route"))
+	assert.Contains(t, env.ExecutedCustomizers, id("owner"))
+	assert.NotNil(t, res.GetConfigMap(func(cm *corev1.ConfigMap) bool {
+		return cm.Name == "test"
+	}))
+	assert.NotNil(t, res.GetDeployment(func(deployment *appsv1.Deployment) bool {
+		return deployment.Name == "test"
+	}))
+}
+
+func TestOpenshiftTraitsWithWeb(t *testing.T) {
+	env := createTestEnv(v1alpha1.IntegrationPlatformClusterOpenShift, "camel:core", "camel:undertow")
+	res := processTestEnv(t, env)
+	assert.Contains(t, env.ExecutedCustomizers, id("base"))
+	assert.Contains(t, env.ExecutedCustomizers, id("service"))
+	assert.Contains(t, env.ExecutedCustomizers, id("route"))
+	assert.Contains(t, env.ExecutedCustomizers, id("owner"))
+	assert.NotNil(t, res.GetConfigMap(func(cm *corev1.ConfigMap) bool {
+		return cm.Name == "test"
+	}))
+	assert.NotNil(t, res.GetDeployment(func(deployment *appsv1.Deployment) bool {
+		return deployment.Name == "test"
+	}))
+	assert.NotNil(t, res.GetService(func(svc *corev1.Service) bool {
+		return svc.Name == "test"
+	}))
+	assert.NotNil(t, res.GetRoute(func(svc *routev1.Route) bool {
+		return svc.Name == "test"
+	}))
+}
+
+func TestOpenshiftTraitsWithWebAndConfig(t *testing.T) {
+	env := createTestEnv(v1alpha1.IntegrationPlatformClusterOpenShift, "camel:core", "camel:undertow")
+	env.Integration.Spec.Traits = make(map[string]v1alpha1.IntegrationTraitSpec)
+	env.Integration.Spec.Traits["service"] = v1alpha1.IntegrationTraitSpec{
+		Configuration: map[string]string{
+			"port": "7071",
+		},
+	}
+	res := processTestEnv(t, env)
+	assert.Contains(t, env.ExecutedCustomizers, id("service"))
+	assert.Contains(t, env.ExecutedCustomizers, id("route"))
+	assert.NotNil(t, res.GetService(func(svc *corev1.Service) bool {
+		return svc.Name == "test" && svc.Spec.Ports[0].TargetPort.IntVal == int32(7071)
+	}))
+}
+
+func TestOpenshiftTraitsWithWebAndDisabledTrait(t *testing.T) {
+	falseBoolean := false
+	env := createTestEnv(v1alpha1.IntegrationPlatformClusterOpenShift, "camel:core", "camel:undertow")
+	env.Integration.Spec.Traits = make(map[string]v1alpha1.IntegrationTraitSpec)
+	env.Integration.Spec.Traits["service"] = v1alpha1.IntegrationTraitSpec{
+		Configuration: map[string]string{
+			"port": "7071",
+		},
+		Enabled: &falseBoolean,
+	}
+	res := processTestEnv(t, env)
+	assert.NotContains(t, env.ExecutedCustomizers, id("service"))
+	assert.NotContains(t, env.ExecutedCustomizers, id("route")) // No route without service
+	assert.Nil(t, res.GetService(func(svc *corev1.Service) bool {
+		return true
+	}))
+}
+
+func TestKubernetesTraits(t *testing.T) {
+	env := createTestEnv(v1alpha1.IntegrationPlatformClusterKubernetes, "camel:core")
+	res := processTestEnv(t, env)
+	assert.Contains(t, env.ExecutedCustomizers, id("base"))
+	assert.NotContains(t, env.ExecutedCustomizers, id("service"))
+	assert.NotContains(t, env.ExecutedCustomizers, id("route"))
+	assert.Contains(t, env.ExecutedCustomizers, id("owner"))
+	assert.NotNil(t, res.GetConfigMap(func(cm *corev1.ConfigMap) bool {
+		return cm.Name == "test"
+	}))
+	assert.NotNil(t, res.GetDeployment(func(deployment *appsv1.Deployment) bool {
+		return deployment.Name == "test"
+	}))
+}
+
+func TestKubernetesTraitsWithWeb(t *testing.T) {
+	env := createTestEnv(v1alpha1.IntegrationPlatformClusterKubernetes, "camel:core", "camel:servlet")
+	res := processTestEnv(t, env)
+	assert.Contains(t, env.ExecutedCustomizers, id("base"))
+	assert.Contains(t, env.ExecutedCustomizers, id("service"))
+	assert.NotContains(t, env.ExecutedCustomizers, id("route"))
+	assert.Contains(t, env.ExecutedCustomizers, id("owner"))
+	assert.NotNil(t, res.GetConfigMap(func(cm *corev1.ConfigMap) bool {
+		return cm.Name == "test"
+	}))
+	assert.NotNil(t, res.GetDeployment(func(deployment *appsv1.Deployment) bool {
+		return deployment.Name == "test"
+	}))
+	assert.NotNil(t, res.GetService(func(svc *corev1.Service) bool {
+		return svc.Name == "test"
+	}))
+}
+
+func processTestEnv(t *testing.T, env *environment) *kubernetes.Collection {
+	resources := kubernetes.NewCollection()
+	customizers := customizersFor(env)
+	_, err := customizers.customize(env, resources)
+	assert.Nil(t, err)
+	return resources
+}
+
+func createTestEnv(cluster v1alpha1.IntegrationPlatformCluster, dependencies ...string) *environment {
+	return &environment{
+		Integration: &v1alpha1.Integration{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "test",
+				Namespace: "ns",
+			},
+			Spec: v1alpha1.IntegrationSpec{
+				Dependencies: dependencies,
+			},
+		},
+		Context: &v1alpha1.IntegrationContext{},
+		Platform: &v1alpha1.IntegrationPlatform{
+			Spec: v1alpha1.IntegrationPlatformSpec{
+				Cluster: cluster,
+			},
+		},
+		ExecutedCustomizers: make([]id, 0),
+	}
+}
diff --git a/pkg/trait/types.go b/pkg/trait/types.go
new file mode 100644
index 0000000..306f0b4
--- /dev/null
+++ b/pkg/trait/types.go
@@ -0,0 +1,111 @@
+/*
+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 (
+	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+	"github.com/apache/camel-k/pkg/util/kubernetes"
+	"github.com/pkg/errors"
+	"strconv"
+)
+
+// ID uniquely identifies a trait
+type id string
+
+// A Customizer performs customization of the deployed objects
+type customizer interface {
+	// The Name of the customizer
+	id() id
+	// Customize executes the trait customization on the resources and return true if the resources have been changed
+	customize(environment *environment, resources *kubernetes.Collection) (bool, error)
+}
+
+// A environment provides the context where the trait is executed
+type environment struct {
+	Platform            *v1alpha1.IntegrationPlatform
+	Context             *v1alpha1.IntegrationContext
+	Integration         *v1alpha1.Integration
+	ExecutedCustomizers []id
+}
+
+func (e *environment) getTraitSpec(traitID id) *v1alpha1.IntegrationTraitSpec {
+	if e.Integration.Spec.Traits == nil {
+		return nil
+	}
+	if conf, ok := e.Integration.Spec.Traits[string(traitID)]; ok {
+		return &conf
+	}
+	return nil
+}
+
+func (e *environment) isExplicitlyEnabled(traitID id) bool {
+	conf := e.getTraitSpec(traitID)
+	return conf != nil && conf.Enabled != nil && *conf.Enabled
+}
+
+func (e *environment) isExplicitlyDisabled(traitID id) bool {
+	conf := e.getTraitSpec(traitID)
+	return conf != nil && conf.Enabled != nil && !*conf.Enabled
+}
+
+func (e *environment) isAutoDetectionMode(traitID id) bool {
+	conf := e.getTraitSpec(traitID)
+	return conf == nil || conf.Enabled == nil
+}
+
+func (e *environment) getConfig(traitID id, key string) *string {
+	conf := e.getTraitSpec(traitID)
+	if conf == nil || conf.Configuration == nil {
+		return nil
+	}
+	if v, ok := conf.Configuration[key]; ok {
+		return &v
+	}
+	return nil
+}
+
+func (e *environment) getConfigOr(traitID id, key string, defaultValue string) string {
+	val := e.getConfig(traitID, key)
+	if val != nil {
+		return *val
+	}
+	return defaultValue
+}
+
+func (e *environment) getIntConfig(traitID id, key string) (*int, error) {
+	val := e.getConfig(traitID, key)
+	if val == nil {
+		return nil, nil
+	}
+	intVal, err := strconv.Atoi(*val)
+	if err != nil {
+		return nil, errors.Wrap(err, "cannot extract a integer from property "+key+" with value "+*val)
+	}
+	return &intVal, nil
+}
+
+func (e *environment) getIntConfigOr(traitID id, key string, defaultValue int) (int, error) {
+	val, err := e.getIntConfig(traitID, key)
+	if err != nil {
+		return 0, err
+	}
+	if val != nil {
+		return *val, nil
+	}
+	return defaultValue, nil
+}
diff --git a/pkg/trait/util.go b/pkg/trait/util.go
new file mode 100644
index 0000000..0e5da55
--- /dev/null
+++ b/pkg/trait/util.go
@@ -0,0 +1,117 @@
+/*
+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"
+	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+	"github.com/operator-framework/operator-sdk/pkg/sdk"
+	"github.com/pkg/errors"
+	"k8s.io/api/core/v1"
+	"strings"
+)
+
+// GetIntegrationContext retrieves the context set on the integration
+func GetIntegrationContext(integration *v1alpha1.Integration) (*v1alpha1.IntegrationContext, error) {
+	if integration.Spec.Context == "" {
+		return nil, errors.New("no context set on the integration")
+	}
+
+	name := integration.Spec.Context
+	ctx := v1alpha1.NewIntegrationContext(integration.Namespace, name)
+	err := sdk.Get(&ctx)
+	return &ctx, err
+}
+
+// PropertiesString --
+func PropertiesString(m map[string]string) string {
+	properties := ""
+	for k, v := range m {
+		properties += fmt.Sprintf("%s=%s\n", k, v)
+	}
+
+	return properties
+}
+
+// EnvironmentAsEnvVarSlice --
+func EnvironmentAsEnvVarSlice(m map[string]string) []v1.EnvVar {
+	env := make([]v1.EnvVar, 0, len(m))
+
+	for k, v := range m {
+		env = append(env, v1.EnvVar{Name: k, Value: v})
+	}
+
+	return env
+}
+
+// CombineConfigurationAsMap --
+func CombineConfigurationAsMap(configurationType string, context *v1alpha1.IntegrationContext, integration *v1alpha1.Integration) map[string]string {
+	result := make(map[string]string)
+	if context != nil {
+		// Add context properties first so integrations can
+		// override it
+		for _, c := range context.Spec.Configuration {
+			if c.Type == configurationType {
+				pair := strings.Split(c.Value, "=")
+				if len(pair) == 2 {
+					result[pair[0]] = pair[1]
+				}
+			}
+		}
+	}
+
+	if integration != nil {
+		for _, c := range integration.Spec.Configuration {
+			if c.Type == configurationType {
+				pair := strings.Split(c.Value, "=")
+				if len(pair) == 2 {
+					result[pair[0]] = pair[1]
+				}
+			}
+		}
+	}
+
+	return result
+}
+
+// CombineConfigurationAsSlice --
+func CombineConfigurationAsSlice(configurationType string, context *v1alpha1.IntegrationContext, integration *v1alpha1.Integration) []string {
+	result := make(map[string]bool, 0)
+	if context != nil {
+		// Add context properties first so integrations can
+		// override it
+		for _, c := range context.Spec.Configuration {
+			if c.Type == configurationType {
+				result[c.Value] = true
+			}
+		}
+	}
+
+	for _, c := range integration.Spec.Configuration {
+		if c.Type == configurationType {
+			result[c.Value] = true
+		}
+	}
+
+	keys := make([]string, 0, len(result))
+	for k := range result {
+		keys = append(keys, k)
+	}
+
+	return keys
+}
diff --git a/pkg/util/kubernetes/collection.go b/pkg/util/kubernetes/collection.go
new file mode 100644
index 0000000..8f099c3
--- /dev/null
+++ b/pkg/util/kubernetes/collection.go
@@ -0,0 +1,144 @@
+/*
+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 kubernetes
+
+import (
+	routev1 "github.com/openshift/api/route/v1"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+// A Collection is a container of Kubernetes resources
+type Collection struct {
+	items []runtime.Object
+}
+
+// NewCollection creates a new empty collection
+func NewCollection() *Collection {
+	return &Collection{
+		items: make([]runtime.Object, 0),
+	}
+}
+
+// Items returns all resources belonging to the collection
+func (c *Collection) Items() []runtime.Object {
+	return c.items
+}
+
+// Add adds a resource to the collection
+func (c *Collection) Add(resource runtime.Object) {
+	c.items = append(c.items, resource)
+}
+
+// VisitDeployment executes the visitor function on all Deployment resources
+func (c *Collection) VisitDeployment(visitor func(*appsv1.Deployment)) {
+	c.Visit(func(res runtime.Object) {
+		if conv, ok := res.(*appsv1.Deployment); ok {
+			visitor(conv)
+		}
+	})
+}
+
+// GetDeployment returns a Deployment that matches the given function
+func (c *Collection) GetDeployment(filter func(*appsv1.Deployment)bool) *appsv1.Deployment {
+	var retValue *appsv1.Deployment
+	c.VisitDeployment(func(re *appsv1.Deployment) {
+		if filter(re) {
+			retValue = re
+		}
+	})
+	return retValue
+}
+
+// VisitConfigMap executes the visitor function on all ConfigMap resources
+func (c *Collection) VisitConfigMap(visitor func(*corev1.ConfigMap)) {
+	c.Visit(func(res runtime.Object) {
+		if conv, ok := res.(*corev1.ConfigMap); ok {
+			visitor(conv)
+		}
+	})
+}
+
+// GetConfigMap returns a ConfigMap that matches the given function
+func (c *Collection) GetConfigMap(filter func(*corev1.ConfigMap)bool) *corev1.ConfigMap {
+	var retValue *corev1.ConfigMap
+	c.VisitConfigMap(func(re *corev1.ConfigMap) {
+		if filter(re) {
+			retValue = re
+		}
+	})
+	return retValue
+}
+
+// VisitService executes the visitor function on all Service resources
+func (c *Collection) VisitService(visitor func(*corev1.Service)) {
+	c.Visit(func(res runtime.Object) {
+		if conv, ok := res.(*corev1.Service); ok {
+			visitor(conv)
+		}
+	})
+}
+
+// GetService returns a Service that matches the given function
+func (c *Collection) GetService(filter func(*corev1.Service)bool) *corev1.Service {
+	var retValue *corev1.Service
+	c.VisitService(func(re *corev1.Service) {
+		if filter(re) {
+			retValue = re
+		}
+	})
+	return retValue
+}
+
+// VisitRoute executes the visitor function on all Route resources
+func (c *Collection) VisitRoute(visitor func(*routev1.Route)) {
+	c.Visit(func(res runtime.Object) {
+		if conv, ok := res.(*routev1.Route); ok {
+			visitor(conv)
+		}
+	})
+}
+
+// GetRoute returns a Route that matches the given function
+func (c *Collection) GetRoute(filter func(*routev1.Route)bool) *routev1.Route {
+	var retValue *routev1.Route
+	c.VisitRoute(func(re *routev1.Route) {
+		if filter(re) {
+			retValue = re
+		}
+	})
+	return retValue
+}
+
+// VisitMetaObject executes the visitor function on all meta.Object resources
+func (c *Collection) VisitMetaObject(visitor func(metav1.Object)) {
+	c.Visit(func(res runtime.Object) {
+		if conv, ok := res.(metav1.Object); ok {
+			visitor(conv)
+		}
+	})
+}
+
+// Visit executes the visitor function on all resources
+func (c *Collection) Visit(visitor func(runtime.Object)) {
+	for _, res := range c.items {
+		visitor(res)
+	}
+}
diff --git a/pkg/util/kubernetes/replace.go b/pkg/util/kubernetes/replace.go
new file mode 100644
index 0000000..45b908d
--- /dev/null
+++ b/pkg/util/kubernetes/replace.go
@@ -0,0 +1,97 @@
+/*
+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 kubernetes
+
+import (
+	routev1 "github.com/openshift/api/route/v1"
+	"github.com/operator-framework/operator-sdk/pkg/sdk"
+	"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"
+)
+
+// ReplaceResources allows to completely replace a list of resources on Kubernetes, taking care of immutable fields and resource versions
+func ReplaceResources(objects []runtime.Object) error {
+	for _, object := range objects {
+		err := ReplaceResource(object)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// ReplaceResource allows to completely replace a resource on Kubernetes, taking care of immutable fields and resource versions
+func ReplaceResource(res runtime.Object) error {
+	err := sdk.Create(res)
+	if err != nil && k8serrors.IsAlreadyExists(err) {
+		existing := res.DeepCopyObject()
+		err = sdk.Get(existing)
+		if err != nil {
+			return err
+		}
+		mapRequiredMeta(existing, res)
+		mapRequiredServiceData(existing, res)
+		mapRequiredRouteData(existing, res)
+		err = sdk.Update(res)
+	}
+	if err != nil {
+		return errors.Wrap(err, "could not create or replace "+findResourceDetails(res))
+	}
+	return nil
+}
+
+func mapRequiredMeta(from runtime.Object, to runtime.Object) {
+	if fromC, ok := from.(metav1.Object); ok {
+		if toC, ok := to.(metav1.Object); ok {
+			toC.SetResourceVersion(fromC.GetResourceVersion())
+		}
+	}
+}
+
+func mapRequiredServiceData(from runtime.Object, to runtime.Object) {
+	if fromC, ok := from.(*corev1.Service); ok {
+		if toC, ok := to.(*corev1.Service); ok {
+			toC.Spec.ClusterIP = fromC.Spec.ClusterIP
+		}
+	}
+}
+
+func mapRequiredRouteData(from runtime.Object, to runtime.Object) {
+	if fromC, ok := from.(*routev1.Route); ok {
+		if toC, ok := to.(*routev1.Route); ok {
+			toC.Spec.Host = fromC.Spec.Host
+		}
+	}
+}
+
+func findResourceDetails(res runtime.Object) string {
+	if res == nil {
+		return "nil resource"
+	}
+	if meta, ok := res.(metav1.Object); ok {
+		name := meta.GetName()
+		if ty, ok := res.(metav1.Type); ok {
+			return ty.GetKind() + " " + name
+		}
+		return "resource " + name
+	}
+	return "unnamed resource"
+}
diff --git a/runtime/examples/routes-rest.js b/runtime/examples/routes-rest.js
index e888019..97d41ec 100644
--- a/runtime/examples/routes-rest.js
+++ b/runtime/examples/routes-rest.js
@@ -17,7 +17,7 @@ l.exchangeFormatter = function(e) {
 
 c = restConfiguration()
 c.component = 'undertow'
-c.port = 8081
+c.port = 8080
 
 // ****************
 //
@@ -37,6 +37,7 @@ function proc(e) {
 
 rest()
     .path('/say/hello')
+    .produces("text/plain")
     .get().route()
         .transform().constant("Hello World");
 


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services