You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by nf...@apache.org on 2018/09/27 10:34:14 UTC

[camel-k] branch master updated: runtime: support for kotlin runtime

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 6d9d529  runtime: support for kotlin runtime
6d9d529 is described below

commit 6d9d529bb4257036bad924be72f9e7f43e7cf218
Author: lburgazzoli <lb...@gmail.com>
AuthorDate: Tue Sep 25 10:32:40 2018 +0200

    runtime: support for kotlin runtime
---
 README.adoc                                        |   1 +
 deploy/platform-integration-context-groovy.yaml    |   5 +-
 ....yaml => platform-integration-context-jvm.yaml} |   5 +-
 ...ml => platform-integration-context-kotlin.yaml} |   6 +-
 deploy/resources.go                                |  31 ++++-
 pkg/apis/camel/v1alpha1/types.go                   |   2 +
 pkg/build/assemble/maven_assembler.go              |   4 +
 pkg/client/cmd/completion_bash.go                  |  22 +++-
 pkg/client/cmd/context_create.go                   |  11 ++
 pkg/client/cmd/run.go                              |  23 +++-
 pkg/discover/dependencies.go                       |   7 +-
 pkg/discover/language.go                           |   6 +-
 pkg/install/operator.go                            |   2 +-
 pkg/stub/action/integration/initialize.go          |   9 +-
 pkg/stub/action/platform/create.go                 |   8 +-
 pkg/util/util.go                                   |  13 +++
 runtime/examples/kotlin-routes.kts                 |  20 ++++
 runtime/groovy/.gitignore                          |  10 ++
 runtime/groovy/pom.xml                             |  45 ++++++++
 .../apache/camel/k/groovy/GroovyRoutesLoader.java  |  63 ++++++++++
 .../services/org.apache.camel.k.jvm.RoutesLoader   |   1 +
 .../apache/camel/k/groovy/RoutesLoadersTest.java   |  47 ++++++++
 .../src/test/resources/routes.groovy               |   0
 runtime/jvm/pom.xml                                |  24 +++-
 .../main/java/org/apache/camel/k/jvm/Language.java |  44 ++++---
 .../java/org/apache/camel/k/jvm/RoutesLoaders.java | 127 ++++-----------------
 .../org/apache/camel/k/jvm/dsl/Components.java     |  47 ++++++++
 .../java/org/apache/camel/k/jvm/dsl/Scripting.java |  47 ++++++++
 .../services/org.apache.camel.k.jvm.RoutesLoader   |   4 +
 .../org/apache/camel/k/jvm/RoutesLoadersTest.java  |  27 +----
 runtime/kotlin/.gitignore                          |  10 ++
 runtime/kotlin/pom.xml                             |  56 +++++++++
 .../apache/camel/k/kotlin/KotlinRoutesLoader.java  |  77 +++++++++++++
 .../services/javax.script.ScriptEngineFactory      |   1 +
 .../services/org.apache.camel.k.jvm.RoutesLoader   |   1 +
 .../apache/camel/k/kotlin/RoutesLoadersTest.java   |  50 ++++++++
 runtime/kotlin/src/test/resources/routes.kts       |  21 ++++
 runtime/pom.xml                                    |   3 +
 38 files changed, 702 insertions(+), 178 deletions(-)

diff --git a/README.adoc b/README.adoc
index 24d0779..fadd6d1 100644
--- a/README.adoc
+++ b/README.adoc
@@ -114,6 +114,7 @@ Camel K supports multiple languages for writing integrations:
 | XML				| Integrations written in plain XML DSL are supported (Spring XML or Blueprint not supported).
 | Groovy			| Groovy `.groovy` files are supported (experimental).
 | JavaScript		| JavaScript `.js` files are supported (experimental).
+| Kotlin			| Kotlin Script `.kts` files are supported (experimental).
 |=======================
 
 Integrations written in different languages are provided in the link:/runtime/examples[examples] directory.
diff --git a/deploy/platform-integration-context-groovy.yaml b/deploy/platform-integration-context-groovy.yaml
index cabe24a..31aaeff 100644
--- a/deploy/platform-integration-context-groovy.yaml
+++ b/deploy/platform-integration-context-groovy.yaml
@@ -9,5 +9,6 @@ metadata:
     camel.apache.org/context.type: platform
 spec:
   dependencies:
-    - camel:core
-    - camel:groovy
\ No newline at end of file
+    - runtime:jvm
+    - runtime:groovy
+    - camel:core
\ No newline at end of file
diff --git a/deploy/platform-integration-context-core.yaml b/deploy/platform-integration-context-jvm.yaml
similarity index 69%
copy from deploy/platform-integration-context-core.yaml
copy to deploy/platform-integration-context-jvm.yaml
index d0d3289..28e8ff5 100644
--- a/deploy/platform-integration-context-core.yaml
+++ b/deploy/platform-integration-context-jvm.yaml
@@ -1,12 +1,13 @@
 apiVersion: camel.apache.org/v1alpha1
 kind: IntegrationContext
 metadata:
-  name: core
+  name: jvm
   labels:
     app: "camel-k"
     camel.apache.org/context.created.by.kind: Operator
-    camel.apache.org/context.created.by.name: core
+    camel.apache.org/context.created.by.name: jvm
     camel.apache.org/context.type: platform
 spec:
   dependencies:
+    - runtime:jvm
     - camel:core
\ No newline at end of file
diff --git a/deploy/platform-integration-context-core.yaml b/deploy/platform-integration-context-kotlin.yaml
similarity index 65%
rename from deploy/platform-integration-context-core.yaml
rename to deploy/platform-integration-context-kotlin.yaml
index d0d3289..822b4a4 100644
--- a/deploy/platform-integration-context-core.yaml
+++ b/deploy/platform-integration-context-kotlin.yaml
@@ -1,12 +1,14 @@
 apiVersion: camel.apache.org/v1alpha1
 kind: IntegrationContext
 metadata:
-  name: core
+  name: kotlin
   labels:
     app: "camel-k"
     camel.apache.org/context.created.by.kind: Operator
-    camel.apache.org/context.created.by.name: core
+    camel.apache.org/context.created.by.name: jvm
     camel.apache.org/context.type: platform
 spec:
   dependencies:
+    - runtime:jvm
+    - runtime:kotlin
     - camel:core
\ No newline at end of file
diff --git a/deploy/resources.go b/deploy/resources.go
index 7749be3..a3274cb 100644
--- a/deploy/resources.go
+++ b/deploy/resources.go
@@ -2467,12 +2467,12 @@ metadata:
     app: "camel-k"
 
 `
-	Resources["platform-integration-context-core.yaml"] =
+	Resources["platform-integration-context-groovy.yaml"] =
 		`
 apiVersion: camel.apache.org/v1alpha1
 kind: IntegrationContext
 metadata:
-  name: core
+  name: groovy
   labels:
     app: "camel-k"
     camel.apache.org/context.created.by.kind: Operator
@@ -2480,23 +2480,42 @@ metadata:
     camel.apache.org/context.type: platform
 spec:
   dependencies:
+    - runtime:jvm
+    - runtime:groovy
     - camel:core
 `
-	Resources["platform-integration-context-groovy.yaml"] =
+	Resources["platform-integration-context-jvm.yaml"] =
 		`
 apiVersion: camel.apache.org/v1alpha1
 kind: IntegrationContext
 metadata:
-  name: groovy
+  name: jvm
   labels:
     app: "camel-k"
     camel.apache.org/context.created.by.kind: Operator
-    camel.apache.org/context.created.by.name: core
+    camel.apache.org/context.created.by.name: jvm
     camel.apache.org/context.type: platform
 spec:
   dependencies:
+    - runtime:jvm
+    - camel:core
+`
+	Resources["platform-integration-context-kotlin.yaml"] =
+		`
+apiVersion: camel.apache.org/v1alpha1
+kind: IntegrationContext
+metadata:
+  name: kotlin
+  labels:
+    app: "camel-k"
+    camel.apache.org/context.created.by.kind: Operator
+    camel.apache.org/context.created.by.name: jvm
+    camel.apache.org/context.type: platform
+spec:
+  dependencies:
+    - runtime:jvm
+    - runtime:kotlin
     - camel:core
-    - camel:groovy
 `
 	Resources["user-cluster-role.yaml"] =
 		`
diff --git a/pkg/apis/camel/v1alpha1/types.go b/pkg/apis/camel/v1alpha1/types.go
index fa27617..114942f 100644
--- a/pkg/apis/camel/v1alpha1/types.go
+++ b/pkg/apis/camel/v1alpha1/types.go
@@ -77,6 +77,8 @@ const (
 	LanguageJavaScript Language = "js"
 	// LanguageXML --
 	LanguageXML Language = "xml"
+	// LanguageKotlin --
+	LanguageKotlin Language = "kts"
 )
 
 // IntegrationStatus --
diff --git a/pkg/build/assemble/maven_assembler.go b/pkg/build/assemble/maven_assembler.go
index 9094181..9b2674e 100644
--- a/pkg/build/assemble/maven_assembler.go
+++ b/pkg/build/assemble/maven_assembler.go
@@ -162,6 +162,10 @@ func generateProject(source *build.Request) (maven.Project, error) {
 			gav := strings.Replace(mid, "/", ":", -1)
 
 			deps.AddEncodedGAV(gav)
+		} else if strings.HasPrefix(d, "runtime:") {
+			artifactID := strings.Replace(d, "runtime:", "camel-k-runtime-", 1)
+
+			deps.AddGAV("org.apache.camel.k", artifactID, version.Version)
 		} else {
 			return maven.Project{}, fmt.Errorf("unknown dependency type: %s", d)
 		}
diff --git a/pkg/client/cmd/completion_bash.go b/pkg/client/cmd/completion_bash.go
index c689f8e..513a821 100644
--- a/pkg/client/cmd/completion_bash.go
+++ b/pkg/client/cmd/completion_bash.go
@@ -50,24 +50,29 @@ __kamel_dependency_type() {
         COMPREPLY=( $( compgen -W "${type_list}" -- "$cur") )
         ;;
     m*)
-        local type_list="mvn:""
+        local type_list="mvn:"
         COMPREPLY=( $( compgen -W "${type_list}" -- "$cur") )
 		compopt -o nospace
         ;;
     f*)
-        local type_list="file:""
+        local type_list="file:"
         COMPREPLY=( $( compgen -W "${type_list}" -- "$cur") )
 		compopt -o nospace
         ;;
     *)
-        local type_list="camel: mvn: file:"
-        COMPREPLY=( $( compgen -W "camel mvn: file:" -- "$cur") )
+        local type_list="camel mvn: file:"
+        COMPREPLY=( $( compgen -W "${type_list}" -- "$cur") )
 	    compopt -o nospace
     esac
 }
 
 __kamel_languages() {
-    local type_list="js groovy java xml"
+    local type_list="js groovy kotlin java xml"
+    COMPREPLY=( $( compgen -W "${type_list}" -- "$cur") )
+}
+
+__kamel_runtimes() {
+    local type_list="jvm groovy kotlin"
     COMPREPLY=( $( compgen -W "${type_list}" -- "$cur") )
 }
 
@@ -195,6 +200,13 @@ func configureKnownBashCompletions(command *cobra.Command) {
 			cobra.BashCompCustom: {"__kamel_languages"},
 		},
 	)
+	configureBashAnnotationForFlag(
+		command,
+		"runtime",
+		map[string][]string{
+			cobra.BashCompCustom: {"__kamel_runtimes"},
+		},
+	)
 }
 
 func configureBashAnnotationForFlag(command *cobra.Command, flagName string, annotations map[string][]string) {
diff --git a/pkg/client/cmd/context_create.go b/pkg/client/cmd/context_create.go
index e95ae73..739fc8d 100644
--- a/pkg/client/cmd/context_create.go
+++ b/pkg/client/cmd/context_create.go
@@ -23,6 +23,8 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/apache/camel-k/pkg/util"
+
 	"github.com/operator-framework/operator-sdk/pkg/sdk"
 
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
@@ -46,6 +48,7 @@ func newContextCreateCmd(rootCmdOptions *RootCmdOptions) *cobra.Command {
 		RunE:  impl.run,
 	}
 
+	cmd.Flags().StringVarP(&impl.runtime, "runtime", "r", "jvm", "Runtime provided by the context")
 	cmd.Flags().StringSliceVarP(&impl.dependencies, "dependency", "d", nil, "Add a dependency")
 	cmd.Flags().StringSliceVarP(&impl.properties, "property", "p", nil, "Add a camel property")
 	cmd.Flags().StringSliceVar(&impl.configmaps, "configmap", nil, "Add a ConfigMap")
@@ -60,6 +63,7 @@ func newContextCreateCmd(rootCmdOptions *RootCmdOptions) *cobra.Command {
 type contextCreateCommand struct {
 	*RootCmdOptions
 
+	runtime      string
 	dependencies []string
 	properties   []string
 	configmaps   []string
@@ -105,6 +109,13 @@ func (command *contextCreateCommand) run(cmd *cobra.Command, args []string) erro
 		}
 	}
 
+	// jvm runtime required by default
+	util.StringSliceUniqueAdd(&ctx.Spec.Dependencies, "runtime:jvm")
+
+	if command.runtime != "" {
+		util.StringSliceUniqueAdd(&ctx.Spec.Dependencies, "runtime:"+command.runtime)
+	}
+
 	for _, item := range command.properties {
 		ctx.Spec.Configuration = append(ctx.Spec.Configuration, v1alpha1.ConfigurationSpec{
 			Type:  "property",
diff --git a/pkg/client/cmd/run.go b/pkg/client/cmd/run.go
index ca4e52f..b9f40eb 100644
--- a/pkg/client/cmd/run.go
+++ b/pkg/client/cmd/run.go
@@ -56,6 +56,7 @@ func newCmdRun(rootCmdOptions *RootCmdOptions) *cobra.Command {
 	}
 
 	cmd.Flags().StringVarP(&options.Language, "language", "l", "", "Programming Language used to write the file")
+	cmd.Flags().StringVarP(&options.Runtime, "runtime", "r", "jvm", "Runtime used by the integration")
 	cmd.Flags().StringVar(&options.IntegrationName, "name", "", "The integration name")
 	cmd.Flags().StringSliceVarP(&options.Dependencies, "dependency", "d", nil, "The integration dependency")
 	cmd.Flags().BoolVarP(&options.Wait, "wait", "w", false, "Waits for the integration to be running")
@@ -78,6 +79,7 @@ type runCmdOptions struct {
 	*RootCmdOptions
 	IntegrationContext        string
 	Language                  string
+	Runtime                   string
 	IntegrationName           string
 	Dependencies              []string
 	Properties                []string
@@ -269,13 +271,22 @@ func (o *runCmdOptions) updateIntegrationCode(filename string) (*v1alpha1.Integr
 		}
 	}
 
-	// special handling for groovy
-	// TODO: we should define handlers for languages and/or file extensions
-	if o.Language == "groovy" && !util.StringSliceExists(o.Dependencies, "camel:groovy") {
-		integration.Spec.Dependencies = append(integration.Spec.Dependencies, "camel:groovy")
+	if o.Language == "groovy" || strings.HasSuffix(filename, ".groovy") {
+		util.StringSliceUniqueAdd(&integration.Spec.Dependencies, "runtime:groovy")
 	}
-	if o.Language == "" && strings.HasSuffix(filename, ".groovy") {
-		integration.Spec.Dependencies = append(integration.Spec.Dependencies, "camel:groovy")
+	if o.Language == "kotlin" || strings.HasSuffix(filename, ".kts") {
+		util.StringSliceUniqueAdd(&integration.Spec.Dependencies, "runtime:kotlin")
+	}
+
+	// jvm runtime required by default
+	util.StringSliceUniqueAdd(&integration.Spec.Dependencies, "runtime:jvm")
+
+	if o.Runtime != "" {
+		util.StringSliceUniqueAdd(&integration.Spec.Dependencies, "runtime:"+o.Runtime)
+	}
+
+	switch o.Runtime {
+
 	}
 
 	for _, item := range o.Properties {
diff --git a/pkg/discover/dependencies.go b/pkg/discover/dependencies.go
index 82ef757..b6daa63 100644
--- a/pkg/discover/dependencies.go
+++ b/pkg/discover/dependencies.go
@@ -18,11 +18,12 @@ limitations under the License.
 package discover
 
 import (
-	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
-	"github.com/apache/camel-k/pkg/util/camel"
 	"regexp"
 	"sort"
 	"strings"
+
+	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
+	"github.com/apache/camel-k/pkg/util/camel"
 )
 
 var (
@@ -65,6 +66,8 @@ func getRegexpsForLanguage(language v1alpha1.Language) []*regexp.Regexp {
 		return []*regexp.Regexp{singleQuotedURI, doubleQuotedURI}
 	case v1alpha1.LanguageJavaScript:
 		return []*regexp.Regexp{singleQuotedURI, doubleQuotedURI}
+	case v1alpha1.LanguageKotlin:
+		return []*regexp.Regexp{doubleQuotedURI}
 	}
 	return []*regexp.Regexp{}
 }
diff --git a/pkg/discover/language.go b/pkg/discover/language.go
index e38d583..cafb6c3 100644
--- a/pkg/discover/language.go
+++ b/pkg/discover/language.go
@@ -19,8 +19,9 @@ limitations under the License.
 package discover
 
 import (
-	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
 	"strings"
+
+	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
 )
 
 // Language discovers the code language from file extension if not set
@@ -33,7 +34,8 @@ func Language(source v1alpha1.SourceSpec) v1alpha1.Language {
 		v1alpha1.LanguageJavaClass,
 		v1alpha1.LanguageJavaScript,
 		v1alpha1.LanguageGroovy,
-		v1alpha1.LanguageJavaScript} {
+		v1alpha1.LanguageJavaScript,
+		v1alpha1.LanguageKotlin} {
 
 		if strings.HasSuffix(source.Name, "."+string(l)) {
 			return l
diff --git a/pkg/install/operator.go b/pkg/install/operator.go
index 0f6039c..eb3ac4c 100644
--- a/pkg/install/operator.go
+++ b/pkg/install/operator.go
@@ -30,7 +30,7 @@ func Operator(namespace string) error {
 
 // Platform installs the platform custom resource
 func Platform(namespace string) error {
-	return Resource(namespace,"platform-cr.yaml")
+	return Resource(namespace, "platform-cr.yaml")
 }
 
 // Example --
diff --git a/pkg/stub/action/integration/initialize.go b/pkg/stub/action/integration/initialize.go
index d9ab6c0..f0b5e7f 100644
--- a/pkg/stub/action/integration/initialize.go
+++ b/pkg/stub/action/integration/initialize.go
@@ -22,6 +22,8 @@ import (
 	"github.com/sirupsen/logrus"
 	"sort"
 
+	"github.com/apache/camel-k/pkg/util"
+
 	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
 	"github.com/apache/camel-k/pkg/discover"
 	"github.com/apache/camel-k/pkg/util/digest"
@@ -46,7 +48,7 @@ func (action *initializeAction) CanHandle(integration *v1alpha1.Integration) boo
 	return integration.Status.Phase == ""
 }
 
-// Handle handles the integratios
+// Handle handles the integrations
 func (action *initializeAction) Handle(integration *v1alpha1.Integration) error {
 	// The integration platform needs to be ready before starting to create integrations
 	if pl, err := platform.GetCurrentPlatform(integration.Namespace); err != nil || pl.Status.Phase != v1alpha1.IntegrationPlatformPhaseReady {
@@ -63,6 +65,11 @@ func (action *initializeAction) Handle(integration *v1alpha1.Integration) error
 	// set the correct language
 	language := discover.Language(target.Spec.Source)
 	target.Spec.Source.Language = language
+
+	if !util.StringSliceExists(target.Spec.Dependencies, "camel:core") {
+		target.Spec.Dependencies = append(target.Spec.Dependencies, "camel:core")
+	}
+
 	// discover dependencies
 	if target.Spec.DependenciesAutoDiscovery == nil {
 		var autoDiscoveryDependencies = true
diff --git a/pkg/stub/action/platform/create.go b/pkg/stub/action/platform/create.go
index c313ccf..0933e2f 100644
--- a/pkg/stub/action/platform/create.go
+++ b/pkg/stub/action/platform/create.go
@@ -24,6 +24,12 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
+var resources = []string{
+	"platform-integration-context-jvm.yaml",
+	"platform-integration-context-groovy.yaml",
+	"platform-integration-context-kotlin.yaml",
+}
+
 // NewCreateAction returns a action that creates resources needed by the platform
 func NewCreateAction() Action {
 	return &createAction{}
@@ -41,7 +47,7 @@ func (action *createAction) CanHandle(platform *v1alpha1.IntegrationPlatform) bo
 }
 
 func (action *createAction) Handle(platform *v1alpha1.IntegrationPlatform) error {
-	err := install.Resources(platform.Namespace, "platform-integration-context-core.yaml", "platform-integration-context-groovy.yaml")
+	err := install.Resources(platform.Namespace, resources...)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/util/util.go b/pkg/util/util.go
index c2a2631..7792210 100644
--- a/pkg/util/util.go
+++ b/pkg/util/util.go
@@ -38,3 +38,16 @@ func StringSliceExists(slice []string, item string) bool {
 
 	return false
 }
+
+// StringSliceUniqueAdd append the given item if not already present in the slice
+func StringSliceUniqueAdd(slice *[]string, item string) bool {
+	for _, i := range *slice {
+		if i == item {
+			return false
+		}
+	}
+
+	*slice = append(*slice, item)
+
+	return true
+}
diff --git a/runtime/examples/kotlin-routes.kts b/runtime/examples/kotlin-routes.kts
new file mode 100644
index 0000000..8c49d44
--- /dev/null
+++ b/runtime/examples/kotlin-routes.kts
@@ -0,0 +1,20 @@
+//
+// To run this integrations use:
+//
+//     kamel run --runtime kotlin runtime/examples/kotlin-routes.kts
+//
+// Or leveraging runtime detection
+//
+//     kamel run runtime/examples/kotlin-routes.kts
+//
+
+val rnd = java.util.Random()
+
+from("timer:kotlin?period=1s")
+    .routeId("kotlin")
+    .setBody()
+        .constant("Hello Camel K!")
+    .process().message {
+        m -> m.headers["RandomValue"] = rnd.nextInt()
+    }
+    .to("log:info?showHeaders=true")
\ No newline at end of file
diff --git a/runtime/groovy/.gitignore b/runtime/groovy/.gitignore
new file mode 100644
index 0000000..ed92983
--- /dev/null
+++ b/runtime/groovy/.gitignore
@@ -0,0 +1,10 @@
+target
+
+*.iml
+
+.idea
+.project
+.metadata
+.settings
+.factorypath
+.classpath
diff --git a/runtime/groovy/pom.xml b/runtime/groovy/pom.xml
new file mode 100644
index 0000000..a3fb478
--- /dev/null
+++ b/runtime/groovy/pom.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>org.apache.camel.k</groupId>
+        <artifactId>camel-k-runtime-parent</artifactId>
+        <version>0.0.3-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>camel-k-runtime-groovy</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.camel.k</groupId>
+            <artifactId>camel-k-runtime-jvm</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-groovy</artifactId>
+        </dependency>
+
+        <!-- ****************************** -->
+        <!--                                -->
+        <!-- TESTS                          -->
+        <!--                                -->
+        <!-- ****************************** -->
+
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <version>${assertj.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/runtime/groovy/src/main/java/org/apache/camel/k/groovy/GroovyRoutesLoader.java b/runtime/groovy/src/main/java/org/apache/camel/k/groovy/GroovyRoutesLoader.java
new file mode 100644
index 0000000..ee8a512
--- /dev/null
+++ b/runtime/groovy/src/main/java/org/apache/camel/k/groovy/GroovyRoutesLoader.java
@@ -0,0 +1,63 @@
+/**
+ * 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 org.apache.camel.k.groovy;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.Collections;
+import java.util.List;
+
+import groovy.lang.Binding;
+import groovy.lang.GroovyShell;
+import groovy.util.DelegatingScript;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.k.jvm.Language;
+import org.apache.camel.k.jvm.RoutesLoader;
+import org.apache.camel.k.jvm.dsl.Scripting;
+import org.apache.camel.util.ResourceHelper;
+import org.codehaus.groovy.control.CompilerConfiguration;
+
+public class GroovyRoutesLoader implements RoutesLoader {
+    @Override
+    public List<Language> getSupportedLanguages() {
+        return Collections.singletonList(Language.Groovy);
+    }
+
+    @Override
+    public RouteBuilder load(String resource) throws Exception {
+        return new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                CompilerConfiguration cc = new CompilerConfiguration();
+                cc.setScriptBaseClass(DelegatingScript.class.getName());
+
+                ClassLoader cl = Thread.currentThread().getContextClassLoader();
+                GroovyShell sh = new GroovyShell(cl, new Binding(), cc);
+
+                try (InputStream is = ResourceHelper.resolveMandatoryResourceAsInputStream(getContext(), resource)) {
+                    Reader reader = new InputStreamReader(is);
+                    DelegatingScript script = (DelegatingScript) sh.parse(reader);
+
+                    // set the delegate target
+                    script.setDelegate(new Scripting(this));
+                    script.run();
+                }
+            }
+        };
+    }
+}
diff --git a/runtime/groovy/src/main/resources/META-INF/services/org.apache.camel.k.jvm.RoutesLoader b/runtime/groovy/src/main/resources/META-INF/services/org.apache.camel.k.jvm.RoutesLoader
new file mode 100644
index 0000000..db214e0
--- /dev/null
+++ b/runtime/groovy/src/main/resources/META-INF/services/org.apache.camel.k.jvm.RoutesLoader
@@ -0,0 +1 @@
+org.apache.camel.k.groovy.GroovyRoutesLoader
\ No newline at end of file
diff --git a/runtime/groovy/src/test/java/org/apache/camel/k/groovy/RoutesLoadersTest.java b/runtime/groovy/src/test/java/org/apache/camel/k/groovy/RoutesLoadersTest.java
new file mode 100644
index 0000000..8a64faa
--- /dev/null
+++ b/runtime/groovy/src/test/java/org/apache/camel/k/groovy/RoutesLoadersTest.java
@@ -0,0 +1,47 @@
+/**
+ * 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 org.apache.camel.k.groovy;
+
+import java.util.List;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.k.jvm.RoutesLoader;
+import org.apache.camel.k.jvm.RoutesLoaders;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.model.ToDefinition;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class RoutesLoadersTest {
+    @Test
+    public void testLoadGroovy() throws Exception {
+        String resource = "classpath:routes.groovy";
+        RoutesLoader loader = RoutesLoaders.loaderFor(resource, null);
+        RouteBuilder builder = loader.load(resource);
+
+        assertThat(loader).isInstanceOf(GroovyRoutesLoader.class);
+        assertThat(builder).isNotNull();
+
+        builder.configure();
+
+        List<RouteDefinition> routes = builder.getRouteCollection().getRoutes();
+        assertThat(routes).hasSize(1);
+        assertThat(routes.get(0).getInputs().get(0).getEndpointUri()).isEqualTo("timer:tick");
+        assertThat(routes.get(0).getOutputs().get(0)).isInstanceOf(ToDefinition.class);
+    }
+}
diff --git a/runtime/jvm/src/test/resources/routes.groovy b/runtime/groovy/src/test/resources/routes.groovy
similarity index 100%
rename from runtime/jvm/src/test/resources/routes.groovy
rename to runtime/groovy/src/test/resources/routes.groovy
diff --git a/runtime/jvm/pom.xml b/runtime/jvm/pom.xml
index 2ea58ff..9dc3918 100644
--- a/runtime/jvm/pom.xml
+++ b/runtime/jvm/pom.xml
@@ -11,7 +11,18 @@
 
     <artifactId>camel-k-runtime-jvm</artifactId>
 
+    <properties>
+        <kotlin.version>1.2.71</kotlin.version>
+    </properties>
+
     <dependencies>
+
+        <!-- ****************************** -->
+        <!--                                -->
+        <!-- RUNTIME                        -->
+        <!--                                -->
+        <!-- ****************************** -->
+
         <dependency>
             <groupId>org.apache.camel</groupId>
             <artifactId>camel-core</artifactId>
@@ -25,6 +36,7 @@
             <groupId>org.apache.logging.log4j</groupId>
             <artifactId>log4j-slf4j-impl</artifactId>
             <version>${log4j2.version}</version>
+            <scope>runtime</scope>
         </dependency>
         <dependency>
             <groupId>org.jooq</groupId>
@@ -41,12 +53,12 @@
             <artifactId>commons-lang3</artifactId>
             <version>${commons-lang.version}</version>
         </dependency>
-        <dependency>
-            <groupId>org.codehaus.groovy</groupId>
-            <artifactId>groovy</artifactId>
-            <version>${groovy.version}</version>
-            <optional>true</optional>
-        </dependency>
+
+        <!-- ****************************** -->
+        <!--                                -->
+        <!-- TESTS                          -->
+        <!--                                -->
+        <!-- ****************************** -->
 
         <dependency>
             <groupId>junit</groupId>
diff --git a/runtime/jvm/src/main/java/org/apache/camel/k/jvm/Language.java b/runtime/jvm/src/main/java/org/apache/camel/k/jvm/Language.java
index a2a8377..d041faa 100644
--- a/runtime/jvm/src/main/java/org/apache/camel/k/jvm/Language.java
+++ b/runtime/jvm/src/main/java/org/apache/camel/k/jvm/Language.java
@@ -16,29 +16,45 @@
  */
 package org.apache.camel.k.jvm;
 
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
 import org.apache.commons.lang3.StringUtils;
 
 public enum Language {
-    Unknow("", Collections.emptyList()),
-    JavaClass("class", Collections.singletonList("class")),
-    JavaSource("java", Collections.singletonList("java")),
-    JavaScript("js", Collections.singletonList("js")),
-    Groovy("groovy", Collections.singletonList("groovy")),
-    Xml("xml", Collections.singletonList("xml"));
+    Unknown(
+        Collections.emptyList(),
+        Collections.emptyList()),
+    JavaClass(
+        Collections.singletonList("class"),
+        Collections.singletonList("class")),
+    JavaSource(
+        Collections.singletonList("java"),
+        Collections.singletonList("java")),
+    JavaScript(
+        Arrays.asList("js", "javascript"),
+        Collections.singletonList("js")),
+    Groovy(
+        Collections.singletonList("groovy"),
+        Collections.singletonList("groovy")),
+    Xml(
+        Collections.singletonList("xml"),
+        Collections.singletonList("xml")),
+    Kotlin(
+        Arrays.asList("kotlin", "kts"),
+        Collections.singletonList("kts"));
 
-    private final String name;
+    private final List<String> names;
     private final List<String> extensions;
 
-    Language(String name, List<String> extensions) {
-        this.name = name;
+    Language(List<String> names, List<String> extensions) {
+        this.names = names;
         this.extensions = extensions;
     }
 
-    public String getName() {
-        return name;
+    public List<String> getNames() {
+        return names;
     }
 
     public List<String> getExtensions() {
@@ -47,12 +63,12 @@ public enum Language {
 
     public static Language fromLanguageName(String name) {
         for (Language language: values()) {
-            if (language.getName().equals(name)) {
+            if (language.getNames().contains(name)) {
                 return language;
             }
         }
 
-        return Unknow;
+        return Unknown;
     }
 
     public static Language fromResource(String resource) {
@@ -66,6 +82,6 @@ public enum Language {
             }
         }
 
-        return Unknow;
+        return Unknown;
     }
 }
diff --git a/runtime/jvm/src/main/java/org/apache/camel/k/jvm/RoutesLoaders.java b/runtime/jvm/src/main/java/org/apache/camel/k/jvm/RoutesLoaders.java
index 448c2bb..2581d48 100644
--- a/runtime/jvm/src/main/java/org/apache/camel/k/jvm/RoutesLoaders.java
+++ b/runtime/jvm/src/main/java/org/apache/camel/k/jvm/RoutesLoaders.java
@@ -18,10 +18,10 @@ package org.apache.camel.k.jvm;
 
 import java.io.InputStream;
 import java.io.InputStreamReader;
-import java.io.Reader;
 import java.nio.charset.StandardCharsets;
 import java.util.Collections;
 import java.util.List;
+import java.util.ServiceLoader;
 import java.util.function.Function;
 import java.util.function.Supplier;
 import javax.script.Bindings;
@@ -30,12 +30,9 @@ import javax.script.ScriptEngineManager;
 import javax.script.SimpleBindings;
 import javax.xml.bind.UnmarshalException;
 
-import groovy.lang.Binding;
-import groovy.lang.GroovyShell;
-import groovy.util.DelegatingScript;
 import org.apache.camel.CamelContext;
-import org.apache.camel.Component;
 import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.k.jvm.dsl.Components;
 import org.apache.camel.model.RouteDefinition;
 import org.apache.camel.model.rest.RestConfigurationDefinition;
 import org.apache.camel.model.rest.RestDefinition;
@@ -43,7 +40,6 @@ import org.apache.camel.util.ObjectHelper;
 import org.apache.camel.util.ResourceHelper;
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.StringUtils;
-import org.codehaus.groovy.control.CompilerConfiguration;
 import org.joor.Reflect;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -51,8 +47,13 @@ import org.slf4j.LoggerFactory;
 import static org.apache.camel.k.jvm.Constants.SCHEME_CLASSPATH;
 import static org.apache.camel.k.jvm.Constants.SCHEME_FILE;
 
-public enum RoutesLoaders implements RoutesLoader {
-    JavaClass {
+public final class RoutesLoaders {
+    private static final Logger LOGGER = LoggerFactory.getLogger(RoutesLoaders.class);
+
+    private RoutesLoaders() {
+    }
+
+    public static class JavaClass implements RoutesLoader {
         @Override
         public List<Language> getSupportedLanguages() {
             return Collections.singletonList(Language.JavaClass);
@@ -72,8 +73,9 @@ public enum RoutesLoaders implements RoutesLoader {
 
             return (RouteBuilder)type.newInstance();
         }
-    },
-    JavaSource {
+    }
+
+    public static class JavaSource implements RoutesLoader {
         @Override
         public List<Language> getSupportedLanguages() {
             return Collections.singletonList(Language.JavaSource);
@@ -93,15 +95,16 @@ public enum RoutesLoaders implements RoutesLoader {
                         }
 
                         // Wrap routes builder
-                        includeRoutes(
+                        addRoutes(
                             Reflect.compile(name, IOUtils.toString(is, StandardCharsets.UTF_8)).create().get()
                         );
                     }
                 }
             };
         }
-    },
-    JavaScript {
+    }
+
+    public static class JavaScript implements RoutesLoader {
         @Override
         public List<Language> getSupportedLanguages() {
             return Collections.singletonList(Language.JavaScript);
@@ -119,6 +122,7 @@ public enum RoutesLoaders implements RoutesLoader {
 
                     // Exposed to the underlying script, but maybe better to have
                     // a nice dsl
+                    bindings.put("builder", this);
                     bindings.put("context", context);
                     bindings.put("components", new Components(context));
                     bindings.put("from", (Function<String, RouteDefinition>) uri -> from(uri));
@@ -131,37 +135,9 @@ public enum RoutesLoaders implements RoutesLoader {
                 }
             };
         }
-    },
-    Groovy {
-        @Override
-        public List<Language> getSupportedLanguages() {
-            return Collections.singletonList(Language.Groovy);
-        }
-
-        @Override
-        public RouteBuilder load(String resource) throws Exception {
-            return new RouteBuilder() {
-                @Override
-                public void configure() throws Exception {
-                    CompilerConfiguration cc = new CompilerConfiguration();
-                    cc.setScriptBaseClass(DelegatingScript.class.getName());
-
-                    ClassLoader cl = Thread.currentThread().getContextClassLoader();
-                    GroovyShell sh = new GroovyShell(cl, new Binding(), cc);
-
-                    try (InputStream is = ResourceHelper.resolveMandatoryResourceAsInputStream(getContext(), resource)) {
-                        Reader reader = new InputStreamReader(is);
-                        DelegatingScript script = (DelegatingScript) sh.parse(reader);
+    }
 
-                        // set the delegate target
-                        script.setDelegate(new ScriptingDsl(this));
-                        script.run();
-                    }
-                }
-            };
-        }
-    },
-    Xml {
+    public static class Xml implements RoutesLoader {
         @Override
         public List<Language> getSupportedLanguages() {
             return Collections.singletonList(Language.Xml);
@@ -192,9 +168,8 @@ public enum RoutesLoaders implements RoutesLoader {
                 }
             };
         }
-    };
+    }
 
-    private static final Logger LOGGER = LoggerFactory.getLogger(RoutesLoaders.class);
 
     public static RoutesLoader loaderFor(String resource, String languageName) {
         if (!resource.startsWith(SCHEME_CLASSPATH) && !resource.startsWith(SCHEME_FILE)) {
@@ -205,7 +180,7 @@ public enum RoutesLoaders implements RoutesLoader {
             ? Language.fromLanguageName(languageName)
             : Language.fromResource(resource);
 
-        for (RoutesLoader loader: RoutesLoaders.values()) {
+        for (RoutesLoader loader: ServiceLoader.load(RoutesLoader.class)) {
             if (loader.getSupportedLanguages().contains(language)) {
                 return loader;
             }
@@ -213,64 +188,4 @@ public enum RoutesLoaders implements RoutesLoader {
 
         throw new IllegalArgumentException("Unable to find loader for: resource=" + resource + " language=" + languageName);
     }
-
-    // ********************************
-    //
-    // Helpers
-    //
-    // TODO: move to a dedicate class
-    // ********************************
-
-
-    public static class Components {
-        private CamelContext context;
-
-        public Components(CamelContext context) {
-            this.context = context;
-        }
-
-        public Component get(String scheme) {
-            return context.getComponent(scheme, true);
-        }
-
-        public Component put(String scheme, Component instance) {
-            context.addComponent(scheme, instance);
-
-            return instance;
-        }
-
-        public Component make(String scheme, String type) {
-            final Class<?> clazz = context.getClassResolver().resolveClass(type);
-            final Component instance = (Component)context.getInjector().newInstance(clazz);
-
-            context.addComponent(scheme, instance);
-
-            return instance;
-        }
-    }
-
-    private static class ScriptingDsl {
-        private final RouteBuilder builder;
-
-        public final CamelContext context;
-        public final Components components;
-
-        public ScriptingDsl(RouteBuilder builder) {
-            this.builder = builder;
-            this.context = builder.getContext();
-            this.components = new Components(builder.getContext());
-        }
-
-        public RouteDefinition from(String endpoint) {
-            return builder.from(endpoint);
-        }
-
-        public RestDefinition rest() {
-            return builder.rest();
-        }
-
-        public RestConfigurationDefinition restConfiguration() {
-            return builder.restConfiguration();
-        }
-    }
 }
diff --git a/runtime/jvm/src/main/java/org/apache/camel/k/jvm/dsl/Components.java b/runtime/jvm/src/main/java/org/apache/camel/k/jvm/dsl/Components.java
new file mode 100644
index 0000000..bac1390
--- /dev/null
+++ b/runtime/jvm/src/main/java/org/apache/camel/k/jvm/dsl/Components.java
@@ -0,0 +1,47 @@
+/**
+ * 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 org.apache.camel.k.jvm.dsl;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Component;
+
+public class Components {
+    private CamelContext context;
+
+    public Components(CamelContext context) {
+        this.context = context;
+    }
+
+    public Component get(String scheme) {
+        return context.getComponent(scheme, true);
+    }
+
+    public Component put(String scheme, Component instance) {
+        context.addComponent(scheme, instance);
+
+        return instance;
+    }
+
+    public Component make(String scheme, String type) {
+        final Class<?> clazz = context.getClassResolver().resolveClass(type);
+        final Component instance = (Component)context.getInjector().newInstance(clazz);
+
+        context.addComponent(scheme, instance);
+
+        return instance;
+    }
+}
diff --git a/runtime/jvm/src/main/java/org/apache/camel/k/jvm/dsl/Scripting.java b/runtime/jvm/src/main/java/org/apache/camel/k/jvm/dsl/Scripting.java
new file mode 100644
index 0000000..3988290
--- /dev/null
+++ b/runtime/jvm/src/main/java/org/apache/camel/k/jvm/dsl/Scripting.java
@@ -0,0 +1,47 @@
+/**
+ * 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 org.apache.camel.k.jvm.dsl;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.model.rest.RestConfigurationDefinition;
+import org.apache.camel.model.rest.RestDefinition;
+
+public class Scripting {
+    public final RouteBuilder builder;
+    public final CamelContext context;
+    public final Components components;
+
+    public Scripting(RouteBuilder builder) {
+        this.builder = builder;
+        this.context = builder.getContext();
+        this.components = new Components(builder.getContext());
+    }
+
+    public RouteDefinition from(String endpoint) {
+        return builder.from(endpoint);
+    }
+
+    public RestDefinition rest() {
+        return builder.rest();
+    }
+
+    public RestConfigurationDefinition restConfiguration() {
+        return builder.restConfiguration();
+    }
+}
diff --git a/runtime/jvm/src/main/resources/META-INF/services/org.apache.camel.k.jvm.RoutesLoader b/runtime/jvm/src/main/resources/META-INF/services/org.apache.camel.k.jvm.RoutesLoader
new file mode 100644
index 0000000..5a57927
--- /dev/null
+++ b/runtime/jvm/src/main/resources/META-INF/services/org.apache.camel.k.jvm.RoutesLoader
@@ -0,0 +1,4 @@
+org.apache.camel.k.jvm.RoutesLoaders$JavaClass
+org.apache.camel.k.jvm.RoutesLoaders$JavaSource
+org.apache.camel.k.jvm.RoutesLoaders$JavaScript
+org.apache.camel.k.jvm.RoutesLoaders$Xml
diff --git a/runtime/jvm/src/test/java/org/apache/camel/k/jvm/RoutesLoadersTest.java b/runtime/jvm/src/test/java/org/apache/camel/k/jvm/RoutesLoadersTest.java
index 4b2090f..54b492b 100644
--- a/runtime/jvm/src/test/java/org/apache/camel/k/jvm/RoutesLoadersTest.java
+++ b/runtime/jvm/src/test/java/org/apache/camel/k/jvm/RoutesLoadersTest.java
@@ -33,7 +33,7 @@ public class RoutesLoadersTest {
         RoutesLoader loader = RoutesLoaders.loaderFor(resource, null);
         RouteBuilder builder = loader.load(resource);
 
-        assertThat(loader).isSameAs(RoutesLoaders.JavaClass);
+        assertThat(loader).isInstanceOf(RoutesLoaders.JavaClass.class);
         assertThat(builder).isNotNull();
 
         builder.configure();
@@ -50,7 +50,7 @@ public class RoutesLoadersTest {
         RoutesLoader loader = RoutesLoaders.loaderFor(resource, null);
         RouteBuilder builder = loader.load(resource);
 
-        assertThat(loader).isSameAs(RoutesLoaders.JavaSource);
+        assertThat(loader).isInstanceOf(RoutesLoaders.JavaSource.class);
         assertThat(builder).isNotNull();
 
         builder.configure();
@@ -67,7 +67,7 @@ public class RoutesLoadersTest {
         RoutesLoader loader = RoutesLoaders.loaderFor(resource, null);
         RouteBuilder builder = loader.load(resource);
 
-        assertThat(loader).isSameAs(RoutesLoaders.JavaScript);
+        assertThat(loader).isInstanceOf(RoutesLoaders.JavaScript.class);
         assertThat(builder).isNotNull();
 
         builder.configure();
@@ -84,24 +84,7 @@ public class RoutesLoadersTest {
         RoutesLoader loader = RoutesLoaders.loaderFor(resource, "js");
         RouteBuilder builder = loader.load(resource);
 
-        assertThat(loader).isSameAs(RoutesLoaders.JavaScript);
-        assertThat(builder).isNotNull();
-
-        builder.configure();
-
-        List<RouteDefinition> routes = builder.getRouteCollection().getRoutes();
-        assertThat(routes).hasSize(1);
-        assertThat(routes.get(0).getInputs().get(0).getEndpointUri()).isEqualTo("timer:tick");
-        assertThat(routes.get(0).getOutputs().get(0)).isInstanceOf(ToDefinition.class);
-    }
-
-    @Test
-    public void testLoadGroovy() throws Exception {
-        String resource = "classpath:routes.groovy";
-        RoutesLoader loader = RoutesLoaders.loaderFor(resource, null);
-        RouteBuilder builder = loader.load(resource);
-
-        assertThat(loader).isSameAs(RoutesLoaders.Groovy);
+        assertThat(loader).isInstanceOf(RoutesLoaders.JavaScript.class);
         assertThat(builder).isNotNull();
 
         builder.configure();
@@ -118,7 +101,7 @@ public class RoutesLoadersTest {
         RoutesLoader loader = RoutesLoaders.loaderFor(resource, null);
         RouteBuilder builder = loader.load(resource);
 
-        assertThat(loader).isSameAs(RoutesLoaders.Xml);
+        assertThat(loader).isInstanceOf(RoutesLoaders.Xml.class);
         assertThat(builder).isNotNull();
 
         builder.configure();
diff --git a/runtime/kotlin/.gitignore b/runtime/kotlin/.gitignore
new file mode 100644
index 0000000..ed92983
--- /dev/null
+++ b/runtime/kotlin/.gitignore
@@ -0,0 +1,10 @@
+target
+
+*.iml
+
+.idea
+.project
+.metadata
+.settings
+.factorypath
+.classpath
diff --git a/runtime/kotlin/pom.xml b/runtime/kotlin/pom.xml
new file mode 100644
index 0000000..9036f76
--- /dev/null
+++ b/runtime/kotlin/pom.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>org.apache.camel.k</groupId>
+        <artifactId>camel-k-runtime-parent</artifactId>
+        <version>0.0.3-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>camel-k-runtime-kotlin</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.camel.k</groupId>
+            <artifactId>camel-k-runtime-jvm</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jetbrains.kotlin</groupId>
+            <artifactId>kotlin-stdlib-jdk8</artifactId>
+            <version>${kotlin.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jetbrains.kotlin</groupId>
+            <artifactId>kotlin-script-util</artifactId>
+            <version>${kotlin.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jetbrains.kotlin</groupId>
+            <artifactId>kotlin-compiler-embeddable</artifactId>
+            <version>${kotlin.version}</version>
+        </dependency>
+
+        <!-- ****************************** -->
+        <!--                                -->
+        <!-- TESTS                          -->
+        <!--                                -->
+        <!-- ****************************** -->
+
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <version>${assertj.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/runtime/kotlin/src/main/java/org/apache/camel/k/kotlin/KotlinRoutesLoader.java b/runtime/kotlin/src/main/java/org/apache/camel/k/kotlin/KotlinRoutesLoader.java
new file mode 100644
index 0000000..0b887ef
--- /dev/null
+++ b/runtime/kotlin/src/main/java/org/apache/camel/k/kotlin/KotlinRoutesLoader.java
@@ -0,0 +1,77 @@
+/**
+ * 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 org.apache.camel.k.kotlin;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collections;
+import java.util.List;
+import javax.script.Bindings;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import javax.script.SimpleBindings;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.k.jvm.dsl.Components;
+import org.apache.camel.k.jvm.Language;
+import org.apache.camel.k.jvm.RoutesLoader;
+import org.apache.camel.util.ResourceHelper;
+
+public class KotlinRoutesLoader implements RoutesLoader {
+
+    @Override
+    public List<Language> getSupportedLanguages() {
+        return Collections.singletonList(Language.Kotlin);
+    }
+
+    @Override
+    public RouteBuilder load(String resource) throws Exception {
+        return new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                final CamelContext context = getContext();
+                final ScriptEngineManager manager = new ScriptEngineManager();
+                final ScriptEngine engine = manager.getEngineByExtension("kts");
+                final Bindings bindings = new SimpleBindings();
+
+                // Exposed to the underlying script, but maybe better to have
+                // a nice dsl
+                bindings.put("builder", this);
+                bindings.put("context", context);
+                bindings.put("components", new Components(context));
+
+                try (InputStream is = ResourceHelper.resolveMandatoryResourceAsInputStream(context, resource)) {
+                    StringBuilder builder = new StringBuilder();
+
+                    // extract global objects from 'bindings'
+                    builder.append("val builder = bindings[\"builder\"] as org.apache.camel.builder.RouteBuilder").append('\n');
+                    builder.append("val context = bindings[\"context\"] as org.apache.camel.CamelContext").append('\n');
+                    builder.append("val components = bindings[\"components\"] as org.apache.camel.k.jvm.dsl.Components").append('\n');
+
+                    // create aliases for common functions
+                    builder.append("fun from(uri: String): org.apache.camel.model.RouteDefinition = builder.from(uri)").append('\n');
+                    builder.append("fun rest(): org.apache.camel.model.rest.RestDefinition = builder.rest()").append('\n');
+                    builder.append("fun restConfiguration(): org.apache.camel.model.rest.RestConfigurationDefinition = builder.restConfiguration()").append('\n');
+
+                    engine.eval(builder.toString(), bindings);
+                    engine.eval(new InputStreamReader(is), bindings);
+                }
+            }
+        };
+    }
+}
diff --git a/runtime/kotlin/src/main/resources/META-INF/services/javax.script.ScriptEngineFactory b/runtime/kotlin/src/main/resources/META-INF/services/javax.script.ScriptEngineFactory
new file mode 100644
index 0000000..f8f5900
--- /dev/null
+++ b/runtime/kotlin/src/main/resources/META-INF/services/javax.script.ScriptEngineFactory
@@ -0,0 +1 @@
+org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory
\ No newline at end of file
diff --git a/runtime/kotlin/src/main/resources/META-INF/services/org.apache.camel.k.jvm.RoutesLoader b/runtime/kotlin/src/main/resources/META-INF/services/org.apache.camel.k.jvm.RoutesLoader
new file mode 100644
index 0000000..83c3f09
--- /dev/null
+++ b/runtime/kotlin/src/main/resources/META-INF/services/org.apache.camel.k.jvm.RoutesLoader
@@ -0,0 +1 @@
+org.apache.camel.k.kotlin.KotlinRoutesLoader
\ No newline at end of file
diff --git a/runtime/kotlin/src/test/java/org/apache/camel/k/kotlin/RoutesLoadersTest.java b/runtime/kotlin/src/test/java/org/apache/camel/k/kotlin/RoutesLoadersTest.java
new file mode 100644
index 0000000..5c23d67
--- /dev/null
+++ b/runtime/kotlin/src/test/java/org/apache/camel/k/kotlin/RoutesLoadersTest.java
@@ -0,0 +1,50 @@
+/**
+ * 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 org.apache.camel.k.kotlin;
+
+import java.util.List;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.k.jvm.RoutesLoader;
+import org.apache.camel.k.jvm.RoutesLoaders;
+import org.apache.camel.model.ProcessDefinition;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.model.ToDefinition;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class RoutesLoadersTest {
+
+    @Test
+    public void testLoadKotlin() throws Exception {
+        String resource = "classpath:routes.kts";
+        RoutesLoader loader = RoutesLoaders.loaderFor(resource, null);
+        RouteBuilder builder = loader.load(resource);
+
+        assertThat(loader).isInstanceOf(KotlinRoutesLoader.class);
+        assertThat(builder).isNotNull();
+
+        builder.configure();
+
+        List<RouteDefinition> routes = builder.getRouteCollection().getRoutes();
+        assertThat(routes).hasSize(1);
+        assertThat(routes.get(0).getInputs().get(0).getEndpointUri()).isEqualTo("timer:tick");
+        assertThat(routes.get(0).getOutputs().get(0)).isInstanceOf(ProcessDefinition.class);
+        assertThat(routes.get(0).getOutputs().get(1)).isInstanceOf(ToDefinition.class);
+    }
+}
diff --git a/runtime/kotlin/src/test/resources/routes.kts b/runtime/kotlin/src/test/resources/routes.kts
new file mode 100644
index 0000000..22b0e14
--- /dev/null
+++ b/runtime/kotlin/src/test/resources/routes.kts
@@ -0,0 +1,21 @@
+
+// ********************
+//
+// setup
+//
+// ********************
+
+//val builder = bindings["builder"] as org.apache.camel.builder.RouteBuilder
+//fun from(uri: String): org.apache.camel.model.RouteDefinition = builder.from(uri)
+
+// ********************
+//
+// routes
+//
+// ********************
+
+from("timer:tick")
+    .process().message {
+        m -> m.headers["MyHeader"] = "MyHeaderValue"
+    }
+    .to("log:info")
\ No newline at end of file
diff --git a/runtime/pom.xml b/runtime/pom.xml
index 0948ca3..8278b4f 100644
--- a/runtime/pom.xml
+++ b/runtime/pom.xml
@@ -24,6 +24,7 @@
         <log4j2.version>2.11.0</log4j2.version>
         <slf4j.version>1.7.25</slf4j.version>
         <groovy.version>2.5.2</groovy.version>
+        <kotlin.version>1.2.71</kotlin.version>
         <snakeyaml.version>1.23</snakeyaml.version>
 
         <fabric8-maven-plugin.version>3.5.40</fabric8-maven-plugin.version>
@@ -55,6 +56,8 @@
 
     <modules>
         <module>jvm</module>
+        <module>groovy</module>
+        <module>kotlin</module>
         <module>catalog-builder</module>
         <module>dependency-lister</module>
     </modules>